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")]
18pub enum FormMethod {
19 Get,
21 Post,
23 Dialog,
25}
26
27impl Default for FormMethod {
28 fn default() -> Self {
29 Self::Get
30 }
31}
32
33impl FormMethod {
34 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
50#[serde(rename_all = "snake_case")]
51pub enum FormType {
52 Login,
54 Registration,
56 Search,
58 Contact,
60 Newsletter,
62 PasswordReset,
64 Checkout,
66 Comment,
68 Upload,
70 Unknown,
72}
73
74impl FormType {
75 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
98#[serde(rename_all = "snake_case")]
99pub enum FieldType {
100 Text,
102 Email,
104 Password,
106 Number,
108 Tel,
110 Url,
112 Search,
114 Textarea,
116 Select,
118 Checkbox,
120 Radio,
122 File,
124 Hidden,
126 Submit,
128 Button,
130 Date,
132 Time,
134 Color,
136 Range,
138 Unknown,
140}
141
142impl FieldType {
143 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 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 pub fn is_selectable(&self) -> bool {
191 matches!(self, Self::Select | Self::Checkbox | Self::Radio)
192 }
193
194 pub fn is_button(&self) -> bool {
196 matches!(self, Self::Submit | Self::Button)
197 }
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct SelectOption {
207 pub value: String,
209 pub label: String,
211 pub selected: bool,
213}
214
215impl SelectOption {
216 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#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct FormField {
233 pub name: Option<String>,
235 pub id: Option<String>,
237 pub field_type: FieldType,
239 pub label: Option<String>,
241 pub placeholder: Option<String>,
243 pub value: Option<String>,
245 pub required: bool,
247 pub disabled: bool,
249 pub readonly: bool,
251 pub autocomplete: Option<String>,
253 pub pattern: Option<String>,
255 pub min_length: Option<usize>,
257 pub max_length: Option<usize>,
259 pub options: Vec<SelectOption>,
261}
262
263impl FormField {
264 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct Form {
312 pub id: Option<String>,
314 pub name: Option<String>,
316 pub action: Option<String>,
318 pub method: FormMethod,
320 pub enctype: Option<String>,
322 pub fields: Vec<FormField>,
324 pub form_type: FormType,
326 pub has_csrf: bool,
328 pub has_captcha: bool,
330 pub submit_text: Option<String>,
332}
333
334impl Form {
335 pub fn new() -> Self {
337 Self::default()
338 }
339
340 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 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 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
376pub 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 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
444fn 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
457pub 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
488pub 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 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
525pub 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
546pub fn extract_form_fields(element: &ElementRef) -> Vec<FormField> {
548 let mut fields = Vec::new();
549
550 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
574pub 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 form.fields = extract_form_fields(element);
595
596 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
605pub 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 let labeled = associate_labels(document, form.fields);
618 forms.push(Form { fields: labeled, ..form });
619 }
620 }
621
622 forms
623}
624
625pub 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
664pub 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
698pub 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
734pub 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 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 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 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 if has_email
803 && field_names.iter().any(|n| n.contains("remember") || n.contains("stay"))
804 {
805 return FormType::Login;
806 }
807
808 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 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 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 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 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 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 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 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
920pub fn get_login_forms(forms: &[Form]) -> Vec<&Form> {
926 forms.iter().filter(|f| f.form_type == FormType::Login).collect()
927}
928
929pub fn get_search_forms(forms: &[Form]) -> Vec<&Form> {
931 forms.iter().filter(|f| f.form_type == FormType::Search).collect()
932}
933
934pub fn get_contact_forms(forms: &[Form]) -> Vec<&Form> {
936 forms.iter().filter(|f| f.form_type == FormType::Contact).collect()
937}
938
939pub 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
948pub fn has_login_form(document: &Html) -> bool {
950 let forms = extract_forms(document);
951 get_login_forms(&forms).first().is_some()
952}
953
954pub fn has_search_form(document: &Html) -> bool {
956 let forms = extract_forms(document);
957 get_search_forms(&forms).first().is_some()
958}
959
960#[cfg(test)]
965mod tests {
966 use super::*;
967
968 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 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}