viewpoint_core/context/routing_impl/mod.rs
1//! Context-level network routing implementation.
2
3use std::future::Future;
4use std::sync::Arc;
5
6use crate::error::NetworkError;
7use crate::network::{HarReplayOptions, Route, UrlPattern};
8
9use super::BrowserContext;
10
11impl BrowserContext {
12 /// Register a route handler that applies to all pages in this context.
13 ///
14 /// Routes registered at the context level are applied to all pages,
15 /// including new pages created after the route is registered.
16 /// Context routes are checked before page-level routes.
17 ///
18 /// # Example
19 ///
20 /// ```no_run
21 /// use viewpoint_core::{Browser, network::Route};
22 ///
23 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
24 /// let browser = Browser::launch().headless(true).launch().await?;
25 /// let context = browser.new_context().await?;
26 ///
27 /// // Block all analytics requests for all pages
28 /// context.route("**/analytics/**", |route: Route| async move {
29 /// route.abort().await
30 /// }).await?;
31 ///
32 /// // Mock an API for all pages
33 /// context.route("**/api/users", |route: Route| async move {
34 /// route.fulfill()
35 /// .status(200)
36 /// .json(&serde_json::json!({"users": []}))
37 /// .send()
38 /// .await
39 /// }).await?;
40 /// # Ok(())
41 /// # }
42 /// ```
43 ///
44 /// # Errors
45 ///
46 /// Returns an error if the context is closed.
47 pub async fn route<M, H, Fut>(&self, pattern: M, handler: H) -> Result<(), NetworkError>
48 where
49 M: Into<UrlPattern>,
50 H: Fn(Route) -> Fut + Send + Sync + 'static,
51 Fut: Future<Output = Result<(), NetworkError>> + Send + 'static,
52 {
53 if self.is_closed() {
54 return Err(NetworkError::Aborted);
55 }
56 self.route_registry.route(pattern, handler).await
57 }
58
59 /// Register a route handler with a predicate function.
60 ///
61 /// # Example
62 ///
63 /// ```no_run
64 /// use viewpoint_core::{Browser, network::Route};
65 ///
66 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
67 /// let browser = Browser::launch().headless(true).launch().await?;
68 /// let context = browser.new_context().await?;
69 ///
70 /// // Block POST requests to any API endpoint
71 /// context.route_predicate(
72 /// |url| url.contains("/api/"),
73 /// |route: Route| async move {
74 /// if route.request().method() == "POST" {
75 /// route.abort().await
76 /// } else {
77 /// route.continue_().await
78 /// }
79 /// }
80 /// ).await?;
81 /// # Ok(())
82 /// # }
83 /// ```
84 ///
85 /// # Errors
86 ///
87 /// Returns an error if the context is closed.
88 pub async fn route_predicate<P, H, Fut>(
89 &self,
90 predicate: P,
91 handler: H,
92 ) -> Result<(), NetworkError>
93 where
94 P: Fn(&str) -> bool + Send + Sync + 'static,
95 H: Fn(Route) -> Fut + Send + Sync + 'static,
96 Fut: Future<Output = Result<(), NetworkError>> + Send + 'static,
97 {
98 if self.is_closed() {
99 return Err(NetworkError::Aborted);
100 }
101 self.route_registry
102 .route_predicate(predicate, handler)
103 .await
104 }
105
106 /// Unregister handlers matching the given pattern.
107 ///
108 /// This removes handlers registered with `route()` that match the pattern.
109 pub async fn unroute(&self, pattern: &str) {
110 self.route_registry.unroute(pattern).await;
111 }
112
113 /// Unregister all route handlers.
114 ///
115 /// This removes all handlers registered with `route()`.
116 pub async fn unroute_all(&self) {
117 self.route_registry.unroute_all().await;
118 }
119
120 /// Route requests from a HAR file for all pages in this context.
121 ///
122 /// Requests that match entries in the HAR file will be fulfilled with the
123 /// recorded responses. Requests that don't match will continue normally
124 /// unless strict mode is enabled.
125 ///
126 /// # Example
127 ///
128 /// ```no_run
129 /// use viewpoint_core::{Browser, network::HarReplayOptions};
130 ///
131 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
132 /// let browser = Browser::launch().headless(true).launch().await?;
133 /// let context = browser.new_context().await?;
134 ///
135 /// // Simple HAR routing for all pages
136 /// context.route_from_har("recordings/api.har").await?;
137 ///
138 /// // With options
139 /// context.route_from_har_with_options(
140 /// "recordings/api.har",
141 /// HarReplayOptions::new()
142 /// .url("**/api/**")
143 /// .strict(true)
144 /// ).await?;
145 /// # Ok(())
146 /// # }
147 /// ```
148 ///
149 /// # Errors
150 ///
151 /// Returns an error if:
152 /// - The HAR file cannot be read or parsed
153 /// - The context is closed
154 pub async fn route_from_har(
155 &self,
156 path: impl AsRef<std::path::Path>,
157 ) -> Result<(), NetworkError> {
158 self.route_from_har_with_options(path, HarReplayOptions::default())
159 .await
160 }
161
162 /// Route requests from a HAR file with options for all pages in this context.
163 ///
164 /// # Example
165 ///
166 /// ```no_run
167 /// use viewpoint_core::{Browser, network::HarReplayOptions};
168 ///
169 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
170 /// let browser = Browser::launch().headless(true).launch().await?;
171 /// let context = browser.new_context().await?;
172 ///
173 /// // Strict mode: fail if no match found
174 /// context.route_from_har_with_options(
175 /// "api.har",
176 /// HarReplayOptions::new().strict(true)
177 /// ).await?;
178 /// # Ok(())
179 /// # }
180 /// ```
181 ///
182 /// ```no_run
183 /// use viewpoint_core::{Browser, network::HarReplayOptions};
184 ///
185 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
186 /// let browser = Browser::launch().headless(true).launch().await?;
187 /// let context = browser.new_context().await?;
188 ///
189 /// // URL filter: only match specific URLs
190 /// context.route_from_har_with_options(
191 /// "api.har",
192 /// HarReplayOptions::new().url("**/api/**")
193 /// ).await?;
194 /// # Ok(())
195 /// # }
196 /// ```
197 ///
198 /// ```no_run
199 /// use viewpoint_core::{Browser, network::HarReplayOptions};
200 ///
201 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
202 /// let browser = Browser::launch().headless(true).launch().await?;
203 /// let context = browser.new_context().await?;
204 ///
205 /// // Simulate original timing
206 /// context.route_from_har_with_options(
207 /// "api.har",
208 /// HarReplayOptions::new().use_original_timing(true)
209 /// ).await?;
210 /// # Ok(())
211 /// # }
212 /// ```
213 ///
214 /// # Errors
215 ///
216 /// Returns an error if:
217 /// - The HAR file cannot be read or parsed
218 /// - The context is closed
219 pub async fn route_from_har_with_options(
220 &self,
221 path: impl AsRef<std::path::Path>,
222 options: HarReplayOptions,
223 ) -> Result<(), NetworkError> {
224 if self.is_closed() {
225 return Err(NetworkError::Aborted);
226 }
227
228 let handler = Arc::new(
229 crate::network::HarReplayHandler::from_file(path)
230 .await?
231 .with_options(options),
232 );
233
234 // Route all requests through the HAR handler
235 let route_handler = crate::network::har_replay::create_har_route_handler(handler);
236 self.route_registry.route("**/*", route_handler).await
237 }
238}