Skip to main content

telegram_webapp_sdk/dom/
document.rs

1// SPDX-FileCopyrightText: 2025-2026 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4use wasm_bindgen::JsValue;
5use web_sys::{Element, HtmlElement};
6
7/// Zero-sized handle providing convenient access to the current
8/// [`web_sys::Document`].
9///
10/// Each method resolves `window().document()` on demand, so no browser handles
11/// are stored. Re-exported as `Document`.
12pub struct Doc;
13
14impl Doc {
15    /// Returns the element with the given `id`, or `None` if it does not exist
16    /// or no document is available.
17    pub fn get_element_by_id(&self, id: &str) -> Option<Element> {
18        web_sys::window()?.document()?.get_element_by_id(id)
19    }
20
21    /// Returns the first element matching the CSS `selector`.
22    ///
23    /// Returns `Ok(None)` when nothing matches, and `Err` when the document is
24    /// unavailable or the selector is invalid.
25    pub fn query_selector(&self, selector: &str) -> Result<Option<Element>, JsValue> {
26        web_sys::window()
27            .and_then(|w| w.document())
28            .ok_or_else(|| JsValue::from_str("document not available"))?
29            .query_selector(selector)
30    }
31
32    /// Creates a new element with the given `tag` name.
33    ///
34    /// Returns `Err` when the window or document is unavailable.
35    pub fn create_element(&self, tag: &str) -> Result<Element, JsValue> {
36        web_sys::window()
37            .ok_or_else(|| JsValue::from_str("window not available"))?
38            .document()
39            .ok_or_else(|| JsValue::from_str("document not available"))?
40            .create_element(tag)
41    }
42
43    /// Returns the document `<body>` element.
44    ///
45    /// Returns `Err` when the window, document, or body is unavailable.
46    pub fn body(&self) -> Result<HtmlElement, JsValue> {
47        web_sys::window()
48            .ok_or_else(|| JsValue::from_str("window not available"))?
49            .document()
50            .ok_or_else(|| JsValue::from_str("document not available"))?
51            .body()
52            .ok_or_else(|| JsValue::from_str("body not available"))
53    }
54}
55
56impl Default for Doc {
57    fn default() -> Self {
58        Self
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use wasm_bindgen::JsCast;
65    use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
66    use web_sys::Element;
67
68    use super::*;
69    use crate::dom::ElementExt;
70
71    wasm_bindgen_test_configure!(run_in_browser);
72
73    fn fresh_root(id: &str) -> Element {
74        let doc = Doc;
75        let body = doc.body().expect("body");
76        let container = doc.create_element("div").expect("create container");
77        container.set_id(id);
78        body.append_child(&container).expect("append container");
79        container
80    }
81
82    fn cleanup(root: &Element) {
83        root.remove();
84    }
85
86    #[wasm_bindgen_test]
87    #[allow(dead_code, clippy::unused_unit)]
88    fn create_element_returns_tagged() {
89        let el = Doc.create_element("section").expect("create");
90        assert_eq!(el.tag_name().to_ascii_lowercase(), "section");
91    }
92
93    #[wasm_bindgen_test]
94    #[allow(dead_code, clippy::unused_unit)]
95    fn get_element_by_id_finds_existing_and_misses() {
96        let root = fresh_root("doc-test-get-by-id");
97        assert!(Doc.get_element_by_id("doc-test-get-by-id").is_some());
98        assert!(Doc.get_element_by_id("doc-test-absent").is_none());
99        cleanup(&root);
100    }
101
102    #[wasm_bindgen_test]
103    #[allow(dead_code, clippy::unused_unit)]
104    fn query_selector_finds_and_misses() {
105        let root = fresh_root("doc-test-qs");
106        let child = Doc.create_element("span").expect("span");
107        child.set_class("hit");
108        root.append_child(&child).expect("append");
109
110        let found = Doc.query_selector("#doc-test-qs .hit").expect("ok");
111        assert!(found.is_some());
112        let absent = Doc.query_selector("#doc-test-qs .nope").expect("ok");
113        assert!(absent.is_none());
114
115        cleanup(&root);
116    }
117
118    #[wasm_bindgen_test]
119    #[allow(dead_code, clippy::unused_unit)]
120    fn query_selector_invalid_returns_err() {
121        assert!(Doc.query_selector(">>>broken<<<").is_err());
122    }
123
124    #[wasm_bindgen_test]
125    #[allow(dead_code, clippy::unused_unit)]
126    fn body_returns_html_element() {
127        let body = Doc.body().expect("body");
128        let as_el: &Element = body.unchecked_ref();
129        assert_eq!(as_el.tag_name().to_ascii_lowercase(), "body");
130    }
131}