Skip to main content

crawlkit_parser/
forms.rs

1//! 表单提取与分析模块
2//!
3//! 基于 halldyll-parser 的表单提取逻辑改写。
4//! 提供从 HTML 文档中提取表单、表单字段、标签关联、表单类型检测等功能。
5
6use scraper::{ElementRef, Html};
7use serde::{Deserialize, Serialize};
8
9use crate::selector::try_parse_selector;
10
11// ============================================================================
12// 表单方法
13// ============================================================================
14
15/// 表单的 HTTP 方法
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18#[derive(Default)]
19pub enum FormMethod {
20    /// GET 方法
21    #[default]
22    Get,
23    /// POST 方法
24    Post,
25    /// dialog 方法(HTML5.2)
26    Dialog,
27}
28
29
30impl FormMethod {
31    /// 从字符串解析表单方法(不区分大小写)
32    pub fn from_str(s: &str) -> Self {
33        match s.trim().to_lowercase().as_str() {
34            "post" => Self::Post,
35            "dialog" => Self::Dialog,
36            _ => Self::Get,
37        }
38    }
39}
40
41// ============================================================================
42// 表单类型
43// ============================================================================
44
45/// 表单的类型(根据字段和上下文推断)
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
47#[serde(rename_all = "snake_case")]
48pub enum FormType {
49    /// 登录表单
50    Login,
51    /// 注册表单
52    Registration,
53    /// 搜索表单
54    Search,
55    /// 联系表单
56    Contact,
57    /// 新闻订阅表单
58    Newsletter,
59    /// 密码重置表单
60    PasswordReset,
61    /// 结账/支付表单
62    Checkout,
63    /// 评论表单
64    Comment,
65    /// 文件上传表单
66    Upload,
67    /// 未知类型
68    Unknown,
69}
70
71impl FormType {
72    /// 返回所有已知表单类型
73    pub fn all() -> &'static [Self] {
74        &[
75            Self::Login,
76            Self::Registration,
77            Self::Search,
78            Self::Contact,
79            Self::Newsletter,
80            Self::PasswordReset,
81            Self::Checkout,
82            Self::Comment,
83            Self::Upload,
84            Self::Unknown,
85        ]
86    }
87}
88
89// ============================================================================
90// 字段类型
91// ============================================================================
92
93/// 表单字段的 HTML 输入类型
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(rename_all = "snake_case")]
96pub enum FieldType {
97    /// 纯文本
98    Text,
99    /// 邮箱
100    Email,
101    /// 密码
102    Password,
103    /// 数字
104    Number,
105    /// 电话
106    Tel,
107    /// URL
108    Url,
109    /// 搜索
110    Search,
111    /// 多行文本域
112    Textarea,
113    /// 下拉选择
114    Select,
115    /// 复选框
116    Checkbox,
117    /// 单选按钮
118    Radio,
119    /// 文件上传
120    File,
121    /// 隐藏字段
122    Hidden,
123    /// 提交按钮
124    Submit,
125    /// 普通按钮
126    Button,
127    /// 日期
128    Date,
129    /// 时间
130    Time,
131    /// 颜色
132    Color,
133    /// 范围滑块
134    Range,
135    /// 未知类型
136    Unknown,
137}
138
139impl FieldType {
140    /// 从 HTML `type` 属性解析字段类型
141    pub fn from_html_type(type_attr: Option<&str>) -> Self {
142        match type_attr {
143            Some(t) => match t.trim().to_lowercase().as_str() {
144                "text" => Self::Text,
145                "email" => Self::Email,
146                "password" => Self::Password,
147                "number" => Self::Number,
148                "tel" => Self::Tel,
149                "url" => Self::Url,
150                "search" => Self::Search,
151                "checkbox" => Self::Checkbox,
152                "radio" => Self::Radio,
153                "file" => Self::File,
154                "hidden" => Self::Hidden,
155                "submit" => Self::Submit,
156                "button" => Self::Button,
157                "date" => Self::Date,
158                "time" => Self::Time,
159                "color" => Self::Color,
160                "range" => Self::Range,
161                _ => Self::Unknown,
162            },
163            None => Self::Text,
164        }
165    }
166
167    /// 判断是否为输入值型字段(用户可输入数据的字段)
168    pub fn is_input(&self) -> bool {
169        matches!(
170            self,
171            Self::Text
172                | Self::Email
173                | Self::Password
174                | Self::Number
175                | Self::Tel
176                | Self::Url
177                | Self::Search
178                | Self::Textarea
179                | Self::Date
180                | Self::Time
181                | Self::Color
182                | Self::Range
183        )
184    }
185
186    /// 判断是否为选择型字段
187    pub fn is_selectable(&self) -> bool {
188        matches!(self, Self::Select | Self::Checkbox | Self::Radio)
189    }
190
191    /// 判断是否为按钮型字段
192    pub fn is_button(&self) -> bool {
193        matches!(self, Self::Submit | Self::Button)
194    }
195}
196
197// ============================================================================
198// 选择选项
199// ============================================================================
200
201/// `<select>` 元素的选项
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct SelectOption {
204    /// 选项值
205    pub value: String,
206    /// 选项显示文本
207    pub label: String,
208    /// 是否默认选中
209    pub selected: bool,
210}
211
212impl SelectOption {
213    /// 创建新选项
214    pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
215        Self {
216            value: value.into(),
217            label: label.into(),
218            selected: false,
219        }
220    }
221}
222
223// ============================================================================
224// 表单字段
225// ============================================================================
226
227/// 表单中的单个字段
228#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct FormField {
230    /// 字段 name 属性
231    pub name: Option<String>,
232    /// 字段 id 属性
233    pub id: Option<String>,
234    /// 字段类型
235    pub field_type: FieldType,
236    /// 关联的 label 文本
237    pub label: Option<String>,
238    /// placeholder 属性
239    pub placeholder: Option<String>,
240    /// 默认值
241    pub value: Option<String>,
242    /// 是否必填
243    pub required: bool,
244    /// 是否禁用
245    pub disabled: bool,
246    /// 是否只读
247    pub readonly: bool,
248    /// autocomplete 属性值
249    pub autocomplete: Option<String>,
250    /// 输入模式校验正则
251    pub pattern: Option<String>,
252    /// 最小长度
253    pub min_length: Option<usize>,
254    /// 最大长度
255    pub max_length: Option<usize>,
256    /// 选择型字段的选项列表
257    pub options: Vec<SelectOption>,
258}
259
260impl FormField {
261    /// 创建新的表单字段
262    pub fn new(name: impl Into<String>, field_type: FieldType) -> Self {
263        Self {
264            name: Some(name.into()),
265            id: None,
266            field_type,
267            label: None,
268            placeholder: None,
269            value: None,
270            required: false,
271            disabled: false,
272            readonly: false,
273            autocomplete: None,
274            pattern: None,
275            min_length: None,
276            max_length: None,
277            options: Vec::new(),
278        }
279    }
280
281    /// 创建一个匿名字段(无 name 属性)
282    pub fn anonymous(field_type: FieldType) -> Self {
283        Self {
284            name: None,
285            id: None,
286            field_type,
287            label: None,
288            placeholder: None,
289            value: None,
290            required: false,
291            disabled: false,
292            readonly: false,
293            autocomplete: None,
294            pattern: None,
295            min_length: None,
296            max_length: None,
297            options: Vec::new(),
298        }
299    }
300}
301
302// ============================================================================
303// 表单
304// ============================================================================
305
306/// 提取出的 HTML 表单结构
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct Form {
309    /// 表单 id 属性
310    pub id: Option<String>,
311    /// 表单 name 属性
312    pub name: Option<String>,
313    /// 提交目标 URL(action 属性)
314    pub action: Option<String>,
315    /// 提交方法
316    pub method: FormMethod,
317    /// 编码类型
318    pub enctype: Option<String>,
319    /// 表单中的字段列表
320    pub fields: Vec<FormField>,
321    /// 推断的表单类型
322    pub form_type: FormType,
323    /// 是否包含 CSRF 令牌字段
324    pub has_csrf: bool,
325    /// 是否包含验证码
326    pub has_captcha: bool,
327    /// 提交按钮文本
328    pub submit_text: Option<String>,
329}
330
331impl Form {
332    /// 创建新表单
333    pub fn new() -> Self {
334        Self::default()
335    }
336
337    /// 获取指定 name 的字段
338    pub fn get_field_by_name(&self, name: &str) -> Option<&FormField> {
339        self.fields.iter().find(|f| f.name.as_deref() == Some(name))
340    }
341
342    /// 获取所有可见字段(非 hidden 类型)
343    pub fn visible_fields(&self) -> Vec<&FormField> {
344        self.fields
345            .iter()
346            .filter(|f| !matches!(f.field_type, FieldType::Hidden))
347            .collect()
348    }
349
350    /// 获取所有需要用户输入的字段
351    pub fn input_fields(&self) -> Vec<&FormField> {
352        self.fields.iter().filter(|f| f.field_type.is_input()).collect()
353    }
354}
355
356impl Default for Form {
357    fn default() -> Self {
358        Self {
359            id: None,
360            name: None,
361            action: None,
362            method: FormMethod::default(),
363            enctype: None,
364            fields: Vec::new(),
365            form_type: FormType::Unknown,
366            has_csrf: false,
367            has_captcha: false,
368            submit_text: None,
369        }
370    }
371}
372
373// ============================================================================
374// 标签关联
375// ============================================================================
376
377/// 将 `<label>` 元素与表单字段关联。
378///
379/// 通过 `for` 属性匹配字段 id,或通过包裹关系匹配。
380/// 返回更新后的字段列表。
381pub fn associate_labels(document: &Html, fields: Vec<FormField>) -> Vec<FormField> {
382    let label_sel = match try_parse_selector("label") {
383        Some(s) => s,
384        None => return fields,
385    };
386
387    let mut field_map: std::collections::HashMap<String, FormField> = std::collections::HashMap::new();
388    let mut unmatched: Vec<FormField> = Vec::new();
389
390    for f in fields {
391        if let Some(id) = f.id.clone() {
392            field_map.insert(id, f);
393        } else {
394            unmatched.push(f);
395        }
396    }
397
398    for label_elem in document.select(&label_sel) {
399        let for_attr = label_elem.value().attr("for");
400        let label_text: String = label_elem.text().collect::<Vec<_>>().join(" ").trim().to_string();
401
402        if let Some(for_id) = for_attr
403            && let Some(field) = field_map.get_mut(for_id) {
404                if field.label.is_none() {
405                    field.label = Some(label_text);
406                }
407                continue;
408            }
409
410        // 没有 for 属性时,查找 label 包裹的第一个表单控件
411        if let Some(child_input) = find_first_form_control(&label_elem) {
412            let child_id = child_input.value().attr("id");
413            let child_name = child_input.value().attr("name");
414
415            if let Some(id) = child_id
416                && let Some(field) = field_map.get_mut(id) {
417                    if field.label.is_none() {
418                        field.label = Some(label_text);
419                    }
420                    continue;
421                }
422
423            if let Some(name) = child_name
424                && let Some(field) = field_map.values_mut().find(|f| f.name.as_deref() == Some(name)) {
425                    if field.label.is_none() {
426                        field.label = Some(label_text);
427                    }
428                    continue;
429                }
430        }
431    }
432
433    let mut result: Vec<FormField> = field_map.into_values().collect();
434    result.append(&mut unmatched);
435    result
436}
437
438/// 在元素内查找第一个表单控件子元素
439fn find_first_form_control<'a>(element: &'a ElementRef<'a>) -> Option<ElementRef<'a>> {
440    for child in element.children() {
441        if let Some(child_elem) = ElementRef::wrap(child) {
442            let tag = child_elem.value().name();
443            if matches!(tag, "input" | "select" | "textarea") {
444                return Some(child_elem);
445            }
446        }
447    }
448    None
449}
450
451// ============================================================================
452// 提取输入字段
453// ============================================================================
454
455/// 从 `<input>` 元素提取表单字段
456pub fn extract_input_field(element: &ElementRef) -> Option<FormField> {
457    let tag = element.value().name();
458    if tag != "input" {
459        return None;
460    }
461
462    let type_attr = element.value().attr("type");
463    let field_type = FieldType::from_html_type(type_attr);
464
465    let mut field = FormField::anonymous(field_type);
466
467    field.name = element.value().attr("name").map(std::string::ToString::to_string);
468    field.id = element.value().attr("id").map(std::string::ToString::to_string);
469    field.placeholder = element.value().attr("placeholder").map(std::string::ToString::to_string);
470    field.value = element.value().attr("value").map(std::string::ToString::to_string);
471    field.required = element.value().attr("required").is_some();
472    field.disabled = element.value().attr("disabled").is_some();
473    field.readonly = element.value().attr("readonly").is_some();
474    field.autocomplete = element.value().attr("autocomplete").map(std::string::ToString::to_string);
475    field.pattern = element.value().attr("pattern").map(std::string::ToString::to_string);
476    field.min_length = element.value().attr("minlength").and_then(|v| v.parse().ok());
477    field.max_length = element.value().attr("maxlength").and_then(|v| v.parse().ok());
478
479    Some(field)
480}
481
482/// 从 `<select>` 元素提取表单字段
483pub fn extract_select_field(element: &ElementRef) -> Option<FormField> {
484    let tag = element.value().name();
485    if tag != "select" {
486        return None;
487    }
488
489    let mut field = FormField::anonymous(FieldType::Select);
490
491    field.name = element.value().attr("name").map(std::string::ToString::to_string);
492    field.id = element.value().attr("id").map(std::string::ToString::to_string);
493    field.required = element.value().attr("required").is_some();
494    field.disabled = element.value().attr("disabled").is_some();
495
496    // 提取选项
497    let option_sel = try_parse_selector("option");
498    if let Some(ref sel) = option_sel {
499        for opt_elem in element.select(sel) {
500            let value = opt_elem.value().attr("value").unwrap_or("");
501            let label: String = opt_elem.text().collect::<Vec<_>>().join(" ").trim().to_string();
502            let label = if label.is_empty() {
503                value.to_string()
504            } else {
505                label
506            };
507            let selected = opt_elem.value().attr("selected").is_some();
508            field.options.push(SelectOption {
509                value: value.to_string(),
510                label,
511                selected,
512            });
513        }
514    }
515
516    Some(field)
517}
518
519/// 从 `<textarea>` 元素提取表单字段
520pub fn extract_textarea_field(element: &ElementRef) -> Option<FormField> {
521    let tag = element.value().name();
522    if tag != "textarea" {
523        return None;
524    }
525
526    let mut field = FormField::anonymous(FieldType::Textarea);
527
528    field.name = element.value().attr("name").map(std::string::ToString::to_string);
529    field.id = element.value().attr("id").map(std::string::ToString::to_string);
530    field.placeholder = element.value().attr("placeholder").map(std::string::ToString::to_string);
531    field.required = element.value().attr("required").is_some();
532    field.disabled = element.value().attr("disabled").is_some();
533    field.readonly = element.value().attr("readonly").is_some();
534    field.value = Some(element.text().collect::<Vec<_>>().join(" ").trim().to_string());
535    field.max_length = element.value().attr("maxlength").and_then(|v| v.parse().ok());
536
537    Some(field)
538}
539
540/// 从 `<form>` 元素中提取所有表单字段
541pub fn extract_form_fields(element: &ElementRef) -> Vec<FormField> {
542    let mut fields = Vec::new();
543
544    // 提取 input 元素
545    let input_sel = try_parse_selector("input, select, textarea");
546    let input_sel = match input_sel {
547        Some(s) => s,
548        None => return fields,
549    };
550
551    for child in element.select(&input_sel) {
552        let tag = child.value().name();
553        let field = match tag {
554            "input" => extract_input_field(&child),
555            "select" => extract_select_field(&child),
556            "textarea" => extract_textarea_field(&child),
557            _ => None,
558        };
559
560        if let Some(f) = field {
561            fields.push(f);
562        }
563    }
564
565    fields
566}
567
568// ============================================================================
569// 表单提取
570// ============================================================================
571
572/// 从单个 `<form>` 元素提取 Form 结构
573pub fn extract_form(element: &ElementRef) -> Option<Form> {
574    let tag = element.value().name();
575    if tag != "form" {
576        return None;
577    }
578
579    let mut form = Form::new();
580
581    form.id = element.value().attr("id").map(std::string::ToString::to_string);
582    form.name = element.value().attr("name").map(std::string::ToString::to_string);
583    form.action = element.value().attr("action").map(std::string::ToString::to_string);
584    form.method = FormMethod::from_str(element.value().attr("method").unwrap_or("get"));
585    form.enctype = element.value().attr("enctype").map(std::string::ToString::to_string);
586
587    // 提取字段
588    form.fields = extract_form_fields(element);
589
590    // 检测表单特征
591    form.has_csrf = detect_csrf_token(&form.fields);
592    form.has_captcha = detect_captcha(&form.fields);
593    form.form_type = detect_form_type(&form);
594    form.submit_text = extract_submit_text(element);
595
596    Some(form)
597}
598
599/// 从 HTML 文档中提取所有表单
600pub fn extract_forms(document: &Html) -> Vec<Form> {
601    let form_sel = match try_parse_selector("form") {
602        Some(s) => s,
603        None => return Vec::new(),
604    };
605
606    let mut forms = Vec::new();
607
608    for form_elem in document.select(&form_sel) {
609        if let Some(form) = extract_form(&form_elem) {
610            // 关联 label
611            let labeled = associate_labels(document, form.fields);
612            forms.push(Form { fields: labeled, ..form });
613        }
614    }
615
616    forms
617}
618
619// ============================================================================
620// CSRF 令牌检测
621// ============================================================================
622
623/// 检测表单字段中是否包含 CSRF 令牌。
624///
625/// 通过字段名称中的关键字(csrf、token、_token、authenticity_token 等)判断。
626pub fn detect_csrf_token(fields: &[FormField]) -> bool {
627    let csrf_patterns = &[
628        "csrf",
629        "_token",
630        "token",
631        "authenticity_token",
632        "csrf_token",
633        "csrfmiddlewaretoken",
634        "__csrf",
635        "csrfkey",
636        "csrf_name",
637        "csrf_value",
638        "_csrf_token",
639        "xsrf",
640        "_xsrf",
641        "x-csrf-token",
642    ];
643
644    fields.iter().any(|field| {
645        if matches!(field.field_type, FieldType::Hidden) {
646            if let Some(ref name) = field.name {
647                let lower = name.to_lowercase();
648                csrf_patterns.iter().any(|p| lower.contains(p))
649            } else {
650                false
651            }
652        } else {
653            false
654        }
655    })
656}
657
658// ============================================================================
659// 验证码检测
660// ============================================================================
661
662/// 检测表单字段中是否包含验证码。
663///
664/// 通过字段 name、id、class、placeholder 中的关键字(captcha、recaptcha、g-recaptcha 等)判断。
665pub fn detect_captcha(fields: &[FormField]) -> bool {
666    let captcha_patterns = &[
667        "captcha",
668        "recaptcha",
669        "g-recaptcha",
670        "h-captcha",
671        "turnstile",
672        "cf-turnstile",
673        "captchacode",
674        "captcha_code",
675        "security_code",
676        "verification_code",
677        "captcha_id",
678        "simple_captcha",
679        "math_captcha",
680    ];
681
682    fields.iter().any(|field| {
683        let name_lower = field.name.as_deref().unwrap_or("").to_lowercase();
684        let id_lower = field.id.as_deref().unwrap_or("").to_lowercase();
685
686        captcha_patterns.iter().any(|p| {
687            name_lower.contains(p) || id_lower.contains(p)
688        })
689    })
690}
691
692// ============================================================================
693// 提交文本提取
694// ============================================================================
695
696/// 从 `<form>` 元素中提取提交按钮文本。
697///
698/// 查找 input[type=submit]、button[type=submit] 的 value 或文本内容。
699pub fn extract_submit_text(element: &ElementRef) -> Option<String> {
700    let submit_sel = try_parse_selector("input[type=submit], button[type=submit]");
701    let submit_sel = submit_sel?;
702
703    for btn in element.select(&submit_sel) {
704        let tag = btn.value().name();
705        let text = match tag {
706            "input" => btn.value().attr("value").map(std::string::ToString::to_string),
707            "button" => {
708                let t: String = btn.text().collect::<Vec<_>>().join(" ").trim().to_string();
709                if t.is_empty() {
710                    btn.value().attr("value").map(std::string::ToString::to_string)
711                } else {
712                    Some(t)
713                }
714            }
715            _ => None,
716        };
717        if text.is_some() {
718            return text;
719        }
720    }
721
722    None
723}
724
725// ============================================================================
726// 表单类型检测
727// ============================================================================
728
729/// 根据表单的字段特征推断表单类型
730pub fn detect_form_type(form: &Form) -> FormType {
731    let name_lower = form.name.as_deref().unwrap_or("").to_lowercase();
732    let id_lower = form.id.as_deref().unwrap_or("").to_lowercase();
733    let action_lower = form.action.as_deref().unwrap_or("").to_lowercase();
734
735    let field_names: Vec<String> = form
736        .fields
737        .iter()
738        .filter_map(|f| f.name.as_ref())
739        .map(|n| n.to_lowercase())
740        .collect();
741
742    let field_ids: Vec<String> = form
743        .fields
744        .iter()
745        .filter_map(|f| f.id.as_ref())
746        .map(|id| id.to_lowercase())
747        .collect();
748
749    let has_password = form.fields.iter().any(|f| matches!(f.field_type, FieldType::Password));
750    let has_email = form.fields.iter().any(|f| matches!(f.field_type, FieldType::Email));
751    let has_file = form.fields.iter().any(|f| matches!(f.field_type, FieldType::File));
752    let has_search = form.fields.iter().any(|f| matches!(f.field_type, FieldType::Search));
753
754    // 收集所有可用于匹配的字符串
755    let all_text: Vec<&str> = field_names
756        .iter()
757        .map(std::string::String::as_str)
758        .chain(field_ids.iter().map(std::string::String::as_str))
759        .chain(std::iter::once(name_lower.as_str()))
760        .chain(std::iter::once(id_lower.as_str()))
761        .chain(std::iter::once(action_lower.as_str()))
762        .collect();
763
764    // 登录表单检测
765    if has_password {
766        for s in &all_text {
767            let lower = s.to_lowercase();
768            if lower.contains("login")
769                || lower.contains("signin")
770                || lower.contains("sign_in")
771                || lower.contains("log-in")
772                || lower.contains("log_in")
773            {
774                return FormType::Login;
775            }
776        }
777
778        // 有密码字段且包含注册关键字 -> 注册
779        for s in &all_text {
780            let lower = s.to_lowercase();
781            if lower.contains("register")
782                || lower.contains("signup")
783                || lower.contains("sign_up")
784                || lower.contains("sign-up")
785                || lower.contains("create_account")
786                || lower.contains("create-account")
787            {
788                return FormType::Registration;
789            }
790        }
791
792        // 有邮件 + 密码 + 记住我 这类典型登录特征
793        if has_email
794            && field_names.iter().any(|n| n.contains("remember") || n.contains("stay"))
795        {
796            return FormType::Login;
797        }
798
799        // 密码重置检测
800        for s in &all_text {
801            let lower = s.to_lowercase();
802            if lower.contains("password_reset")
803                || lower.contains("reset_password")
804                || lower.contains("forgot_password")
805                || lower.contains("reset-password")
806                || lower.contains("forgot-password")
807                || lower.contains("recover")
808            {
809                return FormType::PasswordReset;
810            }
811        }
812
813        return FormType::Login;
814    }
815
816    // 搜索表单检测
817    if has_search {
818        return FormType::Search;
819    }
820    for s in &all_text {
821        let lower = s.to_lowercase();
822        if lower.contains("search")
823            || lower.contains("query")
824            || lower.contains("q=")
825            || lower == "q"
826            || lower.contains("find")
827        {
828            return FormType::Search;
829        }
830    }
831
832    // 文件上传检测
833    if has_file {
834        return FormType::Upload;
835    }
836    for s in &all_text {
837        let lower = s.to_lowercase();
838        if lower.contains("upload")
839            || lower.contains("file")
840            || lower.contains("attachment")
841        {
842            return FormType::Upload;
843        }
844    }
845
846    // 联系表单检测
847    for s in &all_text {
848        let lower = s.to_lowercase();
849        if lower.contains("contact")
850            || lower.contains("feedback")
851            || lower.contains("support")
852            || lower.contains("inquiry")
853            || lower.contains("enquiry")
854        {
855            return FormType::Contact;
856        }
857    }
858
859    // 新闻订阅检测
860    for s in &all_text {
861        let lower = s.to_lowercase();
862        if lower.contains("newsletter")
863            || lower.contains("subscribe")
864            || lower.contains("subscription")
865            || lower.contains("mailing_list")
866        {
867            return FormType::Newsletter;
868        }
869    }
870
871    // 结账检测
872    for s in &all_text {
873        let lower = s.to_lowercase();
874        if lower.contains("checkout")
875            || lower.contains("payment")
876            || lower.contains("cart")
877            || lower.contains("billing")
878            || lower.contains("order")
879        {
880            return FormType::Checkout;
881        }
882    }
883
884    // 评论检测
885    for s in &all_text {
886        let lower = s.to_lowercase();
887        if lower.contains("comment")
888            || lower.contains("reply")
889            || lower.contains("feedback")
890        {
891            return FormType::Comment;
892        }
893    }
894
895    // 注册检测(无密码时的备用检测)
896    for s in &all_text {
897        let lower = s.to_lowercase();
898        if lower.contains("register")
899            || lower.contains("signup")
900            || lower.contains("sign_up")
901            || lower.contains("create_account")
902            || lower.contains("registration")
903        {
904            return FormType::Registration;
905        }
906    }
907
908    FormType::Unknown
909}
910
911// ============================================================================
912// 便利函数
913// ============================================================================
914
915/// 从表单列表中筛选出登录表单
916pub fn get_login_forms(forms: &[Form]) -> Vec<&Form> {
917    forms.iter().filter(|f| f.form_type == FormType::Login).collect()
918}
919
920/// 从表单列表中筛选出搜索表单
921pub fn get_search_forms(forms: &[Form]) -> Vec<&Form> {
922    forms.iter().filter(|f| f.form_type == FormType::Search).collect()
923}
924
925/// 从表单列表中筛选出联系表单
926pub fn get_contact_forms(forms: &[Form]) -> Vec<&Form> {
927    forms.iter().filter(|f| f.form_type == FormType::Contact).collect()
928}
929
930/// 检查 HTML 文档中是否包含表单
931pub fn has_forms(document: &Html) -> bool {
932    let form_sel = match try_parse_selector("form") {
933        Some(s) => s,
934        None => return false,
935    };
936    document.select(&form_sel).next().is_some()
937}
938
939/// 检查 HTML 文档中是否包含登录表单
940pub fn has_login_form(document: &Html) -> bool {
941    let forms = extract_forms(document);
942    !get_login_forms(&forms).is_empty()
943}
944
945/// 检查 HTML 文档中是否包含搜索表单
946pub fn has_search_form(document: &Html) -> bool {
947    let forms = extract_forms(document);
948    !get_search_forms(&forms).is_empty()
949}
950
951// ============================================================================
952// 测试
953// ============================================================================
954
955#[cfg(test)]
956mod tests {
957    use super::*;
958
959    // ─── 辅助函数 ──────────────────────────────────────────
960
961    fn parse_html(html: &str) -> Html {
962        Html::parse_document(html)
963    }
964
965    fn parse_fragment(html: &str) -> Html {
966        Html::parse_fragment(html)
967    }
968
969    fn first_form_element(doc: &Html) -> ElementRef<'_> {
970        let sel = try_parse_selector("form").unwrap();
971        doc.select(&sel).next().expect("应存在表单元素")
972    }
973
974    // ─── FormMethod 测试 ──────────────────────────────────────
975
976    #[test]
977    fn test_form_method_from_str() {
978        assert_eq!(FormMethod::from_str("get"), FormMethod::Get);
979        assert_eq!(FormMethod::from_str("GET"), FormMethod::Get);
980        assert_eq!(FormMethod::from_str("post"), FormMethod::Post);
981        assert_eq!(FormMethod::from_str("POST"), FormMethod::Post);
982        assert_eq!(FormMethod::from_str("dialog"), FormMethod::Dialog);
983        assert_eq!(FormMethod::from_str(""), FormMethod::Get);
984        assert_eq!(FormMethod::from_str("invalid"), FormMethod::Get);
985    }
986
987    #[test]
988    fn test_form_method_default() {
989        let method: FormMethod = Default::default();
990        assert_eq!(method, FormMethod::Get);
991    }
992
993    #[test]
994    fn test_form_method_serialize() {
995        let json = serde_json::to_string(&FormMethod::Post).unwrap();
996        assert_eq!(json, "\"post\"");
997    }
998
999    // ─── FieldType 测试 ──────────────────────────────────────
1000
1001    #[test]
1002    fn test_field_type_from_html_type() {
1003        assert_eq!(FieldType::from_html_type(Some("text")), FieldType::Text);
1004        assert_eq!(FieldType::from_html_type(Some("email")), FieldType::Email);
1005        assert_eq!(FieldType::from_html_type(Some("password")), FieldType::Password);
1006        assert_eq!(FieldType::from_html_type(Some("number")), FieldType::Number);
1007        assert_eq!(FieldType::from_html_type(Some("checkbox")), FieldType::Checkbox);
1008        assert_eq!(FieldType::from_html_type(Some("submit")), FieldType::Submit);
1009        assert_eq!(FieldType::from_html_type(Some("hidden")), FieldType::Hidden);
1010        assert_eq!(FieldType::from_html_type(Some("file")), FieldType::File);
1011        assert_eq!(FieldType::from_html_type(Some("date")), FieldType::Date);
1012        assert_eq!(FieldType::from_html_type(Some("color")), FieldType::Color);
1013        assert_eq!(FieldType::from_html_type(Some("range")), FieldType::Range);
1014        assert_eq!(FieldType::from_html_type(Some("tel")), FieldType::Tel);
1015        assert_eq!(FieldType::from_html_type(Some("url")), FieldType::Url);
1016        assert_eq!(FieldType::from_html_type(Some("search")), FieldType::Search);
1017        assert_eq!(FieldType::from_html_type(Some("radio")), FieldType::Radio);
1018        assert_eq!(FieldType::from_html_type(Some("button")), FieldType::Button);
1019        assert_eq!(FieldType::from_html_type(Some("time")), FieldType::Time);
1020        assert_eq!(FieldType::from_html_type(Some("unknown_type")), FieldType::Unknown);
1021        assert_eq!(FieldType::from_html_type(None), FieldType::Text);
1022    }
1023
1024    #[test]
1025    fn test_field_type_classification() {
1026        assert!(FieldType::Text.is_input());
1027        assert!(FieldType::Email.is_input());
1028        assert!(FieldType::Password.is_input());
1029        assert!(!FieldType::Hidden.is_input());
1030        assert!(!FieldType::Submit.is_input());
1031
1032        assert!(FieldType::Select.is_selectable());
1033        assert!(FieldType::Checkbox.is_selectable());
1034        assert!(FieldType::Radio.is_selectable());
1035        assert!(!FieldType::Text.is_selectable());
1036
1037        assert!(FieldType::Submit.is_button());
1038        assert!(FieldType::Button.is_button());
1039        assert!(!FieldType::Text.is_button());
1040    }
1041
1042    // ─── SelectOption 测试 ────────────────────────────────────
1043
1044    #[test]
1045    fn test_select_option_creation() {
1046        let opt = SelectOption::new("cn", "中国");
1047        assert_eq!(opt.value, "cn");
1048        assert_eq!(opt.label, "中国");
1049        assert!(!opt.selected);
1050    }
1051
1052    // ─── FormField 测试 ───────────────────────────────────────
1053
1054    #[test]
1055    fn test_form_field_creation() {
1056        let field = FormField::new("username", FieldType::Text);
1057        assert_eq!(field.name, Some("username".to_string()));
1058        assert_eq!(field.field_type, FieldType::Text);
1059        assert!(!field.required);
1060        assert!(!field.disabled);
1061    }
1062
1063    #[test]
1064    fn test_form_field_anonymous() {
1065        let field = FormField::anonymous(FieldType::Hidden);
1066        assert!(field.name.is_none());
1067        assert_eq!(field.field_type, FieldType::Hidden);
1068    }
1069
1070    // ─── 提取 Input 字段测试 ──────────────────────────────────
1071
1072    #[test]
1073    fn test_extract_input_field_basic() {
1074        let html = r#"<input type="text" name="username" id="user" placeholder="请输入用户名" required>"#;
1075        let doc = parse_fragment(html);
1076        let sel = try_parse_selector("input").unwrap();
1077        let elem = doc.select(&sel).next().unwrap();
1078        let field = extract_input_field(&elem).unwrap();
1079
1080        assert_eq!(field.name, Some("username".to_string()));
1081        assert_eq!(field.id, Some("user".to_string()));
1082        assert_eq!(field.field_type, FieldType::Text);
1083        assert_eq!(field.placeholder, Some("请输入用户名".to_string()));
1084        assert!(field.required);
1085        assert!(!field.disabled);
1086    }
1087
1088    #[test]
1089    fn test_extract_input_field_hidden() {
1090        let html = r#"<input type="hidden" name="csrf_token" value="abc123">"#;
1091        let doc = parse_fragment(html);
1092        let sel = try_parse_selector("input").unwrap();
1093        let elem = doc.select(&sel).next().unwrap();
1094        let field = extract_input_field(&elem).unwrap();
1095
1096        assert_eq!(field.field_type, FieldType::Hidden);
1097        assert_eq!(field.value, Some("abc123".to_string()));
1098    }
1099
1100    #[test]
1101    fn test_extract_input_field_all_attributes() {
1102        let html = r#"<input type="password" name="pass" id="pwd" placeholder="密码" value="" required disabled readonly autocomplete="off" pattern=".{6,}" minlength="6" maxlength="20">"#;
1103        let doc = parse_fragment(html);
1104        let sel = try_parse_selector("input").unwrap();
1105        let elem = doc.select(&sel).next().unwrap();
1106        let field = extract_input_field(&elem).unwrap();
1107
1108        assert_eq!(field.field_type, FieldType::Password);
1109        assert_eq!(field.name, Some("pass".to_string()));
1110        assert_eq!(field.id, Some("pwd".to_string()));
1111        assert_eq!(field.autocomplete, Some("off".to_string()));
1112        assert_eq!(field.pattern, Some(".{6,}".to_string()));
1113        assert_eq!(field.min_length, Some(6));
1114        assert_eq!(field.max_length, Some(20));
1115        assert!(field.required);
1116        assert!(field.disabled);
1117        assert!(field.readonly);
1118    }
1119
1120    #[test]
1121    fn test_extract_input_field_non_input_tag() {
1122        let html = r"<span>不是 input</span>";
1123        let doc = parse_fragment(html);
1124        let sel = try_parse_selector("span").unwrap();
1125        let elem = doc.select(&sel).next().unwrap();
1126        let result = extract_input_field(&elem);
1127        assert!(result.is_none());
1128    }
1129
1130    #[test]
1131    fn test_extract_input_field_no_type_defaults_to_text() {
1132        let html = r#"<input name="q">"#;
1133        let doc = parse_fragment(html);
1134        let sel = try_parse_selector("input").unwrap();
1135        let elem = doc.select(&sel).next().unwrap();
1136        let field = extract_input_field(&elem).unwrap();
1137        assert_eq!(field.field_type, FieldType::Text);
1138    }
1139
1140    // ─── 提取 Select 字段测试 ────────────────────────────────
1141
1142    #[test]
1143    fn test_extract_select_field_basic() {
1144        let html = r#"<select name="country" id="country">
1145            <option value="cn">中国</option>
1146            <option value="us" selected>美国</option>
1147            <option value="jp">日本</option>
1148        </select>"#;
1149        let doc = parse_fragment(html);
1150        let sel = try_parse_selector("select").unwrap();
1151        let elem = doc.select(&sel).next().unwrap();
1152        let field = extract_select_field(&elem).unwrap();
1153
1154        assert_eq!(field.field_type, FieldType::Select);
1155        assert_eq!(field.name, Some("country".to_string()));
1156        assert_eq!(field.options.len(), 3);
1157        assert_eq!(field.options[0].value, "cn");
1158        assert_eq!(field.options[0].label, "中国");
1159        assert!(!field.options[0].selected);
1160        assert!(field.options[1].selected);
1161    }
1162
1163    #[test]
1164    fn test_extract_select_field_non_select_tag() {
1165        let html = r"<div>不是 select</div>";
1166        let doc = parse_fragment(html);
1167        let sel = try_parse_selector("div").unwrap();
1168        let elem = doc.select(&sel).next().unwrap();
1169        let result = extract_select_field(&elem);
1170        assert!(result.is_none());
1171    }
1172
1173    #[test]
1174    fn test_extract_select_field_empty() {
1175        let html = r#"<select name="empty"></select>"#;
1176        let doc = parse_fragment(html);
1177        let sel = try_parse_selector("select").unwrap();
1178        let elem = doc.select(&sel).next().unwrap();
1179        let field = extract_select_field(&elem).unwrap();
1180        assert!(field.options.is_empty());
1181    }
1182
1183    #[test]
1184    fn test_extract_select_field_option_without_value_uses_text() {
1185        let html = r#"<select name="s"><option>请选择</option></select>"#;
1186        let doc = parse_fragment(html);
1187        let sel = try_parse_selector("select").unwrap();
1188        let elem = doc.select(&sel).next().unwrap();
1189        let field = extract_select_field(&elem).unwrap();
1190        assert_eq!(field.options[0].value, "");
1191        assert_eq!(field.options[0].label, "请选择");
1192    }
1193
1194    // ─── 提取 Textarea 字段测试 ──────────────────────────────
1195
1196    #[test]
1197    fn test_extract_textarea_field_basic() {
1198        let html = r#"<textarea name="bio" id="bio" placeholder="自我介绍" maxlength="500">这是内容</textarea>"#;
1199        let doc = parse_fragment(html);
1200        let sel = try_parse_selector("textarea").unwrap();
1201        let elem = doc.select(&sel).next().unwrap();
1202        let field = extract_textarea_field(&elem).unwrap();
1203
1204        assert_eq!(field.field_type, FieldType::Textarea);
1205        assert_eq!(field.name, Some("bio".to_string()));
1206        assert_eq!(field.placeholder, Some("自我介绍".to_string()));
1207        assert_eq!(field.max_length, Some(500));
1208        assert_eq!(field.value, Some("这是内容".to_string()));
1209    }
1210
1211    #[test]
1212    fn test_extract_textarea_field_non_textarea_tag() {
1213        let html = r"<p>不是 textarea</p>";
1214        let doc = parse_fragment(html);
1215        let sel = try_parse_selector("p").unwrap();
1216        let elem = doc.select(&sel).next().unwrap();
1217        let result = extract_textarea_field(&elem);
1218        assert!(result.is_none());
1219    }
1220
1221    #[test]
1222    fn test_extract_textarea_field_empty() {
1223        let html = r#"<textarea name="empty"></textarea>"#;
1224        let doc = parse_fragment(html);
1225        let sel = try_parse_selector("textarea").unwrap();
1226        let elem = doc.select(&sel).next().unwrap();
1227        let field = extract_textarea_field(&elem).unwrap();
1228        assert_eq!(field.value, Some(String::new()));
1229    }
1230
1231    // ─── 表单字段提取测试 ────────────────────────────────────
1232
1233    #[test]
1234    fn test_extract_form_fields_mixed() {
1235        let html = r#"<form>
1236            <input type="text" name="username">
1237            <input type="password" name="password">
1238            <select name="country">
1239                <option value="cn">中国</option>
1240            </select>
1241            <textarea name="bio"></textarea>
1242            <input type="hidden" name="token" value="xyz">
1243        </form>"#;
1244        let doc = parse_html(html);
1245        let form_elem = first_form_element(&doc);
1246        let fields = extract_form_fields(&form_elem);
1247
1248        assert_eq!(fields.len(), 5);
1249        assert_eq!(fields[0].field_type, FieldType::Text);
1250        assert_eq!(fields[1].field_type, FieldType::Password);
1251        assert_eq!(fields[2].field_type, FieldType::Select);
1252        assert_eq!(fields[3].field_type, FieldType::Textarea);
1253        assert_eq!(fields[4].field_type, FieldType::Hidden);
1254    }
1255
1256    #[test]
1257    fn test_extract_form_fields_no_form_controls() {
1258        let html = r"<form><p>没有表单控件</p></form>";
1259        let doc = parse_html(html);
1260        let form_elem = first_form_element(&doc);
1261        let fields = extract_form_fields(&form_elem);
1262        assert!(fields.is_empty());
1263    }
1264
1265    // ─── 标签关联测试 ────────────────────────────────────────
1266
1267    #[test]
1268    fn test_associate_labels_by_for_attr() {
1269        let html = r#"<form>
1270            <label for="email">邮箱地址</label>
1271            <input type="email" id="email" name="email">
1272        </form>"#;
1273        let doc = parse_html(html);
1274        let form_elem = first_form_element(&doc);
1275        let fields = extract_form_fields(&form_elem);
1276        let labeled = associate_labels(&doc, fields);
1277
1278        assert_eq!(labeled.len(), 1);
1279        assert_eq!(labeled[0].label, Some("邮箱地址".to_string()));
1280    }
1281
1282    #[test]
1283    fn test_associate_labels_by_wrapping() {
1284        let html = r#"<form>
1285            <label>用户名
1286                <input type="text" name="username" id="user">
1287            </label>
1288        </form>"#;
1289        let doc = parse_html(html);
1290        let form_elem = first_form_element(&doc);
1291        let fields = extract_form_fields(&form_elem);
1292        let labeled = associate_labels(&doc, fields);
1293
1294        assert_eq!(labeled.len(), 1);
1295        assert_eq!(labeled[0].label, Some("用户名".to_string()));
1296    }
1297
1298    #[test]
1299    fn test_associate_labels_no_labels() {
1300        let html = r#"<form><input type="text" name="q"></form>"#;
1301        let doc = parse_html(html);
1302        let form_elem = first_form_element(&doc);
1303        let fields = extract_form_fields(&form_elem);
1304        let labeled = associate_labels(&doc, fields);
1305
1306        assert_eq!(labeled.len(), 1);
1307        assert!(labeled[0].label.is_none());
1308    }
1309
1310    // ─── 表单提取测试 ────────────────────────────────────────
1311
1312    #[test]
1313    fn test_extract_forms_single_form() {
1314        let html = r#"<html><body>
1315            <form id="f1" name="login" action="/login" method="post">
1316                <input type="text" name="user">
1317                <input type="password" name="pass">
1318                <input type="submit" value="登录">
1319            </form>
1320        </body></html>"#;
1321        let doc = parse_html(html);
1322        let forms = extract_forms(&doc);
1323
1324        assert_eq!(forms.len(), 1);
1325        assert_eq!(forms[0].id, Some("f1".to_string()));
1326        assert_eq!(forms[0].name, Some("login".to_string()));
1327        assert_eq!(forms[0].action, Some("/login".to_string()));
1328        assert_eq!(forms[0].method, FormMethod::Post);
1329    }
1330
1331    #[test]
1332    fn test_extract_forms_multiple_forms() {
1333        let html = r#"<html><body>
1334            <form id="search" action="/search"><input type="search" name="q"></form>
1335            <form id="login" action="/login"><input type="password" name="p"></form>
1336        </body></html>"#;
1337        let doc = parse_html(html);
1338        let forms = extract_forms(&doc);
1339
1340        assert_eq!(forms.len(), 2);
1341    }
1342
1343    #[test]
1344    fn test_extract_forms_no_forms() {
1345        let html = r"<html><body><p>无表单</p></body></html>";
1346        let doc = parse_html(html);
1347        let forms = extract_forms(&doc);
1348        assert!(forms.is_empty());
1349    }
1350
1351    #[test]
1352    fn test_extract_form_non_form_element() {
1353        let html = r"<div>不是 form</div>";
1354        let doc = parse_fragment(html);
1355        let sel = try_parse_selector("div").unwrap();
1356        let elem = doc.select(&sel).next().unwrap();
1357        let result = extract_form(&elem);
1358        assert!(result.is_none());
1359    }
1360
1361    #[test]
1362    fn test_extract_form_default_method() {
1363        let html = r#"<form action="/search">
1364            <input type="text" name="q">
1365        </form>"#;
1366        let doc = parse_html(html);
1367        let form_elem = first_form_element(&doc);
1368        let form = extract_form(&form_elem).unwrap();
1369        assert_eq!(form.method, FormMethod::Get);
1370    }
1371
1372    #[test]
1373    fn test_extract_form_dialog_method() {
1374        let html = r#"<form method="dialog"><input type="text" name="x"></form>"#;
1375        let doc = parse_html(html);
1376        let form_elem = first_form_element(&doc);
1377        let form = extract_form(&form_elem).unwrap();
1378        assert_eq!(form.method, FormMethod::Dialog);
1379    }
1380
1381    // ─── 表单类型检测测试 ────────────────────────────────────
1382
1383    #[test]
1384    fn test_detect_form_type_login() {
1385        let html = r#"<form id="login-form" action="/login" method="post">
1386            <input type="text" name="username">
1387            <input type="password" name="password">
1388            <input type="submit" value="登录">
1389        </form>"#;
1390        let doc = parse_html(html);
1391        let form_elem = first_form_element(&doc);
1392        let form = extract_form(&form_elem).unwrap();
1393        assert_eq!(form.form_type, FormType::Login);
1394    }
1395
1396    #[test]
1397    fn test_detect_form_type_search() {
1398        let html = r#"<form action="/search">
1399            <input type="search" name="q" placeholder="搜索...">
1400            <input type="submit" value="搜索">
1401        </form>"#;
1402        let doc = parse_html(html);
1403        let form_elem = first_form_element(&doc);
1404        let form = extract_form(&form_elem).unwrap();
1405        assert_eq!(form.form_type, FormType::Search);
1406    }
1407
1408    #[test]
1409    fn test_detect_form_type_search_by_name() {
1410        let html = r#"<form>
1411            <input type="text" name="q" placeholder="搜索">
1412        </form>"#;
1413        let doc = parse_html(html);
1414        let form_elem = first_form_element(&doc);
1415        let form = extract_form(&form_elem).unwrap();
1416        assert_eq!(form.form_type, FormType::Search);
1417    }
1418
1419    #[test]
1420    fn test_detect_form_type_registration() {
1421        let html = r#"<form id="register" action="/signup">
1422            <input type="text" name="username">
1423            <input type="email" name="email">
1424            <input type="password" name="password">
1425        </form>"#;
1426        let doc = parse_html(html);
1427        let form_elem = first_form_element(&doc);
1428        let form = extract_form(&form_elem).unwrap();
1429        assert_eq!(form.form_type, FormType::Registration);
1430    }
1431
1432    #[test]
1433    fn test_detect_form_type_contact() {
1434        let html = r#"<form id="contact-form">
1435            <input type="text" name="name">
1436            <input type="email" name="email">
1437            <textarea name="message"></textarea>
1438        </form>"#;
1439        let doc = parse_html(html);
1440        let form_elem = first_form_element(&doc);
1441        let form = extract_form(&form_elem).unwrap();
1442        assert_eq!(form.form_type, FormType::Contact);
1443    }
1444
1445    #[test]
1446    fn test_detect_form_type_newsletter() {
1447        let html = r#"<form class="newsletter">
1448            <input type="email" name="subscribe_email">
1449            <input type="submit" value="订阅">
1450        </form>"#;
1451        let doc = parse_html(html);
1452        let form_elem = first_form_element(&doc);
1453        let form = extract_form(&form_elem).unwrap();
1454        assert_eq!(form.form_type, FormType::Newsletter);
1455    }
1456
1457    #[test]
1458    fn test_detect_form_type_checkout() {
1459        let html = r#"<form id="checkout-form">
1460            <input type="text" name="card_number">
1461            <input type="text" name="address">
1462        </form>"#;
1463        let doc = parse_html(html);
1464        let form_elem = first_form_element(&doc);
1465        let form = extract_form(&form_elem).unwrap();
1466        assert_eq!(form.form_type, FormType::Checkout);
1467    }
1468
1469    #[test]
1470    fn test_detect_form_type_upload() {
1471        let html = r#"<form enctype="multipart/form-data">
1472            <input type="file" name="file">
1473        </form>"#;
1474        let doc = parse_html(html);
1475        let form_elem = first_form_element(&doc);
1476        let form = extract_form(&form_elem).unwrap();
1477        assert_eq!(form.form_type, FormType::Upload);
1478    }
1479
1480    #[test]
1481    fn test_detect_form_type_comment() {
1482        let html = r#"<form class="comment-form">
1483            <textarea name="comment"></textarea>
1484            <input type="submit" value="发表评论">
1485        </form>"#;
1486        let doc = parse_html(html);
1487        let form_elem = first_form_element(&doc);
1488        let form = extract_form(&form_elem).unwrap();
1489        assert_eq!(form.form_type, FormType::Comment);
1490    }
1491
1492    #[test]
1493    fn test_detect_form_type_password_reset() {
1494        let html = r#"<form id="reset-password-form">
1495            <input type="email" name="email">
1496            <input type="password" name="new_password">
1497        </form>"#;
1498        let doc = parse_html(html);
1499        let form_elem = first_form_element(&doc);
1500        let form = extract_form(&form_elem).unwrap();
1501        assert_eq!(form.form_type, FormType::PasswordReset);
1502    }
1503
1504    #[test]
1505    fn test_detect_form_type_unknown() {
1506        let html = r#"<form>
1507            <input type="text" name="data">
1508            <input type="number" name="count">
1509        </form>"#;
1510        let doc = parse_html(html);
1511        let form_elem = first_form_element(&doc);
1512        let form = extract_form(&form_elem).unwrap();
1513        assert_eq!(form.form_type, FormType::Unknown);
1514    }
1515
1516    // ─── CSRF 检测测试 ──────────────────────────────────────
1517
1518    #[test]
1519    fn test_detect_csrf_token_present() {
1520        let fields = vec![
1521            FormField::new("username", FieldType::Text),
1522            FormField {
1523                name: Some("csrf_token".to_string()),
1524                field_type: FieldType::Hidden,
1525                value: Some("abc123".to_string()),
1526                ..FormField::anonymous(FieldType::Hidden)
1527            },
1528        ];
1529        assert!(detect_csrf_token(&fields));
1530    }
1531
1532    #[test]
1533    fn test_detect_csrf_token_absent() {
1534        let fields = vec![
1535            FormField::new("username", FieldType::Text),
1536            FormField::new("password", FieldType::Password),
1537        ];
1538        assert!(!detect_csrf_token(&fields));
1539    }
1540
1541    #[test]
1542    fn test_detect_csrf_token_non_hidden_ignored() {
1543        let fields = vec![FormField {
1544            name: Some("csrf_token".to_string()),
1545            field_type: FieldType::Text,
1546            ..FormField::anonymous(FieldType::Text)
1547        }];
1548        assert!(!detect_csrf_token(&fields));
1549    }
1550
1551    #[test]
1552    fn test_detect_csrf_token_various_patterns() {
1553        let names = &[
1554            "_token",
1555            "authenticity_token",
1556            "csrfmiddlewaretoken",
1557            "_csrf_token",
1558            "xsrf",
1559        ];
1560        for name in names {
1561            let fields = vec![FormField {
1562                name: Some(name.to_string()),
1563                field_type: FieldType::Hidden,
1564                ..FormField::anonymous(FieldType::Hidden)
1565            }];
1566            assert!(detect_csrf_token(&fields), "应检测到 CSRF 字段: {name}");
1567        }
1568    }
1569
1570    // ─── 验证码检测测试 ────────────────────────────────────
1571
1572    #[test]
1573    fn test_detect_captcha_by_name() {
1574        let fields = vec![FormField {
1575            name: Some("captcha_code".to_string()),
1576            field_type: FieldType::Text,
1577            ..FormField::anonymous(FieldType::Text)
1578        }];
1579        assert!(detect_captcha(&fields));
1580    }
1581
1582    #[test]
1583    fn test_detect_captcha_by_id() {
1584        let fields = vec![FormField {
1585            id: Some("g-recaptcha".to_string()),
1586            field_type: FieldType::Text,
1587            ..FormField::anonymous(FieldType::Text)
1588        }];
1589        assert!(detect_captcha(&fields));
1590    }
1591
1592    #[test]
1593    fn test_detect_captcha_absent() {
1594        let fields = vec![FormField::new("username", FieldType::Text)];
1595        assert!(!detect_captcha(&fields));
1596    }
1597
1598    #[test]
1599    fn test_detect_captcha_recaptcha() {
1600        let fields = vec![FormField {
1601            name: Some("g-recaptcha-response".to_string()),
1602            field_type: FieldType::Text,
1603            ..FormField::anonymous(FieldType::Text)
1604        }];
1605        assert!(detect_captcha(&fields));
1606    }
1607
1608    #[test]
1609    fn test_detect_captcha_turnstile() {
1610        let fields = vec![FormField {
1611            id: Some("cf-turnstile".to_string()),
1612            field_type: FieldType::Text,
1613            ..FormField::anonymous(FieldType::Text)
1614        }];
1615        assert!(detect_captcha(&fields));
1616    }
1617
1618    // ─── 提交文本提取测试 ────────────────────────────────────
1619
1620    #[test]
1621    fn test_extract_submit_text_input() {
1622        let html = r#"<form>
1623            <input type="submit" value="登录">
1624        </form>"#;
1625        let doc = parse_html(html);
1626        let form_elem = first_form_element(&doc);
1627        let text = extract_submit_text(&form_elem);
1628        assert_eq!(text, Some("登录".to_string()));
1629    }
1630
1631    #[test]
1632    fn test_extract_submit_text_button() {
1633        let html = r#"<form>
1634            <button type="submit">注册账号</button>
1635        </form>"#;
1636        let doc = parse_html(html);
1637        let form_elem = first_form_element(&doc);
1638        let text = extract_submit_text(&form_elem);
1639        assert_eq!(text, Some("注册账号".to_string()));
1640    }
1641
1642    #[test]
1643    fn test_extract_submit_text_button_with_value() {
1644        let html = r#"<form>
1645            <button type="submit" value="提交">发送</button>
1646        </form>"#;
1647        let doc = parse_html(html);
1648        let form_elem = first_form_element(&doc);
1649        let text = extract_submit_text(&form_elem);
1650        assert_eq!(text, Some("发送".to_string()));
1651    }
1652
1653    #[test]
1654    fn test_extract_submit_text_no_submit_button() {
1655        let html = r#"<form><input type="text" name="q"></form>"#;
1656        let doc = parse_html(html);
1657        let form_elem = first_form_element(&doc);
1658        let text = extract_submit_text(&form_elem);
1659        assert!(text.is_none());
1660    }
1661
1662    #[test]
1663    fn test_extract_submit_text_only_first() {
1664        let html = r#"<form>
1665            <input type="submit" value="提交">
1666            <input type="submit" value="重置">
1667        </form>"#;
1668        let doc = parse_html(html);
1669        let form_elem = first_form_element(&doc);
1670        let text = extract_submit_text(&form_elem);
1671        assert_eq!(text, Some("提交".to_string()));
1672    }
1673
1674    // ─── 便利函数测试 ────────────────────────────────────────
1675
1676    #[test]
1677    fn test_get_login_forms() {
1678        let mut login_form = Form::new();
1679        login_form.form_type = FormType::Login;
1680        let mut search_form = Form::new();
1681        search_form.form_type = FormType::Search;
1682
1683        let forms = vec![login_form, search_form];
1684        let logins = get_login_forms(&forms);
1685        assert_eq!(logins.len(), 1);
1686        assert_eq!(logins[0].form_type, FormType::Login);
1687    }
1688
1689    #[test]
1690    fn test_get_search_forms() {
1691        let mut login_form = Form::new();
1692        login_form.form_type = FormType::Login;
1693        let mut search_form = Form::new();
1694        search_form.form_type = FormType::Search;
1695
1696        let forms = vec![login_form, search_form];
1697        let searches = get_search_forms(&forms);
1698        assert_eq!(searches.len(), 1);
1699        assert_eq!(searches[0].form_type, FormType::Search);
1700    }
1701
1702    #[test]
1703    fn test_get_contact_forms() {
1704        let mut contact_form = Form::new();
1705        contact_form.form_type = FormType::Contact;
1706        let mut unknown_form = Form::new();
1707        unknown_form.form_type = FormType::Unknown;
1708
1709        let forms = vec![contact_form, unknown_form];
1710        let contacts = get_contact_forms(&forms);
1711        assert_eq!(contacts.len(), 1);
1712        assert_eq!(contacts[0].form_type, FormType::Contact);
1713    }
1714
1715    #[test]
1716    fn test_get_contact_forms_empty() {
1717        let forms: Vec<Form> = Vec::new();
1718        assert!(get_contact_forms(&forms).is_empty());
1719    }
1720
1721    #[test]
1722    fn test_has_forms_true() {
1723        let html = r#"<html><body><form><input type="text"></form></body></html>"#;
1724        let doc = parse_html(html);
1725        assert!(has_forms(&doc));
1726    }
1727
1728    #[test]
1729    fn test_has_forms_false() {
1730        let html = r"<html><body><p>无表单</p></body></html>";
1731        let doc = parse_html(html);
1732        assert!(!has_forms(&doc));
1733    }
1734
1735    #[test]
1736    fn test_has_login_form_true() {
1737        let html = r#"<html><body>
1738            <form id="login" action="/login">
1739                <input type="text" name="username">
1740                <input type="password" name="password">
1741            </form>
1742        </body></html>"#;
1743        let doc = parse_html(html);
1744        assert!(has_login_form(&doc));
1745    }
1746
1747    #[test]
1748    fn test_has_login_form_false() {
1749        let html = r#"<html><body>
1750            <form action="/search">
1751                <input type="search" name="q">
1752            </form>
1753        </body></html>"#;
1754        let doc = parse_html(html);
1755        assert!(!has_login_form(&doc));
1756    }
1757
1758    #[test]
1759    fn test_has_search_form_true() {
1760        let html = r#"<html><body>
1761            <form action="/search">
1762                <input type="search" name="q">
1763            </form>
1764        </body></html>"#;
1765        let doc = parse_html(html);
1766        assert!(has_search_form(&doc));
1767    }
1768
1769    #[test]
1770    fn test_has_search_form_false() {
1771        let html = r"<html><body><p>无搜索</p></body></html>";
1772        let doc = parse_html(html);
1773        assert!(!has_search_form(&doc));
1774    }
1775
1776    // ─── Form 方法测试 ──────────────────────────────────────
1777
1778    #[test]
1779    fn test_form_get_field_by_name() {
1780        let mut form = Form::new();
1781        form.fields = vec![
1782            FormField::new("username", FieldType::Text),
1783            FormField::new("password", FieldType::Password),
1784        ];
1785        assert!(form.get_field_by_name("username").is_some());
1786        assert!(form.get_field_by_name("nonexistent").is_none());
1787    }
1788
1789    #[test]
1790    fn test_form_visible_fields() {
1791        let mut form = Form::new();
1792        form.fields = vec![
1793            FormField::new("username", FieldType::Text),
1794            FormField {
1795                name: Some("token".to_string()),
1796                field_type: FieldType::Hidden,
1797                ..FormField::anonymous(FieldType::Hidden)
1798            },
1799        ];
1800        let visible = form.visible_fields();
1801        assert_eq!(visible.len(), 1);
1802        assert_eq!(visible[0].name, Some("username".to_string()));
1803    }
1804
1805    #[test]
1806    fn test_form_input_fields() {
1807        let mut form = Form::new();
1808        form.fields = vec![
1809            FormField::new("text", FieldType::Text),
1810            FormField::new("email", FieldType::Email),
1811            FormField::new("submit", FieldType::Submit),
1812            FormField {
1813                name: Some("hidden".to_string()),
1814                field_type: FieldType::Hidden,
1815                ..FormField::anonymous(FieldType::Hidden)
1816            },
1817        ];
1818        let inputs = form.input_fields();
1819        assert_eq!(inputs.len(), 2);
1820    }
1821
1822    // ─── 集成测试 ──────────────────────────────────────────
1823
1824    #[test]
1825    fn test_complete_login_form_extraction() {
1826        let html = r#"<html><body>
1827            <form id="login-form" action="/signin" method="post" name="login">
1828                <label for="username">用户名</label>
1829                <input type="text" id="username" name="username" placeholder="请输入用户名" required>
1830
1831                <label for="password">密码</label>
1832                <input type="password" id="password" name="password" placeholder="请输入密码" required minlength="6">
1833
1834                <label>
1835                    <input type="checkbox" name="remember" value="1"> 记住我
1836                </label>
1837
1838                <input type="hidden" name="_csrf_token" value="abc123">
1839
1840                <button type="submit">登录</button>
1841            </form>
1842        </body></html>"#;
1843
1844        let doc = parse_html(html);
1845        let forms = extract_forms(&doc);
1846
1847        assert_eq!(forms.len(), 1);
1848        let form = &forms[0];
1849
1850        assert_eq!(form.id, Some("login-form".to_string()));
1851        assert_eq!(form.name, Some("login".to_string()));
1852        assert_eq!(form.action, Some("/signin".to_string()));
1853        assert_eq!(form.method, FormMethod::Post);
1854        assert_eq!(form.form_type, FormType::Login);
1855        assert!(form.has_csrf);
1856        assert!(!form.has_captcha);
1857        assert_eq!(form.submit_text, Some("登录".to_string()));
1858
1859        assert_eq!(form.fields.len(), 4);
1860
1861        // 验证字段标签关联
1862        let user_field = form.get_field_by_name("username").unwrap();
1863        assert_eq!(user_field.label, Some("用户名".to_string()));
1864        assert!(user_field.required);
1865
1866        let pass_field = form.get_field_by_name("password").unwrap();
1867        assert_eq!(pass_field.label, Some("密码".to_string()));
1868        assert_eq!(pass_field.min_length, Some(6));
1869
1870        let csrf_field = form.get_field_by_name("_csrf_token").unwrap();
1871        assert_eq!(csrf_field.field_type, FieldType::Hidden);
1872    }
1873
1874    #[test]
1875    fn test_complete_search_form_extraction() {
1876        let html = r#"<html><body>
1877            <form action="/search" method="get" id="search-form">
1878                <label for="q">搜索</label>
1879                <input type="search" id="q" name="q" placeholder="输入关键词">
1880                <button type="submit">搜索</button>
1881            </form>
1882        </body></html>"#;
1883
1884        let doc = parse_html(html);
1885        let forms = extract_forms(&doc);
1886
1887        assert_eq!(forms.len(), 1);
1888        let form = &forms[0];
1889
1890        assert_eq!(form.form_type, FormType::Search);
1891        assert_eq!(form.method, FormMethod::Get);
1892        assert_eq!(form.submit_text, Some("搜索".to_string()));
1893        assert!(!form.has_csrf);
1894
1895        let field = form.get_field_by_name("q").unwrap();
1896        assert_eq!(field.label, Some("搜索".to_string()));
1897        assert_eq!(field.field_type, FieldType::Search);
1898    }
1899
1900    #[test]
1901    fn test_no_forms_in_empty_document() {
1902        let html = r"<html><head><title>空页面</title></head><body></body></html>";
1903        let doc = parse_html(html);
1904        let forms = extract_forms(&doc);
1905        assert!(forms.is_empty());
1906        assert!(!has_forms(&doc));
1907    }
1908
1909    #[test]
1910    fn test_complex_form_with_select_and_textarea() {
1911        let html = r#"<html><body>
1912            <form id="contact-form" action="/contact" method="post">
1913                <label for="name">姓名</label>
1914                <input type="text" id="name" name="name" required>
1915
1916                <label for="category">分类</label>
1917                <select id="category" name="category">
1918                    <option value="general">一般咨询</option>
1919                    <option value="support" selected>技术支持</option>
1920                    <option value="sales">销售</option>
1921                </select>
1922
1923                <label for="message">消息</label>
1924                <textarea id="message" name="message" placeholder="请输入内容" required></textarea>
1925
1926                <input type="submit" value="提交">
1927            </form>
1928        </body></html>"#;
1929
1930        let doc = parse_html(html);
1931        let forms = extract_forms(&doc);
1932
1933        assert_eq!(forms.len(), 1);
1934        let form = &forms[0];
1935
1936        assert_eq!(form.form_type, FormType::Contact);
1937        assert_eq!(form.fields.len(), 4);
1938
1939        // 检查 select 字段
1940        let category = form.get_field_by_name("category").unwrap();
1941        assert_eq!(category.field_type, FieldType::Select);
1942        assert_eq!(category.options.len(), 3);
1943        assert_eq!(category.options[0].label, "一般咨询");
1944        assert!(category.options[1].selected);
1945
1946        // 检查 textarea 字段
1947        let message = form.get_field_by_name("message").unwrap();
1948        assert_eq!(message.field_type, FieldType::Textarea);
1949        assert!(message.required);
1950    }
1951
1952    #[test]
1953    fn test_form_type_all_enum_variants() {
1954        let types = FormType::all();
1955        assert_eq!(types.len(), 10);
1956        assert!(types.contains(&FormType::Login));
1957        assert!(types.contains(&FormType::Unknown));
1958    }
1959
1960    #[test]
1961    fn test_form_without_id_or_name() {
1962        let html = r#"<form action="/submit">
1963            <input type="text" name="data">
1964        </form>"#;
1965        let doc = parse_html(html);
1966        let form_elem = first_form_element(&doc);
1967        let form = extract_form(&form_elem).unwrap();
1968        assert!(form.id.is_none());
1969        assert!(form.name.is_none());
1970        assert_eq!(form.form_type, FormType::Unknown);
1971    }
1972
1973    #[test]
1974    fn test_extract_forms_handles_malformed_html() {
1975        let html = r#"<html><body>
1976            <form action="/login">
1977                <input type=text name=user>
1978                <input type=password name=pass>
1979            </form>
1980        </body></html>"#;
1981        let doc = parse_html(html);
1982        let forms = extract_forms(&doc);
1983        assert_eq!(forms.len(), 1);
1984    }
1985
1986    #[test]
1987    fn test_detect_form_type_signin_variant() {
1988        let html = r#"<form action="/signin">
1989            <input type="text" name="user">
1990            <input type="password" name="pass">
1991        </form>"#;
1992        let doc = parse_html(html);
1993        let form_elem = first_form_element(&doc);
1994        let form = extract_form(&form_elem).unwrap();
1995        assert_eq!(form.form_type, FormType::Login);
1996    }
1997
1998    #[test]
1999    fn test_detect_form_type_create_account() {
2000        let html = r#"<form id="create-account-form">
2001            <input type="email" name="email">
2002            <input type="password" name="password">
2003            <input type="password" name="confirm_password">
2004        </form>"#;
2005        let doc = parse_html(html);
2006        let form_elem = first_form_element(&doc);
2007        let form = extract_form(&form_elem).unwrap();
2008        assert_eq!(form.form_type, FormType::Registration);
2009    }
2010
2011    #[test]
2012    fn test_detect_form_type_search_with_q_name() {
2013        let html = r#"<form>
2014            <input type="text" name="q">
2015        </form>"#;
2016        let doc = parse_html(html);
2017        let form_elem = first_form_element(&doc);
2018        let form = extract_form(&form_elem).unwrap();
2019        assert_eq!(form.form_type, FormType::Search);
2020    }
2021
2022    #[test]
2023    fn test_detect_csrf_token_django_style() {
2024        let fields = vec![FormField {
2025            name: Some("csrfmiddlewaretoken".to_string()),
2026            field_type: FieldType::Hidden,
2027            ..FormField::anonymous(FieldType::Hidden)
2028        }];
2029        assert!(detect_csrf_token(&fields));
2030    }
2031
2032    #[test]
2033    fn test_detect_csrf_token_no_hidden_skip() {
2034        let fields = vec![FormField::new("_token", FieldType::Text)];
2035        assert!(!detect_csrf_token(&fields));
2036    }
2037
2038    #[test]
2039    fn test_detect_captcha_hcaptcha() {
2040        let fields = vec![FormField {
2041            id: Some("h-captcha".to_string()),
2042            field_type: FieldType::Text,
2043            ..FormField::anonymous(FieldType::Text)
2044        }];
2045        assert!(detect_captcha(&fields));
2046    }
2047
2048    #[test]
2049    fn test_get_login_forms_no_match() {
2050        let forms = vec![
2051            Form { form_type: FormType::Search, ..Form::new() },
2052            Form { form_type: FormType::Contact, ..Form::new() },
2053        ];
2054        assert!(get_login_forms(&forms).is_empty());
2055    }
2056
2057    #[test]
2058    fn test_get_search_forms_multiple() {
2059        let forms = vec![
2060            Form { form_type: FormType::Search, ..Form::new() },
2061            Form { form_type: FormType::Search, ..Form::new() },
2062            Form { form_type: FormType::Login, ..Form::new() },
2063        ];
2064        assert_eq!(get_search_forms(&forms).len(), 2);
2065    }
2066}