turbopack_bench/util/
page_guard.rs

1use std::{sync::Arc, time::Duration};
2
3use anyhow::{Context, Result, anyhow};
4use chromiumoxide::{
5    Page,
6    cdp::js_protocol::runtime::{EventBindingCalled, EventExceptionThrown},
7    listeners::EventStream,
8};
9use futures::{Stream, StreamExt};
10use tokio::time::timeout;
11
12use crate::{BINDING_NAME, PreparedApp};
13
14const MAX_HYDRATION_TIMEOUT: Duration = Duration::from_secs(120);
15const TEST_APP_HYDRATION_DONE: &str = "Hydration done";
16
17/// Closes a browser page on Drop.
18pub struct PageGuard<'a> {
19    page: Option<Page>,
20    app: Option<PreparedApp<'a>>,
21    events: Box<dyn Stream<Item = Event> + Unpin>,
22}
23
24enum Event {
25    EventBindingCalled(Arc<EventBindingCalled>),
26    EventExceptionThrown(Arc<EventExceptionThrown>),
27}
28
29impl<'a> PageGuard<'a> {
30    /// Creates a new guard for the given page.
31    pub fn new(
32        page: Page,
33        events: EventStream<EventBindingCalled>,
34        errors: EventStream<EventExceptionThrown>,
35        app: PreparedApp<'a>,
36    ) -> Self {
37        Self {
38            page: Some(page),
39            app: Some(app),
40            events: Box::new(futures::stream::select(
41                events.map(Event::EventBindingCalled),
42                errors.map(Event::EventExceptionThrown),
43            )),
44        }
45    }
46
47    /// Returns a reference to the page.
48    pub fn page(&self) -> &Page {
49        // Invariant: page is always Some while the guard is alive.
50        self.page.as_ref().unwrap()
51    }
52
53    /// Closes the page, returns the app.
54    pub async fn close_page(mut self) -> Result<PreparedApp<'a>> {
55        // Invariant: the page is always Some while the guard is alive.
56        self.page.take().unwrap().close().await?;
57        Ok(
58            // Invariant: the app is always Some while the guard is alive.
59            self.app.take().unwrap(),
60        )
61    }
62
63    /// Waits until the binding is called with the given payload.
64    pub async fn wait_for_binding(&mut self, payload: &str) -> Result<()> {
65        while let Some(event) = self.events.next().await {
66            match event {
67                Event::EventBindingCalled(event) => {
68                    if event.name == BINDING_NAME && event.payload == payload {
69                        return Ok(());
70                    }
71                }
72                Event::EventExceptionThrown(event) => {
73                    anyhow::bail!("Exception throw in page: {}", event.exception_details)
74                }
75            }
76        }
77
78        Err(anyhow!("event stream ended before binding was called"))
79    }
80
81    /// Waits until the page and the page JavaScript is hydrated.
82    pub async fn wait_for_hydration(&mut self) -> Result<()> {
83        timeout(
84            MAX_HYDRATION_TIMEOUT,
85            self.wait_for_binding(TEST_APP_HYDRATION_DONE),
86        )
87        .await
88        .context("Timeout happened while waiting for hydration")?
89        .context("Error happened while waiting for hydration")?;
90        Ok(())
91    }
92}
93
94impl Drop for PageGuard<'_> {
95    fn drop(&mut self) {
96        // The page might have been closed already in `close_page`.
97        if let Some(page) = self.page.take() {
98            // This is a way to block on a future in a destructor. It's not ideal, but for
99            // the purposes of this benchmark it's fine.
100            futures::executor::block_on(page.close()).expect("failed to close page");
101        }
102    }
103}