Skip to main content

dioxus_native_dom/
dioxus_document.rs

1//! Integration between Dioxus and Blitz
2use crate::NodeId;
3use crate::events::{
4    BlitzKeyboardData, NativeConverter, NativeFocusData, NativeFormData, NativePointerData,
5    NativeScrollData, NativeTouchData, NativeWheelData, NodeHandle,
6};
7use crate::mutation_writer::DioxusState;
8use crate::qual_name;
9use blitz_dom::{
10    Attribute, BaseDocument, DEFAULT_CSS, DocGuard, DocGuardMut, Document, DocumentConfig,
11    EventDriver, EventHandler, Node,
12};
13use blitz_traits::events::{DomEvent, DomEventData, EventState, UiEvent};
14use dioxus_core::{ElementId, Event, VirtualDom};
15use dioxus_html::{PlatformEventData, set_event_converter};
16use futures_util::task::noop_waker;
17use std::cell::RefCell;
18use std::future::Future;
19use std::mem;
20use std::pin::pin;
21use std::sync::LazyLock;
22use std::task::{Context as TaskContext, Waker};
23use std::{any::Any, rc::Rc};
24
25fn wrap_event_data<T: Any>(value: T) -> Rc<dyn Any> {
26    Rc::new(PlatformEventData::new(Box::new(value)))
27}
28
29/// Get the value of the "dioxus-data-id" attribute parsed aa usize
30fn get_dioxus_id(node: &Node) -> Option<ElementId> {
31    node.element_data()?
32        .attrs
33        .iter()
34        .find(|attr| *attr.name.local == *"data-dioxus-id")
35        .and_then(|attr| attr.value.parse::<usize>().ok())
36        .map(ElementId::from_raw)
37}
38
39/// Integrates [`BaseDocument`] from  [`blitz-dom`](blitz_dom)  with [`VirtualDom`] from [`dioxus-core`](dioxus_core)
40///
41/// ### Example
42///
43/// ```rust
44/// use blitz_traits::shell::{Viewport, ColorScheme};
45/// use dioxus_native_dom::{DioxusDocument, DocumentConfig};
46/// use dioxus::prelude::*;
47///
48/// // Example Dioxus app
49/// fn app() -> Element {
50///     rsx! {
51///         div { "Hello, world!" }
52///     }
53/// }
54///
55/// fn main() {
56///    let vdom = VirtualDom::new(app);
57///    let mut doc = DioxusDocument::new(vdom, DocumentConfig {
58///         viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
59///         ..Default::default()
60///    });
61///    doc.initial_build();
62/// }
63/// ```
64///
65/// You can just push events into the [`DioxusDocument`] with [`doc.handle_ui_event(..)`](Self::handle_ui_event)
66/// and then flush the changes with [`doc.poll(..)`](Self::poll)
67pub struct DioxusDocument {
68    pub inner: Rc<RefCell<BaseDocument>>,
69    pub vdom: VirtualDom,
70    pub vdom_state: DioxusState,
71
72    #[allow(unused)]
73    pub(crate) html_element_id: NodeId,
74    #[allow(unused)]
75    pub(crate) head_element_id: NodeId,
76    #[allow(unused)]
77    pub(crate) body_element_id: NodeId,
78    #[allow(unused)]
79    pub(crate) main_element_id: NodeId,
80}
81
82impl DioxusDocument {
83    /// Create a new [`DioxusDocument`] from a [`VirtualDom`].
84    pub fn new(vdom: VirtualDom, mut config: DocumentConfig) -> Self {
85        // Only really needs to happen once
86        set_event_converter(Box::new(NativeConverter {}));
87
88        config.base_url = Some(
89            config
90                .base_url
91                .unwrap_or_else(|| String::from("dioxus://index.html")),
92        );
93        let mut doc = BaseDocument::new(config);
94
95        // Include default stylesheet
96        doc.add_user_agent_stylesheet(DEFAULT_CSS);
97
98        // Create some minimal HTML to render the app into.
99        // HTML is equivalent to:
100        //
101        // <html>
102        // <head></head>
103        // <body>
104        //    <div id="main"></div>
105        // </body>
106        // </html>
107        //
108        // TODO: Support arbitrary "index.html" templates
109
110        // Create the html element
111        let mut mutr = doc.mutate();
112        let html_element_id = mutr.create_element(qual_name("html", None), vec![]);
113        mutr.append_children(mutr.doc.root_node().id, &[html_element_id]);
114
115        // Create the body element
116        let head_element_id = mutr.create_element(qual_name("head", None), vec![]);
117        mutr.append_children(html_element_id, &[head_element_id]);
118
119        // Create the body element
120        let body_element_id = mutr.create_element(qual_name("body", None), vec![]);
121        mutr.append_children(html_element_id, &[body_element_id]);
122
123        // Create another virtual element to hold the root <div id="main"></div> under the html element
124        let main_attr = blitz_dom::Attribute {
125            name: qual_name("id", None),
126            value: "main".to_string(),
127        };
128        let main_element_id = mutr.create_element(qual_name("main", None), vec![main_attr]);
129        mutr.append_children(body_element_id, &[main_element_id]);
130
131        drop(mutr);
132
133        let vdom_state = DioxusState::create(main_element_id);
134        Self {
135            vdom,
136            vdom_state,
137            inner: Rc::new(RefCell::new(doc)),
138            html_element_id,
139            head_element_id,
140            body_element_id,
141            main_element_id,
142        }
143    }
144
145    /// Run an initial build of the Dioxus vdom
146    pub fn initial_build(&mut self) {
147        let mut inner = self.inner.borrow_mut();
148        let mut writer = self.vdom_state.writer(&mut inner);
149        self.vdom.rebuild(&mut writer);
150        drop(writer);
151        drop(inner);
152        self.flush_queued_mounted_events();
153    }
154
155    /// Used to respond to a `CreateHeadElement` event generated by Dioxus. These
156    /// events allow Dioxus to create elements in the `<head>` of the document.
157    #[doc(hidden)]
158    pub fn create_head_element(
159        &mut self,
160        name: &str,
161        attributes: &[(String, String)],
162        contents: &Option<String>,
163    ) {
164        let mut inner = self.inner.borrow_mut();
165        let mut mutr = inner.mutate();
166
167        let attributes = attributes
168            .iter()
169            .map(|(name, value)| Attribute {
170                name: qual_name(name, None),
171                value: value.clone(),
172            })
173            .collect();
174
175        let new_elem_id = mutr.create_element(qual_name(name, None), attributes);
176        mutr.append_children(self.head_element_id, &[new_elem_id]);
177        if let Some(contents) = contents {
178            let text_node_id = mutr.create_text_node(contents);
179            mutr.append_children(new_elem_id, &[text_node_id]);
180        }
181    }
182
183    pub(crate) fn flush_queued_mounted_events(&mut self) {
184        let mut queued_mounted_events = mem::take(&mut self.vdom_state.queued_mounted_events);
185        for element_id in queued_mounted_events.drain(..) {
186            let node_id = self.vdom_state.element_to_node_id(element_id);
187
188            if self.inner.borrow().get_node(node_id).is_some() {
189                let event = Event::new(
190                    Rc::new(PlatformEventData::new(Box::new(NodeHandle {
191                        doc: Rc::clone(&self.inner),
192                        node_id,
193                    }))) as Rc<dyn Any>,
194                    false,
195                );
196                self.vdom
197                    .runtime()
198                    .handle_event("mounted", event, element_id);
199            }
200        }
201
202        self.vdom_state.queued_mounted_events = queued_mounted_events;
203    }
204}
205
206// Implement DocumentLike and required traits for DioxusDocument
207impl Document for DioxusDocument {
208    fn id(&self) -> usize {
209        self.inner.borrow().id()
210    }
211
212    fn inner(&self) -> DocGuard<'_> {
213        DocGuard::RefCell(self.inner.borrow())
214    }
215
216    fn inner_mut(&mut self) -> DocGuardMut<'_> {
217        DocGuardMut::RefCell(self.inner.borrow_mut())
218    }
219
220    fn poll(&mut self, cx: Option<TaskContext>) -> bool {
221        static NOOP_WAKER: LazyLock<Waker> = LazyLock::new(noop_waker);
222        let waker = cx
223            .as_ref()
224            .map(|cx| cx.waker().clone())
225            .unwrap_or_else(|| NOOP_WAKER.clone());
226
227        // Poll any sub-documents, which may have pending async operations of
228        // their own (e.g. JavaScript timers)
229        let subdoc_changes = self.inner.borrow_mut().poll_subdocuments(Some(&waker));
230
231        {
232            let fut = self.vdom.wait_for_work();
233            let mut pinned_fut = pin!(fut);
234
235            let mut cx = TaskContext::from_waker(&waker);
236            match pinned_fut.as_mut().poll(&mut cx) {
237                std::task::Poll::Ready(_) => {}
238                std::task::Poll::Pending => return subdoc_changes,
239            }
240        }
241
242        let mut inner = self.inner.borrow_mut();
243        let mut writer = self.vdom_state.writer(&mut inner);
244        self.vdom.render_immediate(&mut writer);
245        drop(writer);
246        drop(inner);
247        self.flush_queued_mounted_events();
248
249        true
250    }
251
252    fn handle_ui_event(&mut self, event: UiEvent) {
253        let handler = DioxusEventHandler {
254            vdom: &mut self.vdom,
255            vdom_state: &mut self.vdom_state,
256        };
257        let mut driver = EventDriver::new(&mut self.inner, handler);
258        driver.handle_ui_event(event);
259    }
260}
261
262pub struct DioxusEventHandler<'v> {
263    vdom: &'v mut VirtualDom,
264    vdom_state: &'v mut DioxusState,
265}
266
267impl EventHandler for DioxusEventHandler<'_> {
268    fn handle_event(
269        &mut self,
270        chain: &[NodeId],
271        event: &mut DomEvent,
272        doc: &mut dyn Document,
273        event_state: &mut EventState,
274    ) {
275        // As an optimisation we maintain a count of the total number event handlers of a given type
276        // If this count is zero then we can skip handling that kind of event entirely.
277        let event_kind_idx = event.data.discriminant() as usize;
278        let event_kind_count = self.vdom_state.event_handler_counts[event_kind_idx];
279        if event_kind_count == 0 {
280            return;
281        }
282
283        let event_data = match &event.data {
284            DomEventData::PointerMove(mevent)
285            | DomEventData::PointerDown(mevent)
286            | DomEventData::PointerUp(mevent)
287            | DomEventData::PointerCancel(mevent)
288            | DomEventData::PointerLeave(mevent)
289            | DomEventData::PointerEnter(mevent)
290            | DomEventData::PointerOver(mevent)
291            | DomEventData::PointerOut(mevent)
292            | DomEventData::MouseMove(mevent)
293            | DomEventData::MouseDown(mevent)
294            | DomEventData::MouseUp(mevent)
295            | DomEventData::MouseLeave(mevent)
296            | DomEventData::MouseEnter(mevent)
297            | DomEventData::MouseOver(mevent)
298            | DomEventData::MouseOut(mevent)
299            | DomEventData::Click(mevent)
300            | DomEventData::ContextMenu(mevent)
301            | DomEventData::DoubleClick(mevent) => {
302                Some(wrap_event_data(NativePointerData(mevent.clone())))
303            }
304
305            DomEventData::TouchStart(tevent)
306            | DomEventData::TouchMove(tevent)
307            | DomEventData::TouchEnd(tevent)
308            | DomEventData::TouchCancel(tevent) => {
309                Some(wrap_event_data(NativeTouchData(tevent.clone())))
310            }
311
312            DomEventData::Scroll(sevent) => Some(wrap_event_data(NativeScrollData(sevent.clone()))),
313            DomEventData::Wheel(wevent) => Some(wrap_event_data(NativeWheelData(wevent.clone()))),
314
315            DomEventData::Focus(_) => Some(wrap_event_data(NativeFocusData)),
316            DomEventData::Blur(_) => Some(wrap_event_data(NativeFocusData)),
317            DomEventData::FocusIn(_) => Some(wrap_event_data(NativeFocusData)),
318            DomEventData::FocusOut(_) => Some(wrap_event_data(NativeFocusData)),
319
320            DomEventData::KeyDown(kevent)
321            | DomEventData::KeyUp(kevent)
322            | DomEventData::KeyPress(kevent) => {
323                Some(wrap_event_data(BlitzKeyboardData(kevent.clone())))
324            }
325
326            DomEventData::Input(data) => Some(wrap_event_data(NativeFormData {
327                value: data.value.clone(),
328                values: vec![],
329            })),
330
331            // TODO: Implement IME handling
332            DomEventData::Ime(_) => None,
333
334            // AppleStandardKeybinding events are not exposed to script
335            DomEventData::AppleStandardKeybinding(_) => None,
336        };
337
338        let Some(event_data) = event_data else {
339            return;
340        };
341
342        // Get dioxus vdom id for node
343        let dioxus_id = chain
344            .iter()
345            .find_map(|node_id| doc.inner().get_node(*node_id).and_then(get_dioxus_id));
346        let Some(id) = dioxus_id else {
347            return;
348        };
349
350        // Handle event in vdom
351        let dx_event = Event::new(event_data.clone(), event.bubbles);
352        self.vdom
353            .runtime()
354            .handle_event(event.name(), dx_event.clone(), id);
355
356        // Update event state
357        if !dx_event.default_action_enabled() {
358            event_state.prevent_default();
359        }
360        if !dx_event.propagates() {
361            event_state.stop_propagation();
362        }
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369    use blitz_dom::DocumentConfig;
370    use dioxus::prelude::*;
371    use dioxus_core::ScopeId;
372    use std::cell::{Cell, RefCell};
373    use std::collections::HashMap;
374
375    #[test]
376    // Regression test for a panic. The keyed `div`s are re-ordered as the (unordered) `HashMap` grows,
377    // which previously caused a crash when moving keyed nodes within their parent.
378    fn keyed_nodes_do_not_crash() {
379        type SharedData = Rc<RefCell<HashMap<usize, usize>>>;
380        let data: SharedData = Rc::new(RefCell::new(HashMap::new()));
381
382        fn app(data: SharedData) -> Element {
383            let entries: Vec<usize> = data.borrow().keys().copied().collect();
384            rsx!(
385                for id in entries {
386                    div {
387                        key: "item_{id}",
388                        "{id}"
389                    }
390                }
391            )
392        }
393
394        let vdom = VirtualDom::new_with_props(app, Rc::clone(&data));
395        let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
396        doc.initial_build();
397
398        // Mirror `examples/crash.rs`: incrementally insert 100 items, flushing
399        // the resulting mutations after each insert.
400        for i in 0..100 {
401            data.borrow_mut().insert(i, i);
402            doc.vdom.mark_dirty(ScopeId::APP);
403            doc.poll(None);
404        }
405
406        // The `<main>` element should end up with exactly one keyed `div` per
407        // inserted item, and applying the mutations must not have panicked.
408        let inner = doc.inner.borrow();
409        let main = inner.get_node(doc.main_element_id).unwrap();
410        assert_eq!(main.children.len(), 100);
411    }
412
413    fn node_count(doc: &DioxusDocument) -> usize {
414        doc.inner.borrow().tree().len()
415    }
416
417    fn rerender(doc: &mut DioxusDocument) {
418        doc.vdom.mark_dirty(ScopeId::APP);
419        poll_until_idle(doc);
420    }
421
422    fn poll_until_idle(doc: &mut DioxusDocument) {
423        for _ in 0..8 {
424            if !doc.poll(None) {
425                return;
426            }
427        }
428        panic!("document did not become idle");
429    }
430
431    fn main_text(doc: &DioxusDocument) -> String {
432        let inner = doc.inner.borrow();
433        let mut text = String::new();
434        let mut stack = vec![doc.main_element_id];
435        while let Some(id) = stack.pop() {
436            let node = inner.get_node(id).unwrap();
437            if let Some(data) = node.text_data() {
438                text.push_str(&data.content);
439            }
440            stack.extend(node.children.iter().rev().copied());
441        }
442        text
443    }
444
445    #[test]
446    fn removed_nodes_are_dropped() {
447        fn app() -> Element {
448            let item_count = consume_context::<Rc<Cell<usize>>>();
449            rsx!(
450                for id in 0..item_count.get() {
451                    div { key: "{id}", span { "{id}" } }
452                }
453            )
454        }
455
456        let item_count = Rc::new(Cell::new(0usize));
457        let vdom = VirtualDom::new(app).with_root_context(Rc::clone(&item_count));
458        let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
459        doc.initial_build();
460
461        let cycle = |doc: &mut DioxusDocument| {
462            item_count.set(50);
463            rerender(doc);
464            let main_id = doc.main_element_id;
465            assert_eq!(
466                doc.inner.borrow().get_node(main_id).unwrap().children.len(),
467                50
468            );
469            item_count.set(0);
470            rerender(doc);
471            node_count(doc)
472        };
473
474        // The first cycle creates the (retained) template prototype nodes.
475        let baseline = cycle(&mut doc);
476        for _ in 0..5 {
477            assert_eq!(cycle(&mut doc), baseline);
478        }
479    }
480
481    #[test]
482    fn suspended_nodes_are_dropped_and_recreated() {
483        #[derive(Default)]
484        struct State {
485            suspended: Cell<bool>,
486            generation: Cell<usize>,
487            waker: Cell<Option<std::task::Waker>>,
488        }
489
490        // `generation` only exists to defeat memoization so that the component re-renders
491        #[component]
492        fn Child(generation: usize) -> Element {
493            let state = consume_context::<Rc<State>>();
494            let mut resolved = use_signal(|| false);
495            if state.suspended.get() {
496                resolved.set(false);
497                let task = spawn(async move {
498                    std::future::poll_fn(|cx| {
499                        if state.suspended.get() {
500                            state.waker.set(Some(cx.waker().clone()));
501                            std::task::Poll::Pending
502                        } else {
503                            std::task::Poll::Ready(())
504                        }
505                    })
506                    .await;
507                    resolved.set(true);
508                });
509                suspend(task)?;
510            }
511            rsx!( div { span { "content" } } )
512        }
513
514        fn app() -> Element {
515            let state = consume_context::<Rc<State>>();
516            rsx!(
517                SuspenseBoundary {
518                    fallback: |_| rsx!( p { "loading" } ),
519                    Child { generation: state.generation.get() }
520                }
521            )
522        }
523
524        let state = Rc::new(State::default());
525        let vdom = VirtualDom::new(app).with_root_context(Rc::clone(&state));
526        let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
527        doc.initial_build();
528        assert_eq!(main_text(&doc), "content");
529
530        let cycle = |doc: &mut DioxusDocument| {
531            state.suspended.set(true);
532            state.generation.set(state.generation.get() + 1);
533            rerender(doc);
534            assert_eq!(main_text(doc), "loading");
535            state.suspended.set(false);
536            state.waker.take().unwrap().wake();
537            poll_until_idle(doc);
538            assert_eq!(main_text(doc), "content");
539            node_count(doc)
540        };
541
542        let baseline = cycle(&mut doc);
543        for _ in 0..5 {
544            assert_eq!(cycle(&mut doc), baseline);
545        }
546    }
547
548    #[test]
549    fn todo_like_stress() {
550        use std::cell::RefCell;
551        #[derive(Clone, PartialEq)]
552        struct Item {
553            id: u32,
554            checked: bool,
555            editing: bool,
556        }
557        #[derive(Default)]
558        struct State {
559            items: RefCell<Vec<Item>>,
560            filter: Cell<u8>,
561        }
562
563        #[component]
564        fn Entry(item: Item) -> Element {
565            rsx! {
566                li { class: if item.checked { "completed" },
567                    div { class: "view",
568                        input { r#type: "checkbox", id: "cbg-{item.id}" }
569                        label { "todo {item.id}" }
570                        button { class: "destroy" }
571                    }
572                    if item.editing {
573                        input { class: "edit", value: "todo {item.id}" }
574                    }
575                }
576            }
577        }
578
579        fn app() -> Element {
580            let state = consume_context::<Rc<State>>();
581            let items = state.items.borrow().clone();
582            let filter = state.filter.get();
583            let active = items.iter().filter(|i| !i.checked).count();
584            let visible: Vec<Item> = items
585                .iter()
586                .filter(|i| match filter {
587                    1 => !i.checked,
588                    2 => i.checked,
589                    _ => true,
590                })
591                .cloned()
592                .collect();
593            rsx! {
594                section {
595                    if !items.is_empty() {
596                        input { id: "toggle-all", r#type: "checkbox" }
597                        label { r#for: "toggle-all" }
598                    }
599                    ul {
600                        for item in visible {
601                            Entry { key: "{item.id}", item }
602                        }
603                    }
604                    if !items.is_empty() {
605                        footer {
606                            span { strong { "{active} " } span { match active { 1 => "item", _ => "items" } " left" } }
607                            if items.iter().any(|i| i.checked) {
608                                button { "Clear completed" }
609                            }
610                        }
611                    }
612                }
613            }
614        }
615
616        let state = Rc::new(State::default());
617        let vdom = VirtualDom::new(app).with_root_context(Rc::clone(&state));
618        let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
619        doc.initial_build();
620
621        let mut seed = 0x2545F4914F6CDD1Du64;
622        let mut rng = move || {
623            seed ^= seed << 13;
624            seed ^= seed >> 7;
625            seed ^= seed << 17;
626            seed
627        };
628        let mut next_id = 0;
629        let mut counts = Vec::new();
630        for step in 1..=3000 {
631            let r = rng();
632            {
633                let mut items = state.items.borrow_mut();
634                let len = items.len();
635                match r % 8 {
636                    0 | 1 => {
637                        items.push(Item {
638                            id: next_id,
639                            checked: false,
640                            editing: false,
641                        });
642                        next_id += 1;
643                    }
644                    2 if len > 0 => {
645                        items.remove((r >> 8) as usize % len);
646                    }
647                    3 if len > 0 => {
648                        let i = (r >> 8) as usize % len;
649                        items[i].checked ^= true;
650                    }
651                    4 if len > 0 => {
652                        let i = (r >> 8) as usize % len;
653                        items[i].editing ^= true;
654                    }
655                    5 => state.filter.set(((r >> 8) % 3) as u8),
656                    6 => items.retain(|i| !i.checked),
657                    7 if len > 12 => items.clear(),
658                    _ => {}
659                }
660            }
661            rerender(&mut doc);
662            if step % 1000 == 0 {
663                state.items.borrow_mut().clear();
664                rerender(&mut doc);
665                counts.push(node_count(&doc));
666            }
667        }
668        // The first round creates the (retained) template prototype nodes.
669        assert_eq!(counts[1], counts[2]);
670    }
671
672    #[test]
673    fn mounted_data_of_removed_element_reports_error() {
674        #[derive(Default)]
675        struct State {
676            show: Cell<bool>,
677            mounted: RefCell<Option<Rc<MountedData>>>,
678        }
679
680        fn app() -> Element {
681            let state = consume_context::<Rc<State>>();
682            rsx!(if state.show.get() {
683                div {
684                    onmounted: move |e| *state.mounted.borrow_mut() = Some(e.data()),
685                }
686            })
687        }
688
689        let state = Rc::new(State::default());
690        state.show.set(true);
691        let vdom = VirtualDom::new(app).with_root_context(Rc::clone(&state));
692        let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
693        doc.initial_build();
694        let mounted = state.mounted.take().unwrap();
695        let focus = |focus: bool| {
696            let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
697            let fut = std::pin::pin!(mounted.set_focus(focus));
698            matches!(fut.poll(&mut cx), std::task::Poll::Ready(Ok(())))
699        };
700        assert!(focus(true));
701        let focussed = |doc: &DioxusDocument| doc.inner.borrow().get_focussed_node_id();
702        let mounted_id = focussed(&doc).unwrap();
703        state.show.set(false);
704        rerender(&mut doc);
705        assert!(doc.inner.borrow().get_node(mounted_id).is_none());
706        assert_ne!(focussed(&doc), Some(mounted_id));
707
708        // Focussing or blurring the removed element is a no-op.
709        let focussed_after_removal = focussed(&doc);
710        assert!(focus(true));
711        assert!(focus(false));
712        assert_eq!(focussed(&doc), focussed_after_removal);
713
714        fn is_err<T>(fut: impl Future<Output = Result<T, MountedError>>) -> bool {
715            let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
716            let fut = std::pin::pin!(fut);
717            matches!(fut.poll(&mut cx), std::task::Poll::Ready(Err(_)))
718        }
719        assert!(is_err(mounted.get_scroll_offset()));
720        assert!(is_err(mounted.get_scroll_size()));
721        assert!(is_err(mounted.get_client_rect()));
722        assert!(is_err(mounted.scroll_to(ScrollBehavior::Instant)));
723        assert!(is_err(
724            mounted.scroll(Default::default(), ScrollBehavior::Instant)
725        ));
726    }
727}