playwright_rs/protocol/browser_context/events.rs
1use super::{BrowserContext, CtxHandlerFuture, ServiceWorkerHandlerFuture};
2use crate::error::Result;
3use crate::protocol::EventValue;
4use crate::protocol::event_registry::{EventRegistry, Handler, bridge};
5use crate::protocol::event_waiter::EventWaiter;
6use crate::protocol::{Download, Frame, Page, Request, ResponseObject};
7use crate::server::channel_owner::ChannelOwner;
8use std::future::Future;
9use std::sync::{Arc, Mutex};
10use tokio::sync::oneshot;
11
12/// Event subscriptions (`on_*`), page-event forwarders, and one-shot waiters (`expect_*`).
13///
14/// A waiter is created before the action that fires its event, then awaited
15/// after it:
16///
17/// ```no_run
18/// # use playwright_rs::Playwright;
19/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
20/// # let pw = Playwright::launch().await?;
21/// # let browser = pw.chromium().launch().await?;
22/// # let context = browser.new_context().await?;
23/// let opened = context.expect_page(None).await?;
24/// let _page = context.new_page().await?;
25/// let new_page = opened.wait().await?;
26///
27/// let closed = context.expect_close(None).await?;
28/// context.close().await?;
29/// closed.wait().await?;
30/// # Ok(())
31/// # }
32/// ```
33impl BrowserContext {
34 /// Adds a listener for the `page` event.
35 ///
36 /// The handler is called whenever a new page is created in this context,
37 /// including popup pages opened through user interactions.
38 ///
39 /// # Arguments
40 ///
41 /// * `handler` - Async function that receives the new `Page`
42 ///
43 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-page>
44 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
45 pub async fn on_page<F, Fut>(&self, handler: F) -> Result<()>
46 where
47 F: Fn(Page) -> Fut + Send + Sync + 'static,
48 Fut: Future<Output = Result<()>> + Send + 'static,
49 {
50 let handler: Handler<Page> = Arc::new(move |page| Box::pin(handler(page)));
51 self.page_events.add_handler(handler);
52 Ok(())
53 }
54
55 /// Subscribe to `reg`'s event if nothing is listening yet.
56 ///
57 /// Same contract as `Page::subscribe_if_idle`: the server only pushes an
58 /// event once asked, so the first handler or `expect_*` turns it on, and
59 /// the name comes from the registry rather than being restated per site.
60 async fn subscribe_if_idle<T>(&self, reg: &EventRegistry<T>) {
61 if reg.is_idle() {
62 _ = self.channel().update_subscription(reg.name(), true).await;
63 }
64 }
65
66 /// Adds a listener for the `download` event: fired when any page in the
67 /// context starts a download. Forwarded from each page's own `download`
68 /// event.
69 ///
70 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-download>
71 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
72 pub async fn on_download<F, Fut>(&self, handler: F) -> Result<()>
73 where
74 F: Fn(Download) -> Fut + Send + Sync + 'static,
75 Fut: Future<Output = Result<()>> + Send + 'static,
76 {
77 add_forwarded(&self.forwarders.download, handler);
78 Ok(())
79 }
80
81 /// Adds a listener for the `frameAttached` event: fired when a frame is
82 /// attached in any page of the context. Forwarded from each page.
83 ///
84 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-frame-attached>
85 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
86 pub async fn on_frame_attached<F, Fut>(&self, handler: F) -> Result<()>
87 where
88 F: Fn(Frame) -> Fut + Send + Sync + 'static,
89 Fut: Future<Output = Result<()>> + Send + 'static,
90 {
91 add_forwarded(&self.forwarders.frame_attached, handler);
92 Ok(())
93 }
94
95 /// Adds a listener for the `frameDetached` event: fired when a frame is
96 /// detached in any page of the context. Forwarded from each page.
97 ///
98 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-frame-detached>
99 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
100 pub async fn on_frame_detached<F, Fut>(&self, handler: F) -> Result<()>
101 where
102 F: Fn(Frame) -> Fut + Send + Sync + 'static,
103 Fut: Future<Output = Result<()>> + Send + 'static,
104 {
105 add_forwarded(&self.forwarders.frame_detached, handler);
106 Ok(())
107 }
108
109 /// Adds a listener for the `frameNavigated` event: fired when a frame
110 /// navigates in any page of the context. Forwarded from each page.
111 ///
112 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-frame-navigated>
113 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
114 pub async fn on_frame_navigated<F, Fut>(&self, handler: F) -> Result<()>
115 where
116 F: Fn(Frame) -> Fut + Send + Sync + 'static,
117 Fut: Future<Output = Result<()>> + Send + 'static,
118 {
119 add_forwarded(&self.forwarders.frame_navigated, handler);
120 Ok(())
121 }
122
123 /// Adds a listener for the `pageLoad` event: fired when any page in the
124 /// context fires its `load` event. The handler receives that `Page`.
125 ///
126 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-page-load>
127 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
128 pub async fn on_page_load<F, Fut>(&self, handler: F) -> Result<()>
129 where
130 F: Fn(Page) -> Fut + Send + Sync + 'static,
131 Fut: Future<Output = Result<()>> + Send + 'static,
132 {
133 add_forwarded(&self.forwarders.page_load, handler);
134 Ok(())
135 }
136
137 /// Adds a listener for the `pageClose` event: fired when any page in the
138 /// context closes. The handler receives that `Page`.
139 ///
140 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-page-close>
141 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
142 pub async fn on_page_close<F, Fut>(&self, handler: F) -> Result<()>
143 where
144 F: Fn(Page) -> Fut + Send + Sync + 'static,
145 Fut: Future<Output = Result<()>> + Send + 'static,
146 {
147 add_forwarded(&self.forwarders.page_close, handler);
148 Ok(())
149 }
150
151 /// Adds a listener for the `close` event.
152 ///
153 /// The handler is called when the browser context is closed.
154 ///
155 /// # Arguments
156 ///
157 /// * `handler` - Async function called with no arguments when the context closes
158 ///
159 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-close>
160 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
161 pub async fn on_close<F, Fut>(&self, handler: F) -> Result<()>
162 where
163 F: Fn() -> Fut + Send + Sync + 'static,
164 Fut: Future<Output = Result<()>> + Send + 'static,
165 {
166 let handler: Handler<()> = Arc::new(move |()| Box::pin(handler()));
167 self.close_events.add_handler(handler);
168 Ok(())
169 }
170
171 /// Adds a listener for the `request` event.
172 ///
173 /// The handler fires whenever a request is issued from any page in the context.
174 /// This is equivalent to subscribing to `on_request` on each individual page,
175 /// but covers all current and future pages of the context.
176 ///
177 /// Context-level handlers fire before page-level handlers.
178 ///
179 /// # Arguments
180 ///
181 /// * `handler` - Async function that receives the `Request`
182 ///
183 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-request>
184 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
185 pub async fn on_request<F, Fut>(&self, handler: F) -> Result<()>
186 where
187 F: Fn(Request) -> Fut + Send + Sync + 'static,
188 Fut: Future<Output = Result<()>> + Send + 'static,
189 {
190 let handler: Handler<Request> = Arc::new(move |request| Box::pin(handler(request)));
191 self.subscribe_if_idle(&self.request).await;
192 self.request.add_handler(handler);
193 Ok(())
194 }
195
196 /// Adds a listener for the `requestFinished` event.
197 ///
198 /// The handler fires after the request has been successfully received by the server
199 /// and a response has been fully downloaded for any page in the context.
200 ///
201 /// Context-level handlers fire before page-level handlers.
202 ///
203 /// # Arguments
204 ///
205 /// * `handler` - Async function that receives the completed `Request`
206 ///
207 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-request-finished>
208 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
209 pub async fn on_request_finished<F, Fut>(&self, handler: F) -> Result<()>
210 where
211 F: Fn(Request) -> Fut + Send + Sync + 'static,
212 Fut: Future<Output = Result<()>> + Send + 'static,
213 {
214 let handler: Handler<Request> = Arc::new(move |request| Box::pin(handler(request)));
215 self.subscribe_if_idle(&self.request_finished).await;
216 self.request_finished.add_handler(handler);
217 Ok(())
218 }
219
220 /// Adds a listener for the `requestFailed` event.
221 ///
222 /// The handler fires when a request from any page in the context fails,
223 /// for example due to a network error or if the server returned an error response.
224 ///
225 /// Context-level handlers fire before page-level handlers.
226 ///
227 /// # Arguments
228 ///
229 /// * `handler` - Async function that receives the failed `Request`
230 ///
231 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-request-failed>
232 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
233 pub async fn on_request_failed<F, Fut>(&self, handler: F) -> Result<()>
234 where
235 F: Fn(Request) -> Fut + Send + Sync + 'static,
236 Fut: Future<Output = Result<()>> + Send + 'static,
237 {
238 let handler: Handler<Request> = Arc::new(move |request| Box::pin(handler(request)));
239 self.subscribe_if_idle(&self.request_failed).await;
240 self.request_failed.add_handler(handler);
241 Ok(())
242 }
243
244 /// Adds a listener for the `response` event.
245 ///
246 /// The handler fires whenever a response is received from any page in the context.
247 ///
248 /// Context-level handlers fire before page-level handlers.
249 ///
250 /// # Arguments
251 ///
252 /// * `handler` - Async function that receives the `ResponseObject`
253 ///
254 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-response>
255 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
256 pub async fn on_response<F, Fut>(&self, handler: F) -> Result<()>
257 where
258 F: Fn(ResponseObject) -> Fut + Send + Sync + 'static,
259 Fut: Future<Output = Result<()>> + Send + 'static,
260 {
261 let handler: Handler<ResponseObject> =
262 Arc::new(move |response| Box::pin(handler(response)));
263 self.subscribe_if_idle(&self.response).await;
264 self.response.add_handler(handler);
265 Ok(())
266 }
267
268 /// Adds a listener for the `dialog` event on this browser context.
269 ///
270 /// The handler fires whenever a JavaScript dialog (alert, confirm, prompt,
271 /// or beforeunload) is triggered from **any** page in the context. Context-level
272 /// handlers fire before page-level handlers.
273 ///
274 /// The dialog must be explicitly accepted or dismissed; otherwise the page
275 /// will freeze waiting for a response.
276 ///
277 /// # Arguments
278 ///
279 /// * `handler` - Async function that receives the [`Dialog`](crate::protocol::Dialog) and calls
280 /// `dialog.accept()` or `dialog.dismiss()`.
281 ///
282 /// # Errors
283 ///
284 /// Returns error if communication with the browser process fails.
285 ///
286 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-dialog>
287 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
288 pub async fn on_dialog<F, Fut>(&self, handler: F) -> Result<()>
289 where
290 F: Fn(crate::protocol::Dialog) -> Fut + Send + Sync + 'static,
291 Fut: Future<Output = Result<()>> + Send + 'static,
292 {
293 let handler: Handler<crate::protocol::Dialog> =
294 Arc::new(move |dialog| Box::pin(handler(dialog)));
295 self.dialog.add_handler(handler);
296 Ok(())
297 }
298
299 /// Adds a listener for the `dialogclosed` event, which fires once a
300 /// dialog has been accepted, dismissed, or closed by the user, on any
301 /// page in the context.
302 ///
303 /// Context-level handlers fire before page-level ones.
304 ///
305 /// # Errors
306 ///
307 /// Returns an error if the handler cannot be registered.
308 ///
309 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-dialog-closed>
310 pub async fn on_dialog_closed<F, Fut>(&self, handler: F) -> Result<()>
311 where
312 F: Fn(crate::protocol::Dialog) -> Fut + Send + Sync + 'static,
313 Fut: Future<Output = Result<()>> + Send + 'static,
314 {
315 let handler: Handler<crate::protocol::Dialog> =
316 Arc::new(move |dialog| Box::pin(handler(dialog)));
317 self.subscribe_if_idle(&self.dialog_closed).await;
318 self.dialog_closed.add_handler(handler);
319 Ok(())
320 }
321
322 /// Subscribe to `dialogClosed` if nothing has yet, so a page-level
323 /// handler receives the event even when the context has none of its own.
324 pub(crate) async fn ensure_dialog_closed_subscription(&self) {
325 self.subscribe_if_idle(&self.dialog_closed).await;
326 }
327
328 /// Registers a context-level console event handler.
329 ///
330 /// The handler fires for any console message emitted by any page in this context.
331 /// Context-level handlers fire before page-level handlers.
332 ///
333 /// The server only sends console events after the first handler is registered
334 /// (subscription is managed automatically per context channel).
335 ///
336 /// # Arguments
337 ///
338 /// * `handler` - Async closure that receives the [`ConsoleMessage`](crate::protocol::ConsoleMessage)
339 ///
340 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-console>
341 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
342 pub async fn on_console<F, Fut>(&self, handler: F) -> Result<()>
343 where
344 F: Fn(crate::protocol::ConsoleMessage) -> Fut + Send + Sync + 'static,
345 Fut: Future<Output = Result<()>> + Send + 'static,
346 {
347 let handler: Handler<crate::protocol::ConsoleMessage> =
348 Arc::new(move |msg| Box::pin(handler(msg)));
349
350 self.subscribe_if_idle(&self.console).await;
351 self.console.add_handler(handler);
352
353 Ok(())
354 }
355
356 /// Registers a context-level handler for uncaught JavaScript exceptions.
357 ///
358 /// The handler fires whenever a page in this context throws an unhandled
359 /// JavaScript error (i.e. an exception that propagates to `window.onerror`
360 /// or an unhandled promise rejection). The [`WebError`](crate::protocol::WebError)
361 /// passed to the handler contains the error message and an optional back-reference
362 /// to the originating page.
363 ///
364 /// # Arguments
365 ///
366 /// * `handler` - Async closure that receives a [`WebError`](crate::protocol::WebError).
367 ///
368 /// # Errors
369 ///
370 /// Returns error if communication with the browser process fails.
371 ///
372 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-web-error>
373 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
374 pub async fn on_weberror<F, Fut>(&self, handler: F) -> Result<()>
375 where
376 F: Fn(crate::protocol::WebError) -> Fut + Send + Sync + 'static,
377 Fut: Future<Output = Result<()>> + Send + 'static,
378 {
379 let handler: Handler<crate::protocol::WebError> =
380 Arc::new(move |web_error| Box::pin(handler(web_error)));
381 self.weberror.add_handler(handler);
382 Ok(())
383 }
384
385 /// Registers a handler for the `serviceWorker` event.
386 ///
387 /// The handler is called when a new service worker is registered in the browser context.
388 ///
389 /// Note: Service worker testing typically requires HTTPS and a registered service worker.
390 ///
391 /// # Arguments
392 ///
393 /// * `handler` - Async closure called with the new [`Worker`](crate::protocol::Worker) object
394 ///
395 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-service-worker>
396 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
397 pub async fn on_serviceworker<F, Fut>(&self, handler: F) -> Result<()>
398 where
399 F: Fn(crate::protocol::Worker) -> Fut + Send + Sync + 'static,
400 Fut: Future<Output = Result<()>> + Send + 'static,
401 {
402 let handler = Arc::new(
403 move |worker: crate::protocol::Worker| -> ServiceWorkerHandlerFuture {
404 Box::pin(handler(worker))
405 },
406 );
407 self.serviceworker_handlers.lock().unwrap().push(handler);
408 Ok(())
409 }
410
411 /// Waits for a new page to be created in this browser context.
412 ///
413 /// Creates a one-shot waiter that resolves when the next `page` event fires.
414 /// The waiter **must** be created before the action that triggers the new page
415 /// (e.g. `new_page()` or a user action that opens a popup) to avoid a race
416 /// condition.
417 ///
418 /// # Arguments
419 ///
420 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
421 ///
422 /// # Errors
423 ///
424 /// Returns [`crate::error::Error::Timeout`] if no page is created within the timeout.
425 ///
426 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-wait-for-event>
427 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
428 pub async fn expect_page(&self, timeout: Option<f64>) -> Result<EventWaiter<Page>> {
429 let rx = self.page_events.wait();
430 Ok(EventWaiter::new(rx, timeout.or(Some(30_000.0))))
431 }
432
433 /// Waits for this browser context to be closed.
434 ///
435 /// Creates a one-shot waiter that resolves when the `close` event fires.
436 /// The waiter **must** be created before the action that closes the context
437 /// to avoid a race condition.
438 ///
439 /// # Arguments
440 ///
441 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
442 ///
443 /// # Errors
444 ///
445 /// Returns [`crate::error::Error::Timeout`] if the context is not closed within the timeout.
446 ///
447 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-wait-for-event>
448 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
449 pub async fn expect_close(&self, timeout: Option<f64>) -> Result<EventWaiter<()>> {
450 let rx = self.close_events.wait();
451 Ok(EventWaiter::new(rx, timeout.or(Some(30_000.0))))
452 }
453
454 /// Waits for a console message from any page in this context.
455 ///
456 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-console>
457 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
458 pub async fn expect_console_message(
459 &self,
460 timeout: Option<f64>,
461 ) -> Result<EventWaiter<crate::protocol::ConsoleMessage>> {
462 self.subscribe_if_idle(&self.console).await;
463 let rx = self.console.wait();
464 Ok(EventWaiter::new(rx, timeout.or(Some(30_000.0))))
465 }
466
467 /// Waits for the given event to fire and returns a typed `EventValue`.
468 ///
469 /// This is the generic version of the specific `expect_*` methods. It matches
470 /// the playwright-python / playwright-js `context.expect_event(event_name)` API.
471 ///
472 /// The waiter **must** be created before the action that triggers the event.
473 ///
474 /// # Supported event names
475 ///
476 /// `"page"`, `"close"`, `"console"`, `"request"`, `"response"`,
477 /// `"weberror"`, `"serviceworker"`
478 ///
479 /// # Arguments
480 ///
481 /// * `event` - Event name (case-sensitive, matches Playwright protocol names).
482 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
483 ///
484 /// # Errors
485 ///
486 /// Returns [`crate::error::Error::InvalidArgument`] for unknown event names.
487 /// Returns [`crate::error::Error::Timeout`] if the event does not fire within the timeout.
488 ///
489 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-wait-for-event>
490 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
491 pub async fn expect_event(
492 &self,
493 event: &str,
494 timeout: Option<f64>,
495 ) -> crate::error::Result<EventWaiter<crate::protocol::EventValue>> {
496 let timeout_ms = timeout.or(Some(30_000.0));
497
498 let waiter = |rx| EventWaiter::new(rx, timeout_ms);
499 match event {
500 "page" => Ok(waiter(bridge(self.page_events.wait(), EventValue::Page))),
501 "close" => Ok(waiter(bridge(self.close_events.wait(), |()| {
502 EventValue::Close
503 }))),
504 "console" => {
505 self.subscribe_if_idle(&self.console).await;
506 Ok(waiter(bridge(
507 self.console.wait(),
508 EventValue::ConsoleMessage,
509 )))
510 }
511 "request" => {
512 self.subscribe_if_idle(&self.request).await;
513 Ok(waiter(bridge(self.request.wait(), EventValue::Request)))
514 }
515 "response" => {
516 self.subscribe_if_idle(&self.response).await;
517 Ok(waiter(bridge(self.response.wait(), EventValue::Response)))
518 }
519 "weberror" => Ok(waiter(bridge(self.weberror.wait(), EventValue::WebError))),
520 "serviceworker" => {
521 let (inner_tx, inner_rx) = oneshot::channel();
522 self.serviceworker_waiters.lock().unwrap().push(inner_tx);
523 Ok(waiter(bridge(inner_rx, EventValue::Worker)))
524 }
525 other => Err(crate::error::Error::InvalidArgument(format!(
526 "Unknown event name '{}'. Supported: page, close, console, request, response, \
527 weberror, serviceworker",
528 other
529 ))),
530 }
531 }
532}
533
534/// A context-level handler for an event forwarded from each page.
535type ForwardedHandler<T> = Arc<dyn Fn(T) -> CtxHandlerFuture + Send + Sync>;
536/// The handlers registered for one forwarded event.
537type Forwarded<T> = Arc<Mutex<Vec<ForwardedHandler<T>>>>;
538
539/// Context-level handlers for the events forwarded from each page.
540///
541/// These are not wire events on the context channel. Each page's own event
542/// is forwarded to the handlers here, matching how the upstream clients
543/// synthesize them. Every page is wired once, when the context learns of
544/// it, so registering a handler never touches a page and there is no
545/// check-then-wire window for two registrations or a page arriving mid-way
546/// to fall into.
547#[derive(Clone, Default)]
548pub(super) struct Forwarders {
549 pub(super) download: Forwarded<Download>,
550 pub(super) frame_attached: Forwarded<Frame>,
551 pub(super) frame_detached: Forwarded<Frame>,
552 pub(super) frame_navigated: Forwarded<Frame>,
553 pub(super) page_load: Forwarded<Page>,
554 pub(super) page_close: Forwarded<Page>,
555}
556
557impl Forwarders {
558 /// Forwards each of `page`'s lifecycle events to these handlers.
559 pub(super) async fn wire(&self, page: &Page) {
560 _ = page.on_download(forward_to(&self.download)).await;
561 _ = page
562 .on_frameattached(forward_to(&self.frame_attached))
563 .await;
564 _ = page
565 .on_framedetached(forward_to(&self.frame_detached))
566 .await;
567 _ = page
568 .on_framenavigated(forward_to(&self.frame_navigated))
569 .await;
570 let (load, p) = (forward_to(&self.page_load), page.clone());
571 _ = page.on_load(move || load(p.clone())).await;
572 let (close, p) = (forward_to(&self.page_close), page.clone());
573 _ = page.on_close(move || close(p.clone())).await;
574 }
575}
576
577/// Registers `handler` for a forwarded event.
578fn add_forwarded<T, F, Fut>(handlers: &Forwarded<T>, handler: F)
579where
580 F: Fn(T) -> Fut + Send + Sync + 'static,
581 Fut: Future<Output = Result<()>> + Send + 'static,
582{
583 handlers
584 .lock()
585 .unwrap()
586 .push(Arc::new(move |value| Box::pin(handler(value))));
587}
588
589/// A page-level handler that runs every handler in `handlers` with the
590/// event's value.
591fn forward_to<T: Clone + Send + 'static>(
592 handlers: &Forwarded<T>,
593) -> impl Fn(T) -> CtxHandlerFuture + Send + Sync + 'static {
594 let handlers = handlers.clone();
595 move |value| {
596 let handlers = handlers.clone();
597 Box::pin(async move {
598 run_all(&handlers, value).await;
599 Ok(())
600 })
601 }
602}
603
604/// Runs the handlers registered at call time, in registration order. An
605/// error is logged so one failing handler does not stop the rest.
606async fn run_all<T: Clone>(handlers: &Forwarded<T>, value: T) {
607 let handlers = handlers.lock().unwrap().clone();
608 for handler in handlers {
609 if let Err(e) = handler(value.clone()).await {
610 tracing::warn!("context handler error: {e}");
611 }
612 }
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618 use std::sync::atomic::{AtomicUsize, Ordering};
619
620 #[tokio::test]
621 async fn run_all_runs_every_handler_in_order_past_a_failure() {
622 let handlers: Forwarded<u8> = Forwarded::default();
623 let seen = Arc::new(Mutex::new(Vec::new()));
624 let calls = Arc::new(AtomicUsize::new(0));
625 for i in 0..3u8 {
626 let (seen, calls) = (seen.clone(), calls.clone());
627 add_forwarded(&handlers, move |v: u8| {
628 let (seen, calls) = (seen.clone(), calls.clone());
629 async move {
630 seen.lock().unwrap().push((i, v));
631 calls.fetch_add(1, Ordering::SeqCst);
632 if i == 1 {
633 Err(crate::error::Error::ProtocolError("boom".into()))
634 } else {
635 Ok(())
636 }
637 }
638 });
639 }
640 run_all(&handlers, 9).await;
641 assert_eq!(*seen.lock().unwrap(), vec![(0, 9), (1, 9), (2, 9)]);
642 assert_eq!(calls.load(Ordering::SeqCst), 3);
643 }
644
645 #[tokio::test]
646 async fn forward_to_reaches_handlers_added_after_wiring() {
647 let handlers: Forwarded<u8> = Forwarded::default();
648 let page_side = forward_to(&handlers);
649 let seen = Arc::new(Mutex::new(Vec::new()));
650 let s = seen.clone();
651 add_forwarded(&handlers, move |v: u8| {
652 let s = s.clone();
653 async move {
654 s.lock().unwrap().push(v);
655 Ok(())
656 }
657 });
658 page_side(4).await.unwrap();
659 assert_eq!(*seen.lock().unwrap(), vec![4]);
660 }
661}