1use scraper::{ElementRef, Html};
7use serde::{Deserialize, Serialize};
8
9use crate::selector::try_parse_selector;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18#[derive(Default)]
19pub enum FormMethod {
20 #[default]
22 Get,
23 Post,
25 Dialog,
27}
28
29
30impl FormMethod {
31 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
47#[serde(rename_all = "snake_case")]
48pub enum FormType {
49 Login,
51 Registration,
53 Search,
55 Contact,
57 Newsletter,
59 PasswordReset,
61 Checkout,
63 Comment,
65 Upload,
67 Unknown,
69}
70
71impl FormType {
72 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(rename_all = "snake_case")]
96pub enum FieldType {
97 Text,
99 Email,
101 Password,
103 Number,
105 Tel,
107 Url,
109 Search,
111 Textarea,
113 Select,
115 Checkbox,
117 Radio,
119 File,
121 Hidden,
123 Submit,
125 Button,
127 Date,
129 Time,
131 Color,
133 Range,
135 Unknown,
137}
138
139impl FieldType {
140 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 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 pub fn is_selectable(&self) -> bool {
188 matches!(self, Self::Select | Self::Checkbox | Self::Radio)
189 }
190
191 pub fn is_button(&self) -> bool {
193 matches!(self, Self::Submit | Self::Button)
194 }
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct SelectOption {
204 pub value: String,
206 pub label: String,
208 pub selected: bool,
210}
211
212impl SelectOption {
213 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#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct FormField {
230 pub name: Option<String>,
232 pub id: Option<String>,
234 pub field_type: FieldType,
236 pub label: Option<String>,
238 pub placeholder: Option<String>,
240 pub value: Option<String>,
242 pub required: bool,
244 pub disabled: bool,
246 pub readonly: bool,
248 pub autocomplete: Option<String>,
250 pub pattern: Option<String>,
252 pub min_length: Option<usize>,
254 pub max_length: Option<usize>,
256 pub options: Vec<SelectOption>,
258}
259
260impl FormField {
261 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct Form {
309 pub id: Option<String>,
311 pub name: Option<String>,
313 pub action: Option<String>,
315 pub method: FormMethod,
317 pub enctype: Option<String>,
319 pub fields: Vec<FormField>,
321 pub form_type: FormType,
323 pub has_csrf: bool,
325 pub has_captcha: bool,
327 pub submit_text: Option<String>,
329}
330
331impl Form {
332 pub fn new() -> Self {
334 Self::default()
335 }
336
337 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 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 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
373pub 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 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
438fn 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
451pub 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
482pub 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 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
519pub 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
540pub fn extract_form_fields(element: &ElementRef) -> Vec<FormField> {
542 let mut fields = Vec::new();
543
544 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
568pub 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 form.fields = extract_form_fields(element);
589
590 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
599pub 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 let labeled = associate_labels(document, form.fields);
612 forms.push(Form { fields: labeled, ..form });
613 }
614 }
615
616 forms
617}
618
619pub 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
658pub 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
692pub 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
725pub 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 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 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 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 if has_email
794 && field_names.iter().any(|n| n.contains("remember") || n.contains("stay"))
795 {
796 return FormType::Login;
797 }
798
799 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 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 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 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 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 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 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 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
911pub fn get_login_forms(forms: &[Form]) -> Vec<&Form> {
917 forms.iter().filter(|f| f.form_type == FormType::Login).collect()
918}
919
920pub fn get_search_forms(forms: &[Form]) -> Vec<&Form> {
922 forms.iter().filter(|f| f.form_type == FormType::Search).collect()
923}
924
925pub fn get_contact_forms(forms: &[Form]) -> Vec<&Form> {
927 forms.iter().filter(|f| f.form_type == FormType::Contact).collect()
928}
929
930pub 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
939pub fn has_login_form(document: &Html) -> bool {
941 let forms = extract_forms(document);
942 !get_login_forms(&forms).is_empty()
943}
944
945pub fn has_search_form(document: &Html) -> bool {
947 let forms = extract_forms(document);
948 !get_search_forms(&forms).is_empty()
949}
950
951#[cfg(test)]
956mod tests {
957 use super::*;
958
959 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 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}