Skip to main content

telegram_webapp_sdk/
webapp.rs

1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4use js_sys::Object;
5
6// Module declarations
7mod buttons;
8mod core;
9mod dialogs;
10mod events;
11mod lifecycle;
12mod navigation;
13mod permissions;
14mod theme;
15/// Public data types shared across the WebApp bindings: button descriptors,
16/// button parameters, link/close options and event handles.
17pub mod types;
18mod viewport;
19
20// Re-export public types
21pub use types::{
22    BackgroundEvent, BottomButton, BottomButtonParams, CloseOptions, EventHandle, OpenLinkOptions,
23    SafeAreaInset, SecondaryButtonParams, SecondaryButtonPosition
24};
25
26/// Safe wrapper around `window.Telegram.WebApp`
27#[derive(Clone)]
28pub struct TelegramWebApp {
29    pub(super) inner: Object
30}
31
32#[cfg(test)]
33mod tests {
34    use std::{
35        cell::{Cell, RefCell},
36        rc::Rc
37    };
38
39    use js_sys::{Function, Object, Reflect};
40    use wasm_bindgen::{JsCast, JsValue, prelude::Closure};
41    use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
42    use web_sys::window;
43
44    use super::*;
45    use crate::core::types::download_file_params::DownloadFileParams;
46
47    wasm_bindgen_test_configure!(run_in_browser);
48
49    #[allow(dead_code)]
50    fn setup_webapp() -> Object {
51        let win = window().unwrap();
52        let telegram = Object::new();
53        let webapp = Object::new();
54        let _ = Reflect::set(&win, &"Telegram".into(), &telegram);
55        let _ = Reflect::set(&telegram, &"WebApp".into(), &webapp);
56        webapp
57    }
58
59    #[wasm_bindgen_test]
60    #[allow(dead_code, clippy::unused_unit)]
61    fn hide_keyboard_calls_js() {
62        let webapp = setup_webapp();
63        let called = Rc::new(Cell::new(false));
64        let called_clone = Rc::clone(&called);
65
66        let hide_cb = Closure::<dyn FnMut()>::new(move || {
67            called_clone.set(true);
68        });
69        let _ = Reflect::set(
70            &webapp,
71            &"hideKeyboard".into(),
72            hide_cb.as_ref().unchecked_ref()
73        );
74        hide_cb.forget();
75
76        let app = TelegramWebApp::instance().unwrap();
77        app.hide_keyboard().unwrap();
78        assert!(called.get());
79    }
80
81    #[wasm_bindgen_test]
82    #[allow(dead_code, clippy::unused_unit)]
83    fn hide_main_button_calls_js() {
84        let webapp = setup_webapp();
85        let main_button = Object::new();
86        let called = Rc::new(Cell::new(false));
87        let called_clone = Rc::clone(&called);
88
89        let hide_cb = Closure::<dyn FnMut()>::new(move || {
90            called_clone.set(true);
91        });
92        let _ = Reflect::set(
93            &main_button,
94            &"hide".into(),
95            hide_cb.as_ref().unchecked_ref()
96        );
97        hide_cb.forget();
98
99        let _ = Reflect::set(&webapp, &"MainButton".into(), &main_button);
100
101        let app = TelegramWebApp::instance().unwrap();
102        app.hide_bottom_button(BottomButton::Main).unwrap();
103        assert!(called.get());
104    }
105
106    #[wasm_bindgen_test]
107    #[allow(dead_code, clippy::unused_unit)]
108    fn hide_secondary_button_calls_js() {
109        let webapp = setup_webapp();
110        let secondary_button = Object::new();
111        let called = Rc::new(Cell::new(false));
112        let called_clone = Rc::clone(&called);
113
114        let hide_cb = Closure::<dyn FnMut()>::new(move || {
115            called_clone.set(true);
116        });
117        let _ = Reflect::set(
118            &secondary_button,
119            &"hide".into(),
120            hide_cb.as_ref().unchecked_ref()
121        );
122        hide_cb.forget();
123
124        let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &secondary_button);
125
126        let app = TelegramWebApp::instance().unwrap();
127        app.hide_bottom_button(BottomButton::Secondary).unwrap();
128        assert!(called.get());
129    }
130
131    #[wasm_bindgen_test]
132    #[allow(dead_code, clippy::unused_unit)]
133    fn set_bottom_button_color_calls_js() {
134        let webapp = setup_webapp();
135        let main_button = Object::new();
136        let received = Rc::new(RefCell::new(None));
137        let rc_clone = Rc::clone(&received);
138
139        let set_color_cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
140            *rc_clone.borrow_mut() = v.as_string();
141        });
142        let _ = Reflect::set(
143            &main_button,
144            &"setColor".into(),
145            set_color_cb.as_ref().unchecked_ref()
146        );
147        set_color_cb.forget();
148
149        let _ = Reflect::set(&webapp, &"MainButton".into(), &main_button);
150
151        let app = TelegramWebApp::instance().unwrap();
152        app.set_bottom_button_color(BottomButton::Main, "#00ff00")
153            .unwrap();
154        assert_eq!(received.borrow().as_deref(), Some("#00ff00"));
155    }
156
157    #[wasm_bindgen_test]
158    #[allow(dead_code, clippy::unused_unit)]
159    fn set_secondary_button_color_calls_js() {
160        let webapp = setup_webapp();
161        let secondary_button = Object::new();
162        let received = Rc::new(RefCell::new(None));
163        let rc_clone = Rc::clone(&received);
164
165        let set_color_cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
166            *rc_clone.borrow_mut() = v.as_string();
167        });
168        let _ = Reflect::set(
169            &secondary_button,
170            &"setColor".into(),
171            set_color_cb.as_ref().unchecked_ref()
172        );
173        set_color_cb.forget();
174
175        let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &secondary_button);
176
177        let app = TelegramWebApp::instance().unwrap();
178        app.set_bottom_button_color(BottomButton::Secondary, "#00ff00")
179            .unwrap();
180        assert_eq!(received.borrow().as_deref(), Some("#00ff00"));
181    }
182
183    #[wasm_bindgen_test]
184    #[allow(dead_code, clippy::unused_unit)]
185    fn set_bottom_button_text_color_calls_js() {
186        let webapp = setup_webapp();
187        let main_button = Object::new();
188        let received = Rc::new(RefCell::new(None));
189        let rc_clone = Rc::clone(&received);
190
191        let set_color_cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
192            *rc_clone.borrow_mut() = v.as_string();
193        });
194        let _ = Reflect::set(
195            &main_button,
196            &"setTextColor".into(),
197            set_color_cb.as_ref().unchecked_ref()
198        );
199        set_color_cb.forget();
200
201        let _ = Reflect::set(&webapp, &"MainButton".into(), &main_button);
202
203        let app = TelegramWebApp::instance().unwrap();
204        app.set_bottom_button_text_color(BottomButton::Main, "#112233")
205            .unwrap();
206        assert_eq!(received.borrow().as_deref(), Some("#112233"));
207    }
208
209    #[wasm_bindgen_test]
210    #[allow(dead_code, clippy::unused_unit)]
211    fn set_secondary_button_text_color_calls_js() {
212        let webapp = setup_webapp();
213        let secondary_button = Object::new();
214        let received = Rc::new(RefCell::new(None));
215        let rc_clone = Rc::clone(&received);
216
217        let set_color_cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
218            *rc_clone.borrow_mut() = v.as_string();
219        });
220        let _ = Reflect::set(
221            &secondary_button,
222            &"setTextColor".into(),
223            set_color_cb.as_ref().unchecked_ref()
224        );
225        set_color_cb.forget();
226
227        let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &secondary_button);
228
229        let app = TelegramWebApp::instance().unwrap();
230        app.set_bottom_button_text_color(BottomButton::Secondary, "#112233")
231            .unwrap();
232        assert_eq!(received.borrow().as_deref(), Some("#112233"));
233    }
234
235    #[wasm_bindgen_test]
236    #[allow(dead_code, clippy::unused_unit)]
237    fn enable_bottom_button_calls_js() {
238        let webapp = setup_webapp();
239        let button = Object::new();
240        let called = Rc::new(Cell::new(false));
241        let called_clone = Rc::clone(&called);
242
243        let enable_cb = Closure::<dyn FnMut()>::new(move || {
244            called_clone.set(true);
245        });
246        let _ = Reflect::set(
247            &button,
248            &"enable".into(),
249            enable_cb.as_ref().unchecked_ref()
250        );
251        enable_cb.forget();
252
253        let _ = Reflect::set(&webapp, &"MainButton".into(), &button);
254
255        let app = TelegramWebApp::instance().unwrap();
256        app.enable_bottom_button(BottomButton::Main).unwrap();
257        assert!(called.get());
258    }
259
260    #[wasm_bindgen_test]
261    #[allow(dead_code, clippy::unused_unit)]
262    fn show_bottom_button_progress_passes_flag() {
263        let webapp = setup_webapp();
264        let button = Object::new();
265        let received = Rc::new(RefCell::new(None));
266        let rc_clone = Rc::clone(&received);
267
268        let cb = Closure::<dyn FnMut(JsValue)>::new(move |arg: JsValue| {
269            *rc_clone.borrow_mut() = arg.as_bool();
270        });
271        let _ = Reflect::set(&button, &"showProgress".into(), cb.as_ref().unchecked_ref());
272        cb.forget();
273
274        let _ = Reflect::set(&webapp, &"MainButton".into(), &button);
275
276        let app = TelegramWebApp::instance().unwrap();
277        app.show_bottom_button_progress(BottomButton::Main, true)
278            .unwrap();
279        assert_eq!(*received.borrow(), Some(true));
280    }
281
282    #[wasm_bindgen_test]
283    #[allow(dead_code, clippy::unused_unit)]
284    fn set_bottom_button_params_serializes() {
285        let webapp = setup_webapp();
286        let button = Object::new();
287        let received = Rc::new(RefCell::new(Object::new()));
288        let rc_clone = Rc::clone(&received);
289
290        let cb = Closure::<dyn FnMut(JsValue)>::new(move |value: JsValue| {
291            let obj = value.dyn_into::<Object>().expect("object");
292            rc_clone.replace(obj);
293        });
294        let _ = Reflect::set(&button, &"setParams".into(), cb.as_ref().unchecked_ref());
295        cb.forget();
296
297        let _ = Reflect::set(&webapp, &"MainButton".into(), &button);
298
299        let app = TelegramWebApp::instance().unwrap();
300        let params = BottomButtonParams {
301            text: Some("Send"),
302            color: Some("#ffffff"),
303            text_color: Some("#000000"),
304            is_active: Some(true),
305            is_visible: Some(true),
306            has_shine_effect: Some(false),
307            ..Default::default()
308        };
309        app.set_bottom_button_params(BottomButton::Main, &params)
310            .unwrap();
311
312        let stored = received.borrow();
313        assert_eq!(
314            Reflect::get(&stored, &"text".into()).unwrap().as_string(),
315            Some("Send".to_string())
316        );
317        assert_eq!(
318            Reflect::get(&stored, &"color".into()).unwrap().as_string(),
319            Some("#ffffff".to_string())
320        );
321        assert_eq!(
322            Reflect::get(&stored, &"text_color".into())
323                .unwrap()
324                .as_string(),
325            Some("#000000".to_string())
326        );
327    }
328
329    #[wasm_bindgen_test]
330    #[allow(dead_code, clippy::unused_unit)]
331    fn set_secondary_button_params_serializes_position() {
332        let webapp = setup_webapp();
333        let button = Object::new();
334        let received = Rc::new(RefCell::new(Object::new()));
335        let rc_clone = Rc::clone(&received);
336
337        let cb = Closure::<dyn FnMut(JsValue)>::new(move |value: JsValue| {
338            let obj = value.dyn_into::<Object>().expect("object");
339            rc_clone.replace(obj);
340        });
341        let _ = Reflect::set(&button, &"setParams".into(), cb.as_ref().unchecked_ref());
342        cb.forget();
343
344        let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &button);
345
346        let app = TelegramWebApp::instance().unwrap();
347        let params = SecondaryButtonParams {
348            common:   BottomButtonParams {
349                text: Some("Next"),
350                ..Default::default()
351            },
352            position: Some(SecondaryButtonPosition::Left)
353        };
354        app.set_secondary_button_params(&params).unwrap();
355
356        let stored = received.borrow();
357        assert_eq!(
358            Reflect::get(&stored, &"text".into()).unwrap().as_string(),
359            Some("Next".to_string())
360        );
361        assert_eq!(
362            Reflect::get(&stored, &"position".into())
363                .unwrap()
364                .as_string(),
365            Some("left".to_string())
366        );
367    }
368
369    #[wasm_bindgen_test]
370    #[allow(dead_code, clippy::unused_unit)]
371    fn bottom_button_getters_return_values() {
372        let webapp = setup_webapp();
373        let button = Object::new();
374        let _ = Reflect::set(&button, &"text".into(), &"Label".into());
375        let _ = Reflect::set(&button, &"textColor".into(), &"#111111".into());
376        let _ = Reflect::set(&button, &"color".into(), &"#222222".into());
377        let _ = Reflect::set(&button, &"isVisible".into(), &JsValue::TRUE);
378        let _ = Reflect::set(&button, &"isActive".into(), &JsValue::TRUE);
379        let _ = Reflect::set(&button, &"isProgressVisible".into(), &JsValue::FALSE);
380        let _ = Reflect::set(&button, &"hasShineEffect".into(), &JsValue::TRUE);
381
382        let _ = Reflect::set(&webapp, &"MainButton".into(), &button);
383
384        let app = TelegramWebApp::instance().unwrap();
385        assert_eq!(
386            app.bottom_button_text(BottomButton::Main),
387            Some("Label".into())
388        );
389        assert_eq!(
390            app.bottom_button_text_color(BottomButton::Main),
391            Some("#111111".into())
392        );
393        assert_eq!(
394            app.bottom_button_color(BottomButton::Main),
395            Some("#222222".into())
396        );
397        assert!(app.is_bottom_button_visible(BottomButton::Main));
398        assert!(app.is_bottom_button_active(BottomButton::Main));
399        assert!(!app.is_bottom_button_progress_visible(BottomButton::Main));
400        assert!(app.bottom_button_has_shine_effect(BottomButton::Main));
401    }
402
403    #[wasm_bindgen_test]
404    #[allow(dead_code, clippy::unused_unit)]
405    fn set_bottom_button_icon_custom_emoji_id_calls_js() {
406        let webapp = setup_webapp();
407        let button = Object::new();
408        let received = Rc::new(RefCell::new(None));
409        let rc_clone = Rc::clone(&received);
410
411        let set_icon_cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
412            *rc_clone.borrow_mut() = v.as_string();
413        });
414        let _ = Reflect::set(
415            &button,
416            &"setIconCustomEmojiId".into(),
417            set_icon_cb.as_ref().unchecked_ref()
418        );
419        set_icon_cb.forget();
420
421        let _ = Reflect::set(&webapp, &"MainButton".into(), &button);
422
423        let app = TelegramWebApp::instance().unwrap();
424        app.set_bottom_button_icon_custom_emoji_id(BottomButton::Main, "123456789")
425            .unwrap();
426        assert_eq!(received.borrow().as_deref(), Some("123456789"));
427    }
428
429    #[wasm_bindgen_test]
430    #[allow(dead_code, clippy::unused_unit)]
431    fn set_secondary_button_icon_custom_emoji_id_calls_js() {
432        let webapp = setup_webapp();
433        let secondary_button = Object::new();
434        let received = Rc::new(RefCell::new(None));
435        let rc_clone = Rc::clone(&received);
436
437        let set_icon_cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
438            *rc_clone.borrow_mut() = v.as_string();
439        });
440        let _ = Reflect::set(
441            &secondary_button,
442            &"setIconCustomEmojiId".into(),
443            set_icon_cb.as_ref().unchecked_ref()
444        );
445        set_icon_cb.forget();
446
447        let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &secondary_button);
448
449        let app = TelegramWebApp::instance().unwrap();
450        app.set_bottom_button_icon_custom_emoji_id(BottomButton::Secondary, "987654321")
451            .unwrap();
452        assert_eq!(received.borrow().as_deref(), Some("987654321"));
453    }
454
455    #[wasm_bindgen_test]
456    #[allow(dead_code, clippy::unused_unit)]
457    fn bottom_button_icon_custom_emoji_id_getter_returns_value() {
458        let webapp = setup_webapp();
459        let button = Object::new();
460        let _ = Reflect::set(&button, &"iconCustomEmojiId".into(), &"123456789".into());
461
462        let _ = Reflect::set(&webapp, &"MainButton".into(), &button);
463
464        let app = TelegramWebApp::instance().unwrap();
465        assert_eq!(
466            app.bottom_button_icon_custom_emoji_id(BottomButton::Main),
467            Some("123456789".into())
468        );
469    }
470
471    #[wasm_bindgen_test]
472    #[allow(dead_code, clippy::unused_unit)]
473    fn bottom_button_icon_custom_emoji_id_returns_none_when_not_set() {
474        let webapp = setup_webapp();
475        let button = Object::new();
476
477        let _ = Reflect::set(&webapp, &"MainButton".into(), &button);
478
479        let app = TelegramWebApp::instance().unwrap();
480        assert_eq!(
481            app.bottom_button_icon_custom_emoji_id(BottomButton::Main),
482            None
483        );
484    }
485
486    #[wasm_bindgen_test]
487    #[allow(dead_code, clippy::unused_unit)]
488    fn set_bottom_button_params_with_icon_custom_emoji_id() {
489        let webapp = setup_webapp();
490        let button = Object::new();
491        let received = Rc::new(RefCell::new(Object::new()));
492        let rc_clone = Rc::clone(&received);
493
494        let cb = Closure::<dyn FnMut(JsValue)>::new(move |value: JsValue| {
495            let obj = value.dyn_into::<Object>().expect("object");
496            rc_clone.replace(obj);
497        });
498        let _ = Reflect::set(&button, &"setParams".into(), cb.as_ref().unchecked_ref());
499        cb.forget();
500
501        let _ = Reflect::set(&webapp, &"MainButton".into(), &button);
502
503        let app = TelegramWebApp::instance().unwrap();
504        let params = BottomButtonParams {
505            text: Some("Send"),
506            icon_custom_emoji_id: Some("123456789"),
507            ..Default::default()
508        };
509        app.set_bottom_button_params(BottomButton::Main, &params)
510            .unwrap();
511
512        let stored = received.borrow();
513        assert_eq!(
514            Reflect::get(&stored, &"text".into()).unwrap().as_string(),
515            Some("Send".to_string())
516        );
517        assert_eq!(
518            Reflect::get(&stored, &"icon_custom_emoji_id".into())
519                .unwrap()
520                .as_string(),
521            Some("123456789".to_string())
522        );
523    }
524
525    #[wasm_bindgen_test]
526    #[allow(dead_code, clippy::unused_unit)]
527    fn secondary_button_position_is_parsed() {
528        let webapp = setup_webapp();
529        let button = Object::new();
530        let _ = Reflect::set(&button, &"position".into(), &"right".into());
531        let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &button);
532
533        let app = TelegramWebApp::instance().unwrap();
534        assert_eq!(
535            app.secondary_button_position(),
536            Some(SecondaryButtonPosition::Right)
537        );
538    }
539
540    #[wasm_bindgen_test]
541    #[allow(dead_code, clippy::unused_unit)]
542    fn set_header_color_calls_js() {
543        let webapp = setup_webapp();
544        let received = Rc::new(RefCell::new(None));
545        let rc_clone = Rc::clone(&received);
546
547        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
548            *rc_clone.borrow_mut() = v.as_string();
549        });
550        let _ = Reflect::set(
551            &webapp,
552            &"setHeaderColor".into(),
553            cb.as_ref().unchecked_ref()
554        );
555        cb.forget();
556
557        let app = TelegramWebApp::instance().unwrap();
558        app.set_header_color("#abcdef").unwrap();
559        assert_eq!(received.borrow().as_deref(), Some("#abcdef"));
560    }
561
562    #[wasm_bindgen_test]
563    #[allow(dead_code, clippy::unused_unit)]
564    fn set_background_color_calls_js() {
565        let webapp = setup_webapp();
566        let received = Rc::new(RefCell::new(None));
567        let rc_clone = Rc::clone(&received);
568
569        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
570            *rc_clone.borrow_mut() = v.as_string();
571        });
572        let _ = Reflect::set(
573            &webapp,
574            &"setBackgroundColor".into(),
575            cb.as_ref().unchecked_ref()
576        );
577        cb.forget();
578
579        let app = TelegramWebApp::instance().unwrap();
580        app.set_background_color("#123456").unwrap();
581        assert_eq!(received.borrow().as_deref(), Some("#123456"));
582    }
583
584    #[wasm_bindgen_test]
585    #[allow(dead_code, clippy::unused_unit)]
586    fn set_bottom_bar_color_calls_js() {
587        let webapp = setup_webapp();
588        let received = Rc::new(RefCell::new(None));
589        let rc_clone = Rc::clone(&received);
590
591        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
592            *rc_clone.borrow_mut() = v.as_string();
593        });
594        let _ = Reflect::set(
595            &webapp,
596            &"setBottomBarColor".into(),
597            cb.as_ref().unchecked_ref()
598        );
599        cb.forget();
600
601        let app = TelegramWebApp::instance().unwrap();
602        app.set_bottom_bar_color("#654321").unwrap();
603        assert_eq!(received.borrow().as_deref(), Some("#654321"));
604    }
605
606    #[wasm_bindgen_test]
607    #[allow(dead_code, clippy::unused_unit)]
608    fn viewport_dimensions() {
609        let webapp = setup_webapp();
610        let _ = Reflect::set(&webapp, &"viewportWidth".into(), &JsValue::from_f64(320.0));
611        let _ = Reflect::set(
612            &webapp,
613            &"viewportStableHeight".into(),
614            &JsValue::from_f64(480.0)
615        );
616        let app = TelegramWebApp::instance().unwrap();
617        assert_eq!(app.viewport_width(), Some(320.0));
618        assert_eq!(app.viewport_stable_height(), Some(480.0));
619    }
620
621    #[wasm_bindgen_test]
622    #[allow(dead_code, clippy::unused_unit)]
623    fn version_check_invokes_js() {
624        let webapp = setup_webapp();
625        let cb = Function::new_with_args("v", "return v === '9.0';");
626        let _ = Reflect::set(&webapp, &"isVersionAtLeast".into(), &cb);
627
628        let app = TelegramWebApp::instance().unwrap();
629        assert!(app.is_version_at_least("9.0").unwrap());
630        assert!(!app.is_version_at_least("9.1").unwrap());
631    }
632
633    #[wasm_bindgen_test]
634    #[allow(dead_code, clippy::unused_unit)]
635    fn safe_area_insets_are_parsed() {
636        let webapp = setup_webapp();
637        let safe_area = Object::new();
638        let _ = Reflect::set(&safe_area, &"top".into(), &JsValue::from_f64(1.0));
639        let _ = Reflect::set(&safe_area, &"bottom".into(), &JsValue::from_f64(2.0));
640        let _ = Reflect::set(&safe_area, &"left".into(), &JsValue::from_f64(3.0));
641        let _ = Reflect::set(&safe_area, &"right".into(), &JsValue::from_f64(4.0));
642        let _ = Reflect::set(&webapp, &"safeAreaInset".into(), &safe_area);
643
644        let content_safe = Object::new();
645        let _ = Reflect::set(&content_safe, &"top".into(), &JsValue::from_f64(5.0));
646        let _ = Reflect::set(&content_safe, &"bottom".into(), &JsValue::from_f64(6.0));
647        let _ = Reflect::set(&content_safe, &"left".into(), &JsValue::from_f64(7.0));
648        let _ = Reflect::set(&content_safe, &"right".into(), &JsValue::from_f64(8.0));
649        let _ = Reflect::set(&webapp, &"contentSafeAreaInset".into(), &content_safe);
650
651        let app = TelegramWebApp::instance().unwrap();
652        let inset = app.safe_area_inset().expect("safe area");
653        assert_eq!(inset.top, 1.0);
654        assert_eq!(inset.bottom, 2.0);
655        assert_eq!(inset.left, 3.0);
656        assert_eq!(inset.right, 4.0);
657
658        let content = app.content_safe_area_inset().expect("content area");
659        assert_eq!(content.top, 5.0);
660    }
661
662    #[wasm_bindgen_test]
663    #[allow(dead_code, clippy::unused_unit)]
664    fn activity_flags_are_reported() {
665        let webapp = setup_webapp();
666        let _ = Reflect::set(&webapp, &"isActive".into(), &JsValue::TRUE);
667        let _ = Reflect::set(&webapp, &"isFullscreen".into(), &JsValue::TRUE);
668        let _ = Reflect::set(&webapp, &"isOrientationLocked".into(), &JsValue::FALSE);
669        let _ = Reflect::set(&webapp, &"isVerticalSwipesEnabled".into(), &JsValue::TRUE);
670
671        let app = TelegramWebApp::instance().unwrap();
672        assert!(app.is_active());
673        assert!(app.is_fullscreen());
674        assert!(!app.is_orientation_locked());
675        assert!(app.is_vertical_swipes_enabled());
676    }
677
678    #[wasm_bindgen_test]
679    #[allow(dead_code, clippy::unused_unit)]
680    fn back_button_visibility_and_callback() {
681        let webapp = setup_webapp();
682        let back_button = Object::new();
683        let _ = Reflect::set(&webapp, &"BackButton".into(), &back_button);
684        let _ = Reflect::set(&back_button, &"isVisible".into(), &JsValue::TRUE);
685
686        let on_click = Function::new_with_args("cb", "this.cb = cb;");
687        let off_click = Function::new_with_args("", "delete this.cb;");
688        let _ = Reflect::set(&back_button, &"onClick".into(), &on_click);
689        let _ = Reflect::set(&back_button, &"offClick".into(), &off_click);
690
691        let called = Rc::new(Cell::new(false));
692        let called_clone = Rc::clone(&called);
693
694        let app = TelegramWebApp::instance().unwrap();
695        assert!(app.is_back_button_visible());
696        let handle = app
697            .set_back_button_callback(move || {
698                called_clone.set(true);
699            })
700            .unwrap();
701
702        let stored = Reflect::has(&back_button, &"cb".into()).unwrap();
703        assert!(stored);
704
705        let cb_fn = Reflect::get(&back_button, &"cb".into())
706            .unwrap()
707            .dyn_into::<Function>()
708            .unwrap();
709        let _ = cb_fn.call0(&JsValue::NULL);
710        assert!(called.get());
711
712        app.remove_back_button_callback(handle).unwrap();
713        let stored_after = Reflect::has(&back_button, &"cb".into()).unwrap();
714        assert!(!stored_after);
715    }
716
717    #[wasm_bindgen_test]
718    #[allow(dead_code, clippy::unused_unit)]
719    fn bottom_button_callback_register_and_remove() {
720        let webapp = setup_webapp();
721        let main_button = Object::new();
722        let _ = Reflect::set(&webapp, &"MainButton".into(), &main_button);
723
724        let on_click = Function::new_with_args("cb", "this.cb = cb;");
725        let off_click = Function::new_with_args("", "delete this.cb;");
726        let _ = Reflect::set(&main_button, &"onClick".into(), &on_click);
727        let _ = Reflect::set(&main_button, &"offClick".into(), &off_click);
728
729        let called = Rc::new(Cell::new(false));
730        let called_clone = Rc::clone(&called);
731
732        let app = TelegramWebApp::instance().unwrap();
733        let handle = app
734            .set_bottom_button_callback(BottomButton::Main, move || {
735                called_clone.set(true);
736            })
737            .unwrap();
738
739        let stored = Reflect::has(&main_button, &"cb".into()).unwrap();
740        assert!(stored);
741
742        let cb_fn = Reflect::get(&main_button, &"cb".into())
743            .unwrap()
744            .dyn_into::<Function>()
745            .unwrap();
746        let _ = cb_fn.call0(&JsValue::NULL);
747        assert!(called.get());
748
749        app.remove_bottom_button_callback(handle).unwrap();
750        let stored_after = Reflect::has(&main_button, &"cb".into()).unwrap();
751        assert!(!stored_after);
752    }
753
754    #[wasm_bindgen_test]
755    #[allow(dead_code, clippy::unused_unit)]
756    fn secondary_button_callback_register_and_remove() {
757        let webapp = setup_webapp();
758        let secondary_button = Object::new();
759        let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &secondary_button);
760
761        let on_click = Function::new_with_args("cb", "this.cb = cb;");
762        let off_click = Function::new_with_args("", "delete this.cb;");
763        let _ = Reflect::set(&secondary_button, &"onClick".into(), &on_click);
764        let _ = Reflect::set(&secondary_button, &"offClick".into(), &off_click);
765
766        let called = Rc::new(Cell::new(false));
767        let called_clone = Rc::clone(&called);
768
769        let app = TelegramWebApp::instance().unwrap();
770        let handle = app
771            .set_bottom_button_callback(BottomButton::Secondary, move || {
772                called_clone.set(true);
773            })
774            .unwrap();
775
776        let stored = Reflect::has(&secondary_button, &"cb".into()).unwrap();
777        assert!(stored);
778
779        let cb_fn = Reflect::get(&secondary_button, &"cb".into())
780            .unwrap()
781            .dyn_into::<Function>()
782            .unwrap();
783        let _ = cb_fn.call0(&JsValue::NULL);
784        assert!(called.get());
785
786        app.remove_bottom_button_callback(handle).unwrap();
787        let stored_after = Reflect::has(&secondary_button, &"cb".into()).unwrap();
788        assert!(!stored_after);
789    }
790
791    #[wasm_bindgen_test]
792    #[allow(dead_code, clippy::unused_unit)]
793    fn on_event_register_and_remove() {
794        let webapp = setup_webapp();
795        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
796        let off_event = Function::new_with_args("name", "delete this[name];");
797        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
798        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
799
800        let app = TelegramWebApp::instance().unwrap();
801        let handle = app.on_event("test", |_: JsValue| {}).unwrap();
802        assert!(Reflect::has(&webapp, &"test".into()).unwrap());
803        app.off_event(handle).unwrap();
804        assert!(!Reflect::has(&webapp, &"test".into()).unwrap());
805    }
806
807    #[wasm_bindgen_test]
808    #[allow(dead_code, clippy::unused_unit)]
809    fn background_event_register_and_remove() {
810        let webapp = setup_webapp();
811        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
812        let off_event = Function::new_with_args("name", "delete this[name];");
813        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
814        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
815
816        let app = TelegramWebApp::instance().unwrap();
817        let handle = app
818            .on_background_event(BackgroundEvent::MainButtonClicked, |_| {})
819            .unwrap();
820        assert!(Reflect::has(&webapp, &"mainButtonClicked".into()).unwrap());
821        app.off_event(handle).unwrap();
822        assert!(!Reflect::has(&webapp, &"mainButtonClicked".into()).unwrap());
823    }
824
825    #[wasm_bindgen_test]
826    #[allow(dead_code, clippy::unused_unit)]
827    fn background_event_delivers_data() {
828        let webapp = setup_webapp();
829        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
830        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
831
832        let app = TelegramWebApp::instance().unwrap();
833        let received = Rc::new(RefCell::new(String::new()));
834        let received_clone = Rc::clone(&received);
835        let _handle = app
836            .on_background_event(BackgroundEvent::InvoiceClosed, move |v| {
837                *received_clone.borrow_mut() = v.as_string().unwrap_or_default();
838            })
839            .unwrap();
840
841        let cb = Reflect::get(&webapp, &"invoiceClosed".into())
842            .unwrap()
843            .dyn_into::<Function>()
844            .unwrap();
845        let _ = cb.call1(&JsValue::NULL, &JsValue::from_str("paid"));
846        assert_eq!(received.borrow().as_str(), "paid");
847    }
848
849    #[wasm_bindgen_test]
850    #[allow(dead_code, clippy::unused_unit)]
851    fn theme_changed_register_and_remove() {
852        let webapp = setup_webapp();
853        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
854        let off_event = Function::new_with_args("name", "delete this[name];");
855        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
856        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
857
858        let app = TelegramWebApp::instance().unwrap();
859        let handle = app.on_theme_changed(|| {}).unwrap();
860        assert!(Reflect::has(&webapp, &"themeChanged".into()).unwrap());
861        app.off_event(handle).unwrap();
862        assert!(!Reflect::has(&webapp, &"themeChanged".into()).unwrap());
863    }
864
865    #[wasm_bindgen_test]
866    #[allow(dead_code, clippy::unused_unit)]
867    fn safe_area_changed_register_and_remove() {
868        let webapp = setup_webapp();
869        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
870        let off_event = Function::new_with_args("name", "delete this[name];");
871        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
872        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
873
874        let app = TelegramWebApp::instance().unwrap();
875        let handle = app.on_safe_area_changed(|| {}).unwrap();
876        assert!(Reflect::has(&webapp, &"safeAreaChanged".into()).unwrap());
877        app.off_event(handle).unwrap();
878        assert!(!Reflect::has(&webapp, &"safeAreaChanged".into()).unwrap());
879    }
880
881    #[wasm_bindgen_test]
882    #[allow(dead_code, clippy::unused_unit)]
883    fn content_safe_area_changed_register_and_remove() {
884        let webapp = setup_webapp();
885        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
886        let off_event = Function::new_with_args("name", "delete this[name];");
887        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
888        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
889
890        let app = TelegramWebApp::instance().unwrap();
891        let handle = app.on_content_safe_area_changed(|| {}).unwrap();
892        assert!(Reflect::has(&webapp, &"contentSafeAreaChanged".into()).unwrap());
893        app.off_event(handle).unwrap();
894        assert!(!Reflect::has(&webapp, &"contentSafeAreaChanged".into()).unwrap());
895    }
896
897    #[wasm_bindgen_test]
898    #[allow(dead_code, clippy::unused_unit)]
899    fn viewport_changed_register_and_remove() {
900        let webapp = setup_webapp();
901        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
902        let off_event = Function::new_with_args("name", "delete this[name];");
903        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
904        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
905
906        let app = TelegramWebApp::instance().unwrap();
907        let handle = app.on_viewport_changed(|| {}).unwrap();
908        assert!(Reflect::has(&webapp, &"viewportChanged".into()).unwrap());
909        app.off_event(handle).unwrap();
910        assert!(!Reflect::has(&webapp, &"viewportChanged".into()).unwrap());
911    }
912
913    #[wasm_bindgen_test]
914    #[allow(dead_code, clippy::unused_unit)]
915    fn clipboard_text_received_register_and_remove() {
916        let webapp = setup_webapp();
917        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
918        let off_event = Function::new_with_args("name", "delete this[name];");
919        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
920        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
921
922        let app = TelegramWebApp::instance().unwrap();
923        let handle = app.on_clipboard_text_received(|_| {}).unwrap();
924        assert!(Reflect::has(&webapp, &"clipboardTextReceived".into()).unwrap());
925        app.off_event(handle).unwrap();
926        assert!(!Reflect::has(&webapp, &"clipboardTextReceived".into()).unwrap());
927    }
928
929    #[wasm_bindgen_test]
930    #[allow(dead_code, clippy::unused_unit)]
931    fn open_link_and_telegram_link() {
932        let webapp = setup_webapp();
933        let open_link = Function::new_with_args("url", "this.open_link = url;");
934        let open_tg_link = Function::new_with_args("url", "this.open_tg_link = url;");
935        let _ = Reflect::set(&webapp, &"openLink".into(), &open_link);
936        let _ = Reflect::set(&webapp, &"openTelegramLink".into(), &open_tg_link);
937
938        let app = TelegramWebApp::instance().unwrap();
939        let url = "https://example.com";
940        app.open_link(url, None).unwrap();
941        app.open_telegram_link(url).unwrap();
942
943        assert_eq!(
944            Reflect::get(&webapp, &"open_link".into())
945                .unwrap()
946                .as_string()
947                .as_deref(),
948            Some(url)
949        );
950        assert_eq!(
951            Reflect::get(&webapp, &"open_tg_link".into())
952                .unwrap()
953                .as_string()
954                .as_deref(),
955            Some(url)
956        );
957    }
958
959    #[wasm_bindgen_test]
960    #[allow(dead_code, clippy::unused_unit)]
961    fn invoice_closed_register_and_remove() {
962        let webapp = setup_webapp();
963        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
964        let off_event = Function::new_with_args("name", "delete this[name];");
965        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
966        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
967
968        let app = TelegramWebApp::instance().unwrap();
969        let handle = app.on_invoice_closed(|_| {}).unwrap();
970        assert!(Reflect::has(&webapp, &"invoiceClosed".into()).unwrap());
971        app.off_event(handle).unwrap();
972        assert!(!Reflect::has(&webapp, &"invoiceClosed".into()).unwrap());
973    }
974
975    #[wasm_bindgen_test]
976    #[allow(dead_code, clippy::unused_unit)]
977    fn invoice_closed_invokes_callback() {
978        let webapp = setup_webapp();
979        let on_event = Function::new_with_args("name, cb", "this.cb = cb;");
980        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
981
982        let app = TelegramWebApp::instance().unwrap();
983        let status = Rc::new(RefCell::new(String::new()));
984        let status_clone = Rc::clone(&status);
985        app.on_invoice_closed(move |s| {
986            *status_clone.borrow_mut() = s;
987        })
988        .unwrap();
989
990        let cb = Reflect::get(&webapp, &"cb".into())
991            .unwrap()
992            .dyn_into::<Function>()
993            .unwrap();
994        cb.call1(&webapp, &"paid".into()).unwrap();
995        assert_eq!(status.borrow().as_str(), "paid");
996        cb.call1(&webapp, &"failed".into()).unwrap();
997        assert_eq!(status.borrow().as_str(), "failed");
998    }
999
1000    #[wasm_bindgen_test]
1001    #[allow(dead_code, clippy::unused_unit)]
1002    fn open_invoice_invokes_callback() {
1003        let webapp = setup_webapp();
1004        let open_invoice = Function::new_with_args("url, cb", "cb('paid');");
1005        let _ = Reflect::set(&webapp, &"openInvoice".into(), &open_invoice);
1006
1007        let app = TelegramWebApp::instance().unwrap();
1008        let status = Rc::new(RefCell::new(String::new()));
1009        let status_clone = Rc::clone(&status);
1010
1011        app.open_invoice_with_callback("https://invoice", move |s| {
1012            *status_clone.borrow_mut() = s;
1013        })
1014        .unwrap();
1015
1016        assert_eq!(status.borrow().as_str(), "paid");
1017    }
1018
1019    #[wasm_bindgen_test]
1020    #[allow(dead_code, clippy::unused_unit)]
1021    fn switch_inline_query_calls_js() {
1022        let webapp = setup_webapp();
1023        let switch_inline =
1024            Function::new_with_args("query, types", "this.query = query; this.types = types;");
1025        let _ = Reflect::set(&webapp, &"switchInlineQuery".into(), &switch_inline);
1026
1027        let app = TelegramWebApp::instance().unwrap();
1028        let types = JsValue::from_str("users");
1029        app.switch_inline_query("search", Some(&types)).unwrap();
1030
1031        assert_eq!(
1032            Reflect::get(&webapp, &"query".into())
1033                .unwrap()
1034                .as_string()
1035                .as_deref(),
1036            Some("search"),
1037        );
1038        assert_eq!(
1039            Reflect::get(&webapp, &"types".into())
1040                .unwrap()
1041                .as_string()
1042                .as_deref(),
1043            Some("users"),
1044        );
1045    }
1046
1047    #[wasm_bindgen_test]
1048    #[allow(dead_code, clippy::unused_unit)]
1049    fn share_message_calls_js() {
1050        let webapp = setup_webapp();
1051        let share = Function::new_with_args("id, cb", "this.shared_id = id; cb(true);");
1052        let _ = Reflect::set(&webapp, &"shareMessage".into(), &share);
1053
1054        let app = TelegramWebApp::instance().unwrap();
1055        let sent = Rc::new(Cell::new(false));
1056        let sent_clone = Rc::clone(&sent);
1057
1058        app.share_message_with_callback("123", move |s| {
1059            sent_clone.set(s);
1060        })
1061        .unwrap();
1062
1063        assert_eq!(
1064            Reflect::get(&webapp, &"shared_id".into())
1065                .unwrap()
1066                .as_string()
1067                .as_deref(),
1068            Some("123"),
1069        );
1070        assert!(sent.get());
1071    }
1072
1073    #[wasm_bindgen_test]
1074    #[allow(dead_code, clippy::unused_unit)]
1075    fn share_to_story_calls_js() {
1076        let webapp = setup_webapp();
1077        let share = Function::new_with_args(
1078            "url, params",
1079            "this.story_url = url; this.story_params = params;"
1080        );
1081        let _ = Reflect::set(&webapp, &"shareToStory".into(), &share);
1082
1083        let app = TelegramWebApp::instance().unwrap();
1084        let url = "https://example.com/media";
1085        let params = Object::new();
1086        let _ = Reflect::set(&params, &"text".into(), &"hi".into());
1087        app.share_to_story(url, Some(&params.into())).unwrap();
1088
1089        assert_eq!(
1090            Reflect::get(&webapp, &"story_url".into())
1091                .unwrap()
1092                .as_string()
1093                .as_deref(),
1094            Some(url),
1095        );
1096        let stored = Reflect::get(&webapp, &"story_params".into()).unwrap();
1097        assert_eq!(
1098            Reflect::get(&stored, &"text".into())
1099                .unwrap()
1100                .as_string()
1101                .as_deref(),
1102            Some("hi"),
1103        );
1104    }
1105
1106    #[wasm_bindgen_test]
1107    #[allow(dead_code, clippy::unused_unit)]
1108    fn share_url_calls_js() {
1109        let webapp = setup_webapp();
1110        let share = Function::new_with_args(
1111            "url, text",
1112            "this.shared_url = url; this.shared_text = text;"
1113        );
1114        let _ = Reflect::set(&webapp, &"shareURL".into(), &share);
1115
1116        let app = TelegramWebApp::instance().unwrap();
1117        let url = "https://example.com";
1118        let text = "check";
1119        app.share_url(url, Some(text)).unwrap();
1120
1121        assert_eq!(
1122            Reflect::get(&webapp, &"shared_url".into())
1123                .unwrap()
1124                .as_string()
1125                .as_deref(),
1126            Some(url),
1127        );
1128        assert_eq!(
1129            Reflect::get(&webapp, &"shared_text".into())
1130                .unwrap()
1131                .as_string()
1132                .as_deref(),
1133            Some(text),
1134        );
1135    }
1136
1137    #[wasm_bindgen_test]
1138    #[allow(dead_code, clippy::unused_unit)]
1139    fn request_chat_calls_js() {
1140        let webapp = setup_webapp();
1141        let request =
1142            Function::new_with_args("req_id, cb", "this.req_chat_id = req_id; cb(true);");
1143        let _ = Reflect::set(&webapp, &"requestChat".into(), &request);
1144
1145        let app = TelegramWebApp::instance().unwrap();
1146        let sent = std::rc::Rc::new(std::cell::Cell::new(false));
1147        let sent_ref = sent.clone();
1148        app.request_chat_with_callback(42, move |s| sent_ref.set(s))
1149            .unwrap();
1150
1151        assert_eq!(
1152            Reflect::get(&webapp, &"req_chat_id".into())
1153                .unwrap()
1154                .as_f64(),
1155            Some(42.0),
1156        );
1157        assert!(sent.get());
1158    }
1159
1160    #[wasm_bindgen_test]
1161    #[allow(dead_code, clippy::unused_unit)]
1162    fn add_to_home_screen_calls_js() {
1163        let webapp = setup_webapp();
1164        let add = Function::new_with_args("", "this.called = true; return true;");
1165        let _ = Reflect::set(&webapp, &"addToHomeScreen".into(), &add);
1166
1167        let app = TelegramWebApp::instance().unwrap();
1168        let shown = app.add_to_home_screen().unwrap();
1169        assert!(shown);
1170        let called = Reflect::get(&webapp, &"called".into())
1171            .unwrap()
1172            .as_bool()
1173            .unwrap_or(false);
1174        assert!(called);
1175    }
1176
1177    #[wasm_bindgen_test]
1178    #[allow(dead_code, clippy::unused_unit)]
1179    fn request_fullscreen_calls_js() {
1180        let webapp = setup_webapp();
1181        let called = Rc::new(Cell::new(false));
1182        let called_clone = Rc::clone(&called);
1183
1184        let cb = Closure::<dyn FnMut()>::new(move || {
1185            called_clone.set(true);
1186        });
1187        let _ = Reflect::set(
1188            &webapp,
1189            &"requestFullscreen".into(),
1190            cb.as_ref().unchecked_ref()
1191        );
1192        cb.forget();
1193
1194        let app = TelegramWebApp::instance().unwrap();
1195        app.request_fullscreen().unwrap();
1196        assert!(called.get());
1197    }
1198
1199    #[wasm_bindgen_test]
1200    #[allow(dead_code, clippy::unused_unit)]
1201    fn exit_fullscreen_calls_js() {
1202        let webapp = setup_webapp();
1203        let called = Rc::new(Cell::new(false));
1204        let called_clone = Rc::clone(&called);
1205
1206        let cb = Closure::<dyn FnMut()>::new(move || {
1207            called_clone.set(true);
1208        });
1209        let _ = Reflect::set(
1210            &webapp,
1211            &"exitFullscreen".into(),
1212            cb.as_ref().unchecked_ref()
1213        );
1214        cb.forget();
1215
1216        let app = TelegramWebApp::instance().unwrap();
1217        app.exit_fullscreen().unwrap();
1218        assert!(called.get());
1219    }
1220
1221    #[wasm_bindgen_test]
1222    #[allow(dead_code, clippy::unused_unit)]
1223    fn check_home_screen_status_invokes_callback() {
1224        let webapp = setup_webapp();
1225        let check = Function::new_with_args("cb", "cb('added');");
1226        let _ = Reflect::set(&webapp, &"checkHomeScreenStatus".into(), &check);
1227
1228        let app = TelegramWebApp::instance().unwrap();
1229        let status = Rc::new(RefCell::new(String::new()));
1230        let status_clone = Rc::clone(&status);
1231
1232        app.check_home_screen_status_with_callback(move |s| {
1233            *status_clone.borrow_mut() = s;
1234        })
1235        .unwrap();
1236
1237        assert_eq!(status.borrow().as_str(), "added");
1238    }
1239
1240    #[wasm_bindgen_test]
1241    #[allow(dead_code, clippy::unused_unit)]
1242    fn lock_orientation_calls_js() {
1243        let webapp = setup_webapp();
1244        let received = Rc::new(RefCell::new(None));
1245        let rc_clone = Rc::clone(&received);
1246
1247        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
1248            *rc_clone.borrow_mut() = v.as_string();
1249        });
1250        let _ = Reflect::set(
1251            &webapp,
1252            &"lockOrientation".into(),
1253            cb.as_ref().unchecked_ref()
1254        );
1255        cb.forget();
1256
1257        let app = TelegramWebApp::instance().unwrap();
1258        app.lock_orientation("portrait").unwrap();
1259        assert_eq!(received.borrow().as_deref(), Some("portrait"));
1260    }
1261
1262    #[wasm_bindgen_test]
1263    #[allow(dead_code, clippy::unused_unit)]
1264    fn unlock_orientation_calls_js() {
1265        let webapp = setup_webapp();
1266        let called = Rc::new(Cell::new(false));
1267        let called_clone = Rc::clone(&called);
1268
1269        let cb = Closure::<dyn FnMut()>::new(move || {
1270            called_clone.set(true);
1271        });
1272        let _ = Reflect::set(
1273            &webapp,
1274            &"unlockOrientation".into(),
1275            cb.as_ref().unchecked_ref()
1276        );
1277        cb.forget();
1278
1279        let app = TelegramWebApp::instance().unwrap();
1280        app.unlock_orientation().unwrap();
1281        assert!(called.get());
1282    }
1283
1284    #[wasm_bindgen_test]
1285    #[allow(dead_code, clippy::unused_unit)]
1286    fn enable_vertical_swipes_calls_js() {
1287        let webapp = setup_webapp();
1288        let called = Rc::new(Cell::new(false));
1289        let called_clone = Rc::clone(&called);
1290
1291        let cb = Closure::<dyn FnMut()>::new(move || {
1292            called_clone.set(true);
1293        });
1294        let _ = Reflect::set(
1295            &webapp,
1296            &"enableVerticalSwipes".into(),
1297            cb.as_ref().unchecked_ref()
1298        );
1299        cb.forget();
1300
1301        let app = TelegramWebApp::instance().unwrap();
1302        app.enable_vertical_swipes().unwrap();
1303        assert!(called.get());
1304    }
1305
1306    #[wasm_bindgen_test]
1307    #[allow(dead_code, clippy::unused_unit)]
1308    fn disable_vertical_swipes_calls_js() {
1309        let webapp = setup_webapp();
1310        let called = Rc::new(Cell::new(false));
1311        let called_clone = Rc::clone(&called);
1312
1313        let cb = Closure::<dyn FnMut()>::new(move || {
1314            called_clone.set(true);
1315        });
1316        let _ = Reflect::set(
1317            &webapp,
1318            &"disableVerticalSwipes".into(),
1319            cb.as_ref().unchecked_ref()
1320        );
1321        cb.forget();
1322
1323        let app = TelegramWebApp::instance().unwrap();
1324        app.disable_vertical_swipes().unwrap();
1325        assert!(called.get());
1326    }
1327
1328    #[wasm_bindgen_test]
1329    #[allow(dead_code, clippy::unused_unit)]
1330    fn request_write_access_invokes_callback() {
1331        let webapp = setup_webapp();
1332        let request = Function::new_with_args("cb", "cb(true);");
1333        let _ = Reflect::set(&webapp, &"requestWriteAccess".into(), &request);
1334
1335        let app = TelegramWebApp::instance().unwrap();
1336        let granted = Rc::new(Cell::new(false));
1337        let granted_clone = Rc::clone(&granted);
1338
1339        let res = app.request_write_access_with_callback(move |g| {
1340            granted_clone.set(g);
1341        });
1342        assert!(res.is_ok());
1343
1344        assert!(granted.get());
1345    }
1346
1347    #[wasm_bindgen_test]
1348    #[allow(dead_code, clippy::unused_unit)]
1349    fn download_file_invokes_callback() {
1350        let webapp = setup_webapp();
1351        let received_url = Rc::new(RefCell::new(String::new()));
1352        let received_name = Rc::new(RefCell::new(String::new()));
1353        let url_clone = Rc::clone(&received_url);
1354        let name_clone = Rc::clone(&received_name);
1355
1356        let download = Closure::<dyn FnMut(JsValue, JsValue)>::new(move |params, cb: JsValue| {
1357            let url = Reflect::get(&params, &"url".into())
1358                .unwrap()
1359                .as_string()
1360                .unwrap_or_default();
1361            let name = Reflect::get(&params, &"file_name".into())
1362                .unwrap()
1363                .as_string()
1364                .unwrap_or_default();
1365            *url_clone.borrow_mut() = url;
1366            *name_clone.borrow_mut() = name;
1367            let func = cb.dyn_ref::<Function>().unwrap();
1368            let _ = func.call1(&JsValue::NULL, &JsValue::from_str("id"));
1369        });
1370        let _ = Reflect::set(
1371            &webapp,
1372            &"downloadFile".into(),
1373            download.as_ref().unchecked_ref()
1374        );
1375        download.forget();
1376
1377        let app = TelegramWebApp::instance().unwrap();
1378        let result = Rc::new(RefCell::new(String::new()));
1379        let result_clone = Rc::clone(&result);
1380        let params = DownloadFileParams {
1381            url:       "https://example.com/data.bin",
1382            file_name: Some("data.bin"),
1383            mime_type: None
1384        };
1385        app.download_file_with_callback(params, move |id| {
1386            *result_clone.borrow_mut() = id;
1387        })
1388        .unwrap();
1389
1390        assert_eq!(
1391            received_url.borrow().as_str(),
1392            "https://example.com/data.bin"
1393        );
1394        assert_eq!(received_name.borrow().as_str(), "data.bin");
1395        assert_eq!(result.borrow().as_str(), "id");
1396    }
1397
1398    #[wasm_bindgen_test]
1399    #[allow(dead_code, clippy::unused_unit)]
1400    fn request_write_access_returns_error_when_missing() {
1401        let _webapp = setup_webapp();
1402        let app = TelegramWebApp::instance().unwrap();
1403        let res = app.request_write_access_with_callback(|_| {});
1404        assert!(res.is_err());
1405    }
1406    #[wasm_bindgen_test]
1407    #[allow(dead_code, clippy::unused_unit)]
1408    fn request_emoji_status_access_invokes_callback() {
1409        let webapp = setup_webapp();
1410        let request = Function::new_with_args("cb", "cb(false);");
1411        let _ = Reflect::set(&webapp, &"requestEmojiStatusAccess".into(), &request);
1412
1413        let app = TelegramWebApp::instance().unwrap();
1414        let granted = Rc::new(Cell::new(true));
1415        let granted_clone = Rc::clone(&granted);
1416
1417        app.request_emoji_status_access_with_callback(move |g| {
1418            granted_clone.set(g);
1419        })
1420        .unwrap();
1421
1422        assert!(!granted.get());
1423    }
1424
1425    #[wasm_bindgen_test]
1426    #[allow(dead_code, clippy::unused_unit)]
1427    fn set_emoji_status_invokes_callback() {
1428        let webapp = setup_webapp();
1429        let set_status = Function::new_with_args("status, cb", "this.st = status; cb(true);");
1430        let _ = Reflect::set(&webapp, &"setEmojiStatus".into(), &set_status);
1431
1432        let status = Object::new();
1433        let _ = Reflect::set(
1434            &status,
1435            &"custom_emoji_id".into(),
1436            &JsValue::from_str("321")
1437        );
1438
1439        let app = TelegramWebApp::instance().unwrap();
1440        let success = Rc::new(Cell::new(false));
1441        let success_clone = Rc::clone(&success);
1442
1443        app.set_emoji_status_with_callback(&status.into(), move |s| {
1444            success_clone.set(s);
1445        })
1446        .unwrap();
1447
1448        assert!(success.get());
1449        let stored = Reflect::get(&webapp, &"st".into()).unwrap();
1450        let id = Reflect::get(&stored, &"custom_emoji_id".into())
1451            .unwrap()
1452            .as_string();
1453        assert_eq!(id.as_deref(), Some("321"));
1454    }
1455
1456    #[wasm_bindgen_test]
1457    #[allow(dead_code, clippy::unused_unit)]
1458    fn show_popup_invokes_callback() {
1459        let webapp = setup_webapp();
1460        let show_popup = Function::new_with_args("params, cb", "cb('ok');");
1461        let _ = Reflect::set(&webapp, &"showPopup".into(), &show_popup);
1462
1463        let app = TelegramWebApp::instance().unwrap();
1464        let button = Rc::new(RefCell::new(String::new()));
1465        let button_clone = Rc::clone(&button);
1466
1467        app.show_popup_with_callback(&JsValue::NULL, move |id| {
1468            *button_clone.borrow_mut() = id;
1469        })
1470        .unwrap();
1471
1472        assert_eq!(button.borrow().as_str(), "ok");
1473    }
1474
1475    #[wasm_bindgen_test]
1476    #[allow(dead_code, clippy::unused_unit)]
1477    fn read_text_from_clipboard_invokes_callback() {
1478        let webapp = setup_webapp();
1479        let read_clip = Function::new_with_args("cb", "cb('clip');");
1480        let _ = Reflect::set(&webapp, &"readTextFromClipboard".into(), &read_clip);
1481
1482        let app = TelegramWebApp::instance().unwrap();
1483        let text = Rc::new(RefCell::new(String::new()));
1484        let text_clone = Rc::clone(&text);
1485
1486        app.read_text_from_clipboard_with_callback(move |t| {
1487            *text_clone.borrow_mut() = t;
1488        })
1489        .unwrap();
1490
1491        assert_eq!(text.borrow().as_str(), "clip");
1492    }
1493
1494    #[wasm_bindgen_test]
1495    #[allow(dead_code, clippy::unused_unit)]
1496    fn scan_qr_popup_invokes_callback_and_close() {
1497        let webapp = setup_webapp();
1498        let show_scan = Function::new_with_args("text, cb", "cb('code');");
1499        let close_scan = Function::new_with_args("", "this.closed = true;");
1500        let _ = Reflect::set(&webapp, &"showScanQrPopup".into(), &show_scan);
1501        let _ = Reflect::set(&webapp, &"closeScanQrPopup".into(), &close_scan);
1502
1503        let app = TelegramWebApp::instance().unwrap();
1504        let text = Rc::new(RefCell::new(String::new()));
1505        let text_clone = Rc::clone(&text);
1506
1507        app.show_scan_qr_popup_with_callback("scan", move |value| {
1508            *text_clone.borrow_mut() = value;
1509        })
1510        .unwrap();
1511        assert_eq!(text.borrow().as_str(), "code");
1512
1513        app.close_scan_qr_popup().unwrap();
1514        let closed = Reflect::get(&webapp, &"closed".into())
1515            .unwrap()
1516            .as_bool()
1517            .unwrap_or(false);
1518        assert!(closed);
1519    }
1520}