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