jugar_probar/
page_object.rs1use crate::locator::{Locator, Selector};
13use std::collections::HashMap;
14
15pub trait PageObject {
56 fn url_pattern(&self) -> &str;
58
59 fn is_loaded(&self) -> bool {
61 true
62 }
63
64 fn load_timeout_ms(&self) -> u64 {
66 30000
67 }
68
69 fn page_name(&self) -> &str {
71 std::any::type_name::<Self>()
72 }
73}
74
75#[derive(Debug, Clone)]
77pub struct PageObjectBuilder {
78 url_pattern: String,
79 locators: HashMap<String, Locator>,
80 load_timeout_ms: u64,
81}
82
83impl Default for PageObjectBuilder {
84 fn default() -> Self {
85 Self::new()
86 }
87}
88
89impl PageObjectBuilder {
90 #[must_use]
92 pub fn new() -> Self {
93 Self {
94 url_pattern: String::new(),
95 locators: HashMap::new(),
96 load_timeout_ms: 30000,
97 }
98 }
99
100 #[must_use]
102 pub fn with_url_pattern(mut self, pattern: impl Into<String>) -> Self {
103 self.url_pattern = pattern.into();
104 self
105 }
106
107 #[must_use]
109 pub fn with_locator(mut self, name: impl Into<String>, selector: Selector) -> Self {
110 let _ = self
111 .locators
112 .insert(name.into(), Locator::from_selector(selector));
113 self
114 }
115
116 #[must_use]
118 pub const fn with_load_timeout(mut self, timeout_ms: u64) -> Self {
119 self.load_timeout_ms = timeout_ms;
120 self
121 }
122
123 #[must_use]
125 pub fn build(self) -> SimplePageObject {
126 SimplePageObject {
127 url_pattern: self.url_pattern,
128 locators: self.locators,
129 load_timeout_ms: self.load_timeout_ms,
130 }
131 }
132}
133
134#[derive(Debug, Clone)]
136pub struct SimplePageObject {
137 url_pattern: String,
138 locators: HashMap<String, Locator>,
139 load_timeout_ms: u64,
140}
141
142impl SimplePageObject {
143 #[must_use]
145 pub fn new(url_pattern: impl Into<String>) -> Self {
146 Self {
147 url_pattern: url_pattern.into(),
148 locators: HashMap::new(),
149 load_timeout_ms: 30000,
150 }
151 }
152
153 #[must_use]
155 pub fn locator(&self, name: &str) -> Option<&Locator> {
156 self.locators.get(name)
157 }
158
159 pub fn add_locator(&mut self, name: impl Into<String>, selector: Selector) {
161 let _ = self
162 .locators
163 .insert(name.into(), Locator::from_selector(selector));
164 }
165
166 #[must_use]
168 pub fn locator_names(&self) -> Vec<&str> {
169 self.locators.keys().map(String::as_str).collect()
170 }
171}
172
173impl PageObject for SimplePageObject {
174 fn url_pattern(&self) -> &str {
175 &self.url_pattern
176 }
177
178 fn load_timeout_ms(&self) -> u64 {
179 self.load_timeout_ms
180 }
181}
182
183#[derive(Debug, Default)]
185pub struct PageRegistry {
186 pages: HashMap<String, Box<dyn PageObjectInfo>>,
187}
188
189pub trait PageObjectInfo: std::fmt::Debug + Send + Sync {
191 fn url_pattern(&self) -> &str;
193
194 fn page_name(&self) -> &str;
196
197 fn load_timeout_ms(&self) -> u64;
199}
200
201impl<T: PageObject + std::fmt::Debug + Send + Sync + 'static> PageObjectInfo for T {
202 fn url_pattern(&self) -> &str {
203 PageObject::url_pattern(self)
204 }
205
206 fn page_name(&self) -> &str {
207 PageObject::page_name(self)
208 }
209
210 fn load_timeout_ms(&self) -> u64 {
211 PageObject::load_timeout_ms(self)
212 }
213}
214
215impl PageRegistry {
216 #[must_use]
218 pub fn new() -> Self {
219 Self::default()
220 }
221
222 pub fn register<T: PageObject + std::fmt::Debug + Send + Sync + 'static>(
224 &mut self,
225 name: impl Into<String>,
226 page: T,
227 ) {
228 let _ = self.pages.insert(name.into(), Box::new(page));
229 }
230
231 #[must_use]
233 pub fn get(&self, name: &str) -> Option<&dyn PageObjectInfo> {
234 self.pages.get(name).map(|p| p.as_ref())
235 }
236
237 #[must_use]
239 pub fn list(&self) -> Vec<&str> {
240 self.pages.keys().map(String::as_str).collect()
241 }
242
243 #[must_use]
245 pub fn count(&self) -> usize {
246 self.pages.len()
247 }
248}
249
250#[derive(Debug, Clone)]
252pub struct UrlMatcher {
253 pattern: String,
254 segments: Vec<UrlSegment>,
255}
256
257#[derive(Debug, Clone)]
258enum UrlSegment {
259 Literal(String),
260 Wildcard,
261 Parameter(String),
262}
263
264impl UrlMatcher {
265 #[must_use]
272 pub fn new(pattern: &str) -> Self {
273 let segments = pattern
274 .split('/')
275 .filter(|s| !s.is_empty())
276 .map(|s| {
277 if s == "*" {
278 UrlSegment::Wildcard
279 } else if let Some(name) = s.strip_prefix(':') {
280 UrlSegment::Parameter(name.to_string())
281 } else {
282 UrlSegment::Literal(s.to_string())
283 }
284 })
285 .collect();
286
287 Self {
288 pattern: pattern.to_string(),
289 segments,
290 }
291 }
292
293 #[must_use]
295 pub fn matches(&self, url: &str) -> bool {
296 let url_segments: Vec<&str> = url.split('/').filter(|s| !s.is_empty()).collect();
297
298 if url_segments.len() != self.segments.len() {
301 return false;
302 }
303
304 for (i, segment) in self.segments.iter().enumerate() {
305 match segment {
306 UrlSegment::Literal(lit) => {
307 if url_segments.get(i) != Some(&lit.as_str()) {
308 return false;
309 }
310 }
311 UrlSegment::Wildcard | UrlSegment::Parameter(_) => {
312 }
314 }
315 }
316
317 true
318 }
319
320 #[must_use]
322 pub fn extract_params(&self, url: &str) -> HashMap<String, String> {
323 let mut params = HashMap::new();
324 let url_segments: Vec<&str> = url.split('/').filter(|s| !s.is_empty()).collect();
325
326 for (i, segment) in self.segments.iter().enumerate() {
327 if let UrlSegment::Parameter(name) = segment {
328 if let Some(value) = url_segments.get(i) {
329 let _ = params.insert(name.clone(), (*value).to_string());
330 }
331 }
332 }
333
334 params
335 }
336
337 #[must_use]
339 pub fn pattern(&self) -> &str {
340 &self.pattern
341 }
342}
343
344#[cfg(test)]
345#[allow(clippy::unwrap_used, clippy::expect_used)]
346mod tests {
347 use super::*;
348
349 mod page_object_builder_tests {
350 use super::*;
351
352 #[test]
353 fn test_builder_basic() {
354 let page = PageObjectBuilder::new()
355 .with_url_pattern("/login")
356 .with_load_timeout(5000)
357 .build();
358
359 assert_eq!(PageObject::url_pattern(&page), "/login");
360 assert_eq!(PageObject::load_timeout_ms(&page), 5000);
361 }
362
363 #[test]
364 fn test_builder_with_locators() {
365 let page = PageObjectBuilder::new()
366 .with_url_pattern("/login")
367 .with_locator("username", Selector::css("input[name='username']"))
368 .with_locator("password", Selector::css("input[name='password']"))
369 .build();
370
371 assert!(page.locator("username").is_some());
372 assert!(page.locator("password").is_some());
373 assert!(page.locator("nonexistent").is_none());
374 }
375
376 #[test]
377 fn test_default_builder() {
378 let builder = PageObjectBuilder::default();
379 let page = builder.build();
380 assert!(PageObject::url_pattern(&page).is_empty());
381 }
382 }
383
384 mod simple_page_object_tests {
385 use super::*;
386
387 #[test]
388 fn test_new() {
389 let page = SimplePageObject::new("/dashboard");
390 assert_eq!(PageObject::url_pattern(&page), "/dashboard");
391 assert_eq!(PageObject::load_timeout_ms(&page), 30000);
392 }
393
394 #[test]
395 fn test_add_locator() {
396 let mut page = SimplePageObject::new("/test");
397 page.add_locator("button", Selector::css("button"));
398
399 assert!(page.locator("button").is_some());
400 assert!(page.locator_names().contains(&"button"));
401 }
402
403 #[test]
404 fn test_is_loaded_default() {
405 let page = SimplePageObject::new("/test");
406 assert!(page.is_loaded());
407 }
408 }
409
410 mod page_registry_tests {
411 use super::*;
412
413 #[test]
414 fn test_new_registry() {
415 let registry = PageRegistry::new();
416 assert_eq!(registry.count(), 0);
417 }
418
419 #[test]
420 fn test_register_and_get() {
421 let mut registry = PageRegistry::new();
422 let page = SimplePageObject::new("/login");
423 registry.register("login", page);
424
425 assert_eq!(registry.count(), 1);
426 assert!(registry.get("login").is_some());
427 assert!(registry.get("nonexistent").is_none());
428 }
429
430 #[test]
431 fn test_list_pages() {
432 let mut registry = PageRegistry::new();
433 registry.register("login", SimplePageObject::new("/login"));
434 registry.register("home", SimplePageObject::new("/"));
435
436 let pages = registry.list();
437 assert_eq!(pages.len(), 2);
438 assert!(pages.contains(&"login"));
439 assert!(pages.contains(&"home"));
440 }
441 }
442
443 mod url_matcher_tests {
444 use super::*;
445
446 #[test]
447 fn test_literal_match() {
448 let matcher = UrlMatcher::new("/login");
449 assert!(matcher.matches("/login"));
450 assert!(!matcher.matches("/register"));
451 assert!(!matcher.matches("/login/extra"));
452 }
453
454 #[test]
455 fn test_wildcard_match() {
456 let matcher = UrlMatcher::new("/users/*");
457 assert!(matcher.matches("/users/123"));
458 assert!(matcher.matches("/users/abc"));
459 assert!(!matcher.matches("/users"));
460 assert!(!matcher.matches("/other/123"));
461 }
462
463 #[test]
464 fn test_parameter_match() {
465 let matcher = UrlMatcher::new("/users/:id");
466 assert!(matcher.matches("/users/123"));
467 assert!(matcher.matches("/users/abc"));
468 assert!(!matcher.matches("/users"));
469 }
470
471 #[test]
472 fn test_extract_params() {
473 let matcher = UrlMatcher::new("/users/:id/posts/:post_id");
474 let params = matcher.extract_params("/users/42/posts/100");
475
476 assert_eq!(params.get("id"), Some(&"42".to_string()));
477 assert_eq!(params.get("post_id"), Some(&"100".to_string()));
478 }
479
480 #[test]
481 fn test_complex_pattern() {
482 let matcher = UrlMatcher::new("/api/v1/users/:id");
483 assert!(matcher.matches("/api/v1/users/123"));
484 assert!(!matcher.matches("/api/v2/users/123"));
485 }
486
487 #[test]
488 fn test_pattern_getter() {
489 let matcher = UrlMatcher::new("/test/pattern");
490 assert_eq!(matcher.pattern(), "/test/pattern");
491 }
492 }
493
494 mod page_object_trait_tests {
495 use super::*;
496
497 #[derive(Debug)]
498 struct TestPage {
499 url: String,
500 loaded: bool,
501 }
502
503 impl PageObject for TestPage {
504 fn url_pattern(&self) -> &str {
505 &self.url
506 }
507
508 fn is_loaded(&self) -> bool {
509 self.loaded
510 }
511
512 fn load_timeout_ms(&self) -> u64 {
513 5000
514 }
515 }
516
517 #[test]
518 fn test_custom_page_object() {
519 let page = TestPage {
520 url: "/custom".to_string(),
521 loaded: true,
522 };
523
524 assert_eq!(PageObject::url_pattern(&page), "/custom");
525 assert!(PageObject::is_loaded(&page));
526 assert_eq!(PageObject::load_timeout_ms(&page), 5000);
527 }
528
529 #[test]
530 fn test_page_name() {
531 let page = SimplePageObject::new("/test");
532 assert!(PageObject::page_name(&page).contains("SimplePageObject"));
534 }
535 }
536}