1mod builder;
72mod error;
73mod field;
74mod render;
75mod template_render;
76
77pub use builder::{FieldBuilder, FileFieldBuilder, FormBuilder};
78pub use error::{FieldError, ValidationErrors};
79pub use field::{FileFieldAttrs, FormField, InputType, SelectOption};
80pub use render::{FormRenderOptions, FormRenderer};
81pub use template_render::{FormRenderError, TemplateFormRenderer};
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn test_simple_form() {
89 let form = FormBuilder::new("/login", "POST")
90 .field("email", InputType::Email)
91 .label("Email")
92 .required()
93 .done()
94 .field("password", InputType::Password)
95 .label("Password")
96 .required()
97 .done()
98 .submit("Login")
99 .build();
100
101 assert!(form.contains(r#"action="/login""#));
102 assert!(form.contains(r#"method="POST""#));
103 assert!(form.contains(r#"type="email""#));
104 assert!(form.contains(r#"type="password""#));
105 assert!(form.contains("Login"));
106 }
107
108 #[test]
109 fn test_csrf_injection() {
110 let form = FormBuilder::new("/users", "POST")
111 .csrf_token("test_token_123")
112 .build();
113
114 assert!(form.contains(r#"name="_csrf_token""#));
115 assert!(form.contains(r#"value="test_token_123""#));
116 }
117
118 #[test]
119 fn test_htmx_attributes() {
120 let form = FormBuilder::new("/search", "GET")
121 .htmx_get("/api/search")
122 .htmx_target("#results")
123 .htmx_swap("innerHTML")
124 .htmx_indicator("#spinner")
125 .build();
126
127 assert!(form.contains(r#"hx-get="/api/search""#));
128 assert!(form.contains(r##"hx-target="#results""##));
129 assert!(form.contains(r#"hx-swap="innerHTML""#));
130 assert!(form.contains(r##"hx-indicator="#spinner""##));
131 }
132
133 #[test]
134 fn test_validation_errors() {
135 let mut errors = ValidationErrors::new();
136 errors.add("email", "is required");
137
138 let form = FormBuilder::new("/users", "POST")
139 .errors(&errors)
140 .field("email", InputType::Email)
141 .label("Email")
142 .done()
143 .build();
144
145 assert!(form.contains("is required"));
146 assert!(form.contains("form-error"));
147 }
148
149 #[test]
150 fn test_field_attributes() {
151 let form = FormBuilder::new("/test", "POST")
152 .field("name", InputType::Text)
153 .label("Full Name")
154 .placeholder("John Doe")
155 .required()
156 .min_length(2)
157 .max_length(100)
158 .pattern(r"[A-Za-z\s]+")
159 .done()
160 .build();
161
162 assert!(form.contains(r#"placeholder="John Doe""#));
163 assert!(form.contains("required"));
164 assert!(form.contains(r#"minlength="2""#));
165 assert!(form.contains(r#"maxlength="100""#));
166 assert!(form.contains("pattern="));
167 }
168
169 #[test]
170 fn test_select_field() {
171 let form = FormBuilder::new("/test", "POST")
172 .select("country")
173 .label("Country")
174 .option("us", "United States")
175 .option("ca", "Canada")
176 .option("mx", "Mexico")
177 .selected("us")
178 .done()
179 .build();
180
181 assert!(form.contains("<select"));
182 assert!(form.contains(r#"value="us""#));
183 assert!(form.contains("United States"));
184 assert!(form.contains("selected"));
185 }
186
187 #[test]
188 fn test_textarea_field() {
189 let form = FormBuilder::new("/test", "POST")
190 .textarea("bio")
191 .label("Biography")
192 .placeholder("Tell us about yourself...")
193 .rows(5)
194 .cols(40)
195 .done()
196 .build();
197
198 assert!(form.contains("<textarea"));
199 assert!(form.contains(r#"rows="5""#));
200 assert!(form.contains(r#"cols="40""#));
201 }
202
203 #[test]
204 fn test_checkbox_field() {
205 let form = FormBuilder::new("/test", "POST")
206 .checkbox("terms")
207 .label("I agree to the terms")
208 .checked()
209 .done()
210 .build();
211
212 assert!(form.contains(r#"type="checkbox""#));
213 assert!(form.contains("checked"));
214 }
215
216 #[test]
217 fn test_form_id_and_class() {
218 let form = FormBuilder::new("/test", "POST")
219 .id("my-form")
220 .class("form-styled")
221 .build();
222
223 assert!(form.contains(r#"id="my-form""#));
224 assert!(form.contains(r#"class="form-styled""#));
225 }
226
227 #[test]
228 fn test_file_upload_field() {
229 let form = FormBuilder::new("/upload", "POST")
230 .file("avatar")
231 .label("Profile Picture")
232 .accept("image/png,image/jpeg")
233 .max_size_mb(5)
234 .required()
235 .done()
236 .build();
237
238 assert!(form.contains(r#"enctype="multipart/form-data""#));
239 assert!(form.contains(r#"type="file""#));
240 assert!(form.contains(r#"accept="image/png,image/jpeg""#));
241 assert!(form.contains(r#"data-max-size-mb="5""#));
242 assert!(form.contains("required"));
243 }
244
245 #[test]
246 fn test_file_upload_multiple() {
247 let form = FormBuilder::new("/upload", "POST")
248 .file("attachments")
249 .label("Attachments")
250 .multiple()
251 .done()
252 .build();
253
254 assert!(form.contains(r#"enctype="multipart/form-data""#));
255 assert!(form.contains("multiple"));
256 }
257
258 #[test]
259 fn test_file_upload_with_preview() {
260 let form = FormBuilder::new("/upload", "POST")
261 .file("image")
262 .label("Image")
263 .show_preview()
264 .drag_drop()
265 .done()
266 .build();
267
268 assert!(form.contains(r#"data-preview="true""#));
269 assert!(form.contains(r#"data-drag-drop="true""#));
270 }
271
272 #[test]
273 fn test_file_upload_with_progress_endpoint() {
274 let form = FormBuilder::new("/upload", "POST")
275 .file("large_file")
276 .label("Large File")
277 .progress_endpoint("/upload/progress")
278 .done()
279 .build();
280
281 assert!(form.contains(r#"data-progress-endpoint="/upload/progress""#));
282 }
283
284 #[test]
285 fn test_multipart_auto_set() {
286 let form = FormBuilder::new("/upload", "POST")
287 .file("file1")
288 .done()
289 .build();
290
291 assert!(form.contains(r#"enctype="multipart/form-data""#));
293 }
294
295 #[test]
296 fn test_file_upload_with_help_text() {
297 let form = FormBuilder::new("/upload", "POST")
298 .file("avatar")
299 .label("Avatar")
300 .help("Maximum size: 5MB. Accepted formats: PNG, JPEG")
301 .done()
302 .build();
303
304 assert!(form.contains("Maximum size: 5MB"));
305 assert!(form.contains("Accepted formats: PNG, JPEG"));
306 }
307}