Skip to main content

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}