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