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        };
306        app.set_bottom_button_params(BottomButton::Main, &params)
307            .unwrap();
308
309        let stored = received.borrow();
310        assert_eq!(
311            Reflect::get(&stored, &"text".into()).unwrap().as_string(),
312            Some("Send".to_string())
313        );
314        assert_eq!(
315            Reflect::get(&stored, &"color".into()).unwrap().as_string(),
316            Some("#ffffff".to_string())
317        );
318        assert_eq!(
319            Reflect::get(&stored, &"text_color".into())
320                .unwrap()
321                .as_string(),
322            Some("#000000".to_string())
323        );
324    }
325
326    #[wasm_bindgen_test]
327    #[allow(dead_code, clippy::unused_unit)]
328    fn set_secondary_button_params_serializes_position() {
329        let webapp = setup_webapp();
330        let button = Object::new();
331        let received = Rc::new(RefCell::new(Object::new()));
332        let rc_clone = Rc::clone(&received);
333
334        let cb = Closure::<dyn FnMut(JsValue)>::new(move |value: JsValue| {
335            let obj = value.dyn_into::<Object>().expect("object");
336            rc_clone.replace(obj);
337        });
338        let _ = Reflect::set(&button, &"setParams".into(), cb.as_ref().unchecked_ref());
339        cb.forget();
340
341        let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &button);
342
343        let app = TelegramWebApp::instance().unwrap();
344        let params = SecondaryButtonParams {
345            common:   BottomButtonParams {
346                text: Some("Next"),
347                ..Default::default()
348            },
349            position: Some(SecondaryButtonPosition::Left)
350        };
351        app.set_secondary_button_params(&params).unwrap();
352
353        let stored = received.borrow();
354        assert_eq!(
355            Reflect::get(&stored, &"text".into()).unwrap().as_string(),
356            Some("Next".to_string())
357        );
358        assert_eq!(
359            Reflect::get(&stored, &"position".into())
360                .unwrap()
361                .as_string(),
362            Some("left".to_string())
363        );
364    }
365
366    #[wasm_bindgen_test]
367    #[allow(dead_code, clippy::unused_unit)]
368    fn bottom_button_getters_return_values() {
369        let webapp = setup_webapp();
370        let button = Object::new();
371        let _ = Reflect::set(&button, &"text".into(), &"Label".into());
372        let _ = Reflect::set(&button, &"textColor".into(), &"#111111".into());
373        let _ = Reflect::set(&button, &"color".into(), &"#222222".into());
374        let _ = Reflect::set(&button, &"isVisible".into(), &JsValue::TRUE);
375        let _ = Reflect::set(&button, &"isActive".into(), &JsValue::TRUE);
376        let _ = Reflect::set(&button, &"isProgressVisible".into(), &JsValue::FALSE);
377        let _ = Reflect::set(&button, &"hasShineEffect".into(), &JsValue::TRUE);
378
379        let _ = Reflect::set(&webapp, &"MainButton".into(), &button);
380
381        let app = TelegramWebApp::instance().unwrap();
382        assert_eq!(
383            app.bottom_button_text(BottomButton::Main),
384            Some("Label".into())
385        );
386        assert_eq!(
387            app.bottom_button_text_color(BottomButton::Main),
388            Some("#111111".into())
389        );
390        assert_eq!(
391            app.bottom_button_color(BottomButton::Main),
392            Some("#222222".into())
393        );
394        assert!(app.is_bottom_button_visible(BottomButton::Main));
395        assert!(app.is_bottom_button_active(BottomButton::Main));
396        assert!(!app.is_bottom_button_progress_visible(BottomButton::Main));
397        assert!(app.bottom_button_has_shine_effect(BottomButton::Main));
398    }
399
400    #[wasm_bindgen_test]
401    #[allow(dead_code, clippy::unused_unit)]
402    fn secondary_button_position_is_parsed() {
403        let webapp = setup_webapp();
404        let button = Object::new();
405        let _ = Reflect::set(&button, &"position".into(), &"right".into());
406        let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &button);
407
408        let app = TelegramWebApp::instance().unwrap();
409        assert_eq!(
410            app.secondary_button_position(),
411            Some(SecondaryButtonPosition::Right)
412        );
413    }
414
415    #[wasm_bindgen_test]
416    #[allow(dead_code, clippy::unused_unit)]
417    fn set_header_color_calls_js() {
418        let webapp = setup_webapp();
419        let received = Rc::new(RefCell::new(None));
420        let rc_clone = Rc::clone(&received);
421
422        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
423            *rc_clone.borrow_mut() = v.as_string();
424        });
425        let _ = Reflect::set(
426            &webapp,
427            &"setHeaderColor".into(),
428            cb.as_ref().unchecked_ref()
429        );
430        cb.forget();
431
432        let app = TelegramWebApp::instance().unwrap();
433        app.set_header_color("#abcdef").unwrap();
434        assert_eq!(received.borrow().as_deref(), Some("#abcdef"));
435    }
436
437    #[wasm_bindgen_test]
438    #[allow(dead_code, clippy::unused_unit)]
439    fn set_background_color_calls_js() {
440        let webapp = setup_webapp();
441        let received = Rc::new(RefCell::new(None));
442        let rc_clone = Rc::clone(&received);
443
444        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
445            *rc_clone.borrow_mut() = v.as_string();
446        });
447        let _ = Reflect::set(
448            &webapp,
449            &"setBackgroundColor".into(),
450            cb.as_ref().unchecked_ref()
451        );
452        cb.forget();
453
454        let app = TelegramWebApp::instance().unwrap();
455        app.set_background_color("#123456").unwrap();
456        assert_eq!(received.borrow().as_deref(), Some("#123456"));
457    }
458
459    #[wasm_bindgen_test]
460    #[allow(dead_code, clippy::unused_unit)]
461    fn set_bottom_bar_color_calls_js() {
462        let webapp = setup_webapp();
463        let received = Rc::new(RefCell::new(None));
464        let rc_clone = Rc::clone(&received);
465
466        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
467            *rc_clone.borrow_mut() = v.as_string();
468        });
469        let _ = Reflect::set(
470            &webapp,
471            &"setBottomBarColor".into(),
472            cb.as_ref().unchecked_ref()
473        );
474        cb.forget();
475
476        let app = TelegramWebApp::instance().unwrap();
477        app.set_bottom_bar_color("#654321").unwrap();
478        assert_eq!(received.borrow().as_deref(), Some("#654321"));
479    }
480
481    #[wasm_bindgen_test]
482    #[allow(dead_code, clippy::unused_unit)]
483    fn viewport_dimensions() {
484        let webapp = setup_webapp();
485        let _ = Reflect::set(&webapp, &"viewportWidth".into(), &JsValue::from_f64(320.0));
486        let _ = Reflect::set(
487            &webapp,
488            &"viewportStableHeight".into(),
489            &JsValue::from_f64(480.0)
490        );
491        let app = TelegramWebApp::instance().unwrap();
492        assert_eq!(app.viewport_width(), Some(320.0));
493        assert_eq!(app.viewport_stable_height(), Some(480.0));
494    }
495
496    #[wasm_bindgen_test]
497    #[allow(dead_code, clippy::unused_unit)]
498    fn version_check_invokes_js() {
499        let webapp = setup_webapp();
500        let cb = Function::new_with_args("v", "return v === '9.0';");
501        let _ = Reflect::set(&webapp, &"isVersionAtLeast".into(), &cb);
502
503        let app = TelegramWebApp::instance().unwrap();
504        assert!(app.is_version_at_least("9.0").unwrap());
505        assert!(!app.is_version_at_least("9.1").unwrap());
506    }
507
508    #[wasm_bindgen_test]
509    #[allow(dead_code, clippy::unused_unit)]
510    fn safe_area_insets_are_parsed() {
511        let webapp = setup_webapp();
512        let safe_area = Object::new();
513        let _ = Reflect::set(&safe_area, &"top".into(), &JsValue::from_f64(1.0));
514        let _ = Reflect::set(&safe_area, &"bottom".into(), &JsValue::from_f64(2.0));
515        let _ = Reflect::set(&safe_area, &"left".into(), &JsValue::from_f64(3.0));
516        let _ = Reflect::set(&safe_area, &"right".into(), &JsValue::from_f64(4.0));
517        let _ = Reflect::set(&webapp, &"safeAreaInset".into(), &safe_area);
518
519        let content_safe = Object::new();
520        let _ = Reflect::set(&content_safe, &"top".into(), &JsValue::from_f64(5.0));
521        let _ = Reflect::set(&content_safe, &"bottom".into(), &JsValue::from_f64(6.0));
522        let _ = Reflect::set(&content_safe, &"left".into(), &JsValue::from_f64(7.0));
523        let _ = Reflect::set(&content_safe, &"right".into(), &JsValue::from_f64(8.0));
524        let _ = Reflect::set(&webapp, &"contentSafeAreaInset".into(), &content_safe);
525
526        let app = TelegramWebApp::instance().unwrap();
527        let inset = app.safe_area_inset().expect("safe area");
528        assert_eq!(inset.top, 1.0);
529        assert_eq!(inset.bottom, 2.0);
530        assert_eq!(inset.left, 3.0);
531        assert_eq!(inset.right, 4.0);
532
533        let content = app.content_safe_area_inset().expect("content area");
534        assert_eq!(content.top, 5.0);
535    }
536
537    #[wasm_bindgen_test]
538    #[allow(dead_code, clippy::unused_unit)]
539    fn activity_flags_are_reported() {
540        let webapp = setup_webapp();
541        let _ = Reflect::set(&webapp, &"isActive".into(), &JsValue::TRUE);
542        let _ = Reflect::set(&webapp, &"isFullscreen".into(), &JsValue::TRUE);
543        let _ = Reflect::set(&webapp, &"isOrientationLocked".into(), &JsValue::FALSE);
544        let _ = Reflect::set(&webapp, &"isVerticalSwipesEnabled".into(), &JsValue::TRUE);
545
546        let app = TelegramWebApp::instance().unwrap();
547        assert!(app.is_active());
548        assert!(app.is_fullscreen());
549        assert!(!app.is_orientation_locked());
550        assert!(app.is_vertical_swipes_enabled());
551    }
552
553    #[wasm_bindgen_test]
554    #[allow(dead_code, clippy::unused_unit)]
555    fn back_button_visibility_and_callback() {
556        let webapp = setup_webapp();
557        let back_button = Object::new();
558        let _ = Reflect::set(&webapp, &"BackButton".into(), &back_button);
559        let _ = Reflect::set(&back_button, &"isVisible".into(), &JsValue::TRUE);
560
561        let on_click = Function::new_with_args("cb", "this.cb = cb;");
562        let off_click = Function::new_with_args("", "delete this.cb;");
563        let _ = Reflect::set(&back_button, &"onClick".into(), &on_click);
564        let _ = Reflect::set(&back_button, &"offClick".into(), &off_click);
565
566        let called = Rc::new(Cell::new(false));
567        let called_clone = Rc::clone(&called);
568
569        let app = TelegramWebApp::instance().unwrap();
570        assert!(app.is_back_button_visible());
571        let handle = app
572            .set_back_button_callback(move || {
573                called_clone.set(true);
574            })
575            .unwrap();
576
577        let stored = Reflect::has(&back_button, &"cb".into()).unwrap();
578        assert!(stored);
579
580        let cb_fn = Reflect::get(&back_button, &"cb".into())
581            .unwrap()
582            .dyn_into::<Function>()
583            .unwrap();
584        let _ = cb_fn.call0(&JsValue::NULL);
585        assert!(called.get());
586
587        app.remove_back_button_callback(handle).unwrap();
588        let stored_after = Reflect::has(&back_button, &"cb".into()).unwrap();
589        assert!(!stored_after);
590    }
591
592    #[wasm_bindgen_test]
593    #[allow(dead_code, clippy::unused_unit)]
594    fn bottom_button_callback_register_and_remove() {
595        let webapp = setup_webapp();
596        let main_button = Object::new();
597        let _ = Reflect::set(&webapp, &"MainButton".into(), &main_button);
598
599        let on_click = Function::new_with_args("cb", "this.cb = cb;");
600        let off_click = Function::new_with_args("", "delete this.cb;");
601        let _ = Reflect::set(&main_button, &"onClick".into(), &on_click);
602        let _ = Reflect::set(&main_button, &"offClick".into(), &off_click);
603
604        let called = Rc::new(Cell::new(false));
605        let called_clone = Rc::clone(&called);
606
607        let app = TelegramWebApp::instance().unwrap();
608        let handle = app
609            .set_bottom_button_callback(BottomButton::Main, move || {
610                called_clone.set(true);
611            })
612            .unwrap();
613
614        let stored = Reflect::has(&main_button, &"cb".into()).unwrap();
615        assert!(stored);
616
617        let cb_fn = Reflect::get(&main_button, &"cb".into())
618            .unwrap()
619            .dyn_into::<Function>()
620            .unwrap();
621        let _ = cb_fn.call0(&JsValue::NULL);
622        assert!(called.get());
623
624        app.remove_bottom_button_callback(handle).unwrap();
625        let stored_after = Reflect::has(&main_button, &"cb".into()).unwrap();
626        assert!(!stored_after);
627    }
628
629    #[wasm_bindgen_test]
630    #[allow(dead_code, clippy::unused_unit)]
631    fn secondary_button_callback_register_and_remove() {
632        let webapp = setup_webapp();
633        let secondary_button = Object::new();
634        let _ = Reflect::set(&webapp, &"SecondaryButton".into(), &secondary_button);
635
636        let on_click = Function::new_with_args("cb", "this.cb = cb;");
637        let off_click = Function::new_with_args("", "delete this.cb;");
638        let _ = Reflect::set(&secondary_button, &"onClick".into(), &on_click);
639        let _ = Reflect::set(&secondary_button, &"offClick".into(), &off_click);
640
641        let called = Rc::new(Cell::new(false));
642        let called_clone = Rc::clone(&called);
643
644        let app = TelegramWebApp::instance().unwrap();
645        let handle = app
646            .set_bottom_button_callback(BottomButton::Secondary, move || {
647                called_clone.set(true);
648            })
649            .unwrap();
650
651        let stored = Reflect::has(&secondary_button, &"cb".into()).unwrap();
652        assert!(stored);
653
654        let cb_fn = Reflect::get(&secondary_button, &"cb".into())
655            .unwrap()
656            .dyn_into::<Function>()
657            .unwrap();
658        let _ = cb_fn.call0(&JsValue::NULL);
659        assert!(called.get());
660
661        app.remove_bottom_button_callback(handle).unwrap();
662        let stored_after = Reflect::has(&secondary_button, &"cb".into()).unwrap();
663        assert!(!stored_after);
664    }
665
666    #[wasm_bindgen_test]
667    #[allow(dead_code, clippy::unused_unit)]
668    fn on_event_register_and_remove() {
669        let webapp = setup_webapp();
670        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
671        let off_event = Function::new_with_args("name", "delete this[name];");
672        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
673        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
674
675        let app = TelegramWebApp::instance().unwrap();
676        let handle = app.on_event("test", |_: JsValue| {}).unwrap();
677        assert!(Reflect::has(&webapp, &"test".into()).unwrap());
678        app.off_event(handle).unwrap();
679        assert!(!Reflect::has(&webapp, &"test".into()).unwrap());
680    }
681
682    #[wasm_bindgen_test]
683    #[allow(dead_code, clippy::unused_unit)]
684    fn background_event_register_and_remove() {
685        let webapp = setup_webapp();
686        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
687        let off_event = Function::new_with_args("name", "delete this[name];");
688        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
689        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
690
691        let app = TelegramWebApp::instance().unwrap();
692        let handle = app
693            .on_background_event(BackgroundEvent::MainButtonClicked, |_| {})
694            .unwrap();
695        assert!(Reflect::has(&webapp, &"mainButtonClicked".into()).unwrap());
696        app.off_event(handle).unwrap();
697        assert!(!Reflect::has(&webapp, &"mainButtonClicked".into()).unwrap());
698    }
699
700    #[wasm_bindgen_test]
701    #[allow(dead_code, clippy::unused_unit)]
702    fn background_event_delivers_data() {
703        let webapp = setup_webapp();
704        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
705        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
706
707        let app = TelegramWebApp::instance().unwrap();
708        let received = Rc::new(RefCell::new(String::new()));
709        let received_clone = Rc::clone(&received);
710        let _handle = app
711            .on_background_event(BackgroundEvent::InvoiceClosed, move |v| {
712                *received_clone.borrow_mut() = v.as_string().unwrap_or_default();
713            })
714            .unwrap();
715
716        let cb = Reflect::get(&webapp, &"invoiceClosed".into())
717            .unwrap()
718            .dyn_into::<Function>()
719            .unwrap();
720        let _ = cb.call1(&JsValue::NULL, &JsValue::from_str("paid"));
721        assert_eq!(received.borrow().as_str(), "paid");
722    }
723
724    #[wasm_bindgen_test]
725    #[allow(dead_code, clippy::unused_unit)]
726    fn theme_changed_register_and_remove() {
727        let webapp = setup_webapp();
728        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
729        let off_event = Function::new_with_args("name", "delete this[name];");
730        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
731        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
732
733        let app = TelegramWebApp::instance().unwrap();
734        let handle = app.on_theme_changed(|| {}).unwrap();
735        assert!(Reflect::has(&webapp, &"themeChanged".into()).unwrap());
736        app.off_event(handle).unwrap();
737        assert!(!Reflect::has(&webapp, &"themeChanged".into()).unwrap());
738    }
739
740    #[wasm_bindgen_test]
741    #[allow(dead_code, clippy::unused_unit)]
742    fn safe_area_changed_register_and_remove() {
743        let webapp = setup_webapp();
744        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
745        let off_event = Function::new_with_args("name", "delete this[name];");
746        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
747        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
748
749        let app = TelegramWebApp::instance().unwrap();
750        let handle = app.on_safe_area_changed(|| {}).unwrap();
751        assert!(Reflect::has(&webapp, &"safeAreaChanged".into()).unwrap());
752        app.off_event(handle).unwrap();
753        assert!(!Reflect::has(&webapp, &"safeAreaChanged".into()).unwrap());
754    }
755
756    #[wasm_bindgen_test]
757    #[allow(dead_code, clippy::unused_unit)]
758    fn content_safe_area_changed_register_and_remove() {
759        let webapp = setup_webapp();
760        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
761        let off_event = Function::new_with_args("name", "delete this[name];");
762        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
763        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
764
765        let app = TelegramWebApp::instance().unwrap();
766        let handle = app.on_content_safe_area_changed(|| {}).unwrap();
767        assert!(Reflect::has(&webapp, &"contentSafeAreaChanged".into()).unwrap());
768        app.off_event(handle).unwrap();
769        assert!(!Reflect::has(&webapp, &"contentSafeAreaChanged".into()).unwrap());
770    }
771
772    #[wasm_bindgen_test]
773    #[allow(dead_code, clippy::unused_unit)]
774    fn viewport_changed_register_and_remove() {
775        let webapp = setup_webapp();
776        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
777        let off_event = Function::new_with_args("name", "delete this[name];");
778        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
779        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
780
781        let app = TelegramWebApp::instance().unwrap();
782        let handle = app.on_viewport_changed(|| {}).unwrap();
783        assert!(Reflect::has(&webapp, &"viewportChanged".into()).unwrap());
784        app.off_event(handle).unwrap();
785        assert!(!Reflect::has(&webapp, &"viewportChanged".into()).unwrap());
786    }
787
788    #[wasm_bindgen_test]
789    #[allow(dead_code, clippy::unused_unit)]
790    fn clipboard_text_received_register_and_remove() {
791        let webapp = setup_webapp();
792        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
793        let off_event = Function::new_with_args("name", "delete this[name];");
794        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
795        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
796
797        let app = TelegramWebApp::instance().unwrap();
798        let handle = app.on_clipboard_text_received(|_| {}).unwrap();
799        assert!(Reflect::has(&webapp, &"clipboardTextReceived".into()).unwrap());
800        app.off_event(handle).unwrap();
801        assert!(!Reflect::has(&webapp, &"clipboardTextReceived".into()).unwrap());
802    }
803
804    #[wasm_bindgen_test]
805    #[allow(dead_code, clippy::unused_unit)]
806    fn open_link_and_telegram_link() {
807        let webapp = setup_webapp();
808        let open_link = Function::new_with_args("url", "this.open_link = url;");
809        let open_tg_link = Function::new_with_args("url", "this.open_tg_link = url;");
810        let _ = Reflect::set(&webapp, &"openLink".into(), &open_link);
811        let _ = Reflect::set(&webapp, &"openTelegramLink".into(), &open_tg_link);
812
813        let app = TelegramWebApp::instance().unwrap();
814        let url = "https://example.com";
815        app.open_link(url, None).unwrap();
816        app.open_telegram_link(url).unwrap();
817
818        assert_eq!(
819            Reflect::get(&webapp, &"open_link".into())
820                .unwrap()
821                .as_string()
822                .as_deref(),
823            Some(url)
824        );
825        assert_eq!(
826            Reflect::get(&webapp, &"open_tg_link".into())
827                .unwrap()
828                .as_string()
829                .as_deref(),
830            Some(url)
831        );
832    }
833
834    #[wasm_bindgen_test]
835    #[allow(dead_code, clippy::unused_unit)]
836    fn invoice_closed_register_and_remove() {
837        let webapp = setup_webapp();
838        let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
839        let off_event = Function::new_with_args("name", "delete this[name];");
840        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
841        let _ = Reflect::set(&webapp, &"offEvent".into(), &off_event);
842
843        let app = TelegramWebApp::instance().unwrap();
844        let handle = app.on_invoice_closed(|_| {}).unwrap();
845        assert!(Reflect::has(&webapp, &"invoiceClosed".into()).unwrap());
846        app.off_event(handle).unwrap();
847        assert!(!Reflect::has(&webapp, &"invoiceClosed".into()).unwrap());
848    }
849
850    #[wasm_bindgen_test]
851    #[allow(dead_code, clippy::unused_unit)]
852    fn invoice_closed_invokes_callback() {
853        let webapp = setup_webapp();
854        let on_event = Function::new_with_args("name, cb", "this.cb = cb;");
855        let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
856
857        let app = TelegramWebApp::instance().unwrap();
858        let status = Rc::new(RefCell::new(String::new()));
859        let status_clone = Rc::clone(&status);
860        app.on_invoice_closed(move |s| {
861            *status_clone.borrow_mut() = s;
862        })
863        .unwrap();
864
865        let cb = Reflect::get(&webapp, &"cb".into())
866            .unwrap()
867            .dyn_into::<Function>()
868            .unwrap();
869        cb.call1(&webapp, &"paid".into()).unwrap();
870        assert_eq!(status.borrow().as_str(), "paid");
871        cb.call1(&webapp, &"failed".into()).unwrap();
872        assert_eq!(status.borrow().as_str(), "failed");
873    }
874
875    #[wasm_bindgen_test]
876    #[allow(dead_code, clippy::unused_unit)]
877    fn open_invoice_invokes_callback() {
878        let webapp = setup_webapp();
879        let open_invoice = Function::new_with_args("url, cb", "cb('paid');");
880        let _ = Reflect::set(&webapp, &"openInvoice".into(), &open_invoice);
881
882        let app = TelegramWebApp::instance().unwrap();
883        let status = Rc::new(RefCell::new(String::new()));
884        let status_clone = Rc::clone(&status);
885
886        app.open_invoice("https://invoice", move |s| {
887            *status_clone.borrow_mut() = s;
888        })
889        .unwrap();
890
891        assert_eq!(status.borrow().as_str(), "paid");
892    }
893
894    #[wasm_bindgen_test]
895    #[allow(dead_code, clippy::unused_unit)]
896    fn switch_inline_query_calls_js() {
897        let webapp = setup_webapp();
898        let switch_inline =
899            Function::new_with_args("query, types", "this.query = query; this.types = types;");
900        let _ = Reflect::set(&webapp, &"switchInlineQuery".into(), &switch_inline);
901
902        let app = TelegramWebApp::instance().unwrap();
903        let types = JsValue::from_str("users");
904        app.switch_inline_query("search", Some(&types)).unwrap();
905
906        assert_eq!(
907            Reflect::get(&webapp, &"query".into())
908                .unwrap()
909                .as_string()
910                .as_deref(),
911            Some("search"),
912        );
913        assert_eq!(
914            Reflect::get(&webapp, &"types".into())
915                .unwrap()
916                .as_string()
917                .as_deref(),
918            Some("users"),
919        );
920    }
921
922    #[wasm_bindgen_test]
923    #[allow(dead_code, clippy::unused_unit)]
924    fn share_message_calls_js() {
925        let webapp = setup_webapp();
926        let share = Function::new_with_args("id, cb", "this.shared_id = id; cb(true);");
927        let _ = Reflect::set(&webapp, &"shareMessage".into(), &share);
928
929        let app = TelegramWebApp::instance().unwrap();
930        let sent = Rc::new(Cell::new(false));
931        let sent_clone = Rc::clone(&sent);
932
933        app.share_message("123", move |s| {
934            sent_clone.set(s);
935        })
936        .unwrap();
937
938        assert_eq!(
939            Reflect::get(&webapp, &"shared_id".into())
940                .unwrap()
941                .as_string()
942                .as_deref(),
943            Some("123"),
944        );
945        assert!(sent.get());
946    }
947
948    #[wasm_bindgen_test]
949    #[allow(dead_code, clippy::unused_unit)]
950    fn share_to_story_calls_js() {
951        let webapp = setup_webapp();
952        let share = Function::new_with_args(
953            "url, params",
954            "this.story_url = url; this.story_params = params;"
955        );
956        let _ = Reflect::set(&webapp, &"shareToStory".into(), &share);
957
958        let app = TelegramWebApp::instance().unwrap();
959        let url = "https://example.com/media";
960        let params = Object::new();
961        let _ = Reflect::set(&params, &"text".into(), &"hi".into());
962        app.share_to_story(url, Some(&params.into())).unwrap();
963
964        assert_eq!(
965            Reflect::get(&webapp, &"story_url".into())
966                .unwrap()
967                .as_string()
968                .as_deref(),
969            Some(url),
970        );
971        let stored = Reflect::get(&webapp, &"story_params".into()).unwrap();
972        assert_eq!(
973            Reflect::get(&stored, &"text".into())
974                .unwrap()
975                .as_string()
976                .as_deref(),
977            Some("hi"),
978        );
979    }
980
981    #[wasm_bindgen_test]
982    #[allow(dead_code, clippy::unused_unit)]
983    fn share_url_calls_js() {
984        let webapp = setup_webapp();
985        let share = Function::new_with_args(
986            "url, text",
987            "this.shared_url = url; this.shared_text = text;"
988        );
989        let _ = Reflect::set(&webapp, &"shareURL".into(), &share);
990
991        let app = TelegramWebApp::instance().unwrap();
992        let url = "https://example.com";
993        let text = "check";
994        app.share_url(url, Some(text)).unwrap();
995
996        assert_eq!(
997            Reflect::get(&webapp, &"shared_url".into())
998                .unwrap()
999                .as_string()
1000                .as_deref(),
1001            Some(url),
1002        );
1003        assert_eq!(
1004            Reflect::get(&webapp, &"shared_text".into())
1005                .unwrap()
1006                .as_string()
1007                .as_deref(),
1008            Some(text),
1009        );
1010    }
1011
1012    #[wasm_bindgen_test]
1013    #[allow(dead_code, clippy::unused_unit)]
1014    fn join_voice_chat_calls_js() {
1015        let webapp = setup_webapp();
1016        let join = Function::new_with_args(
1017            "id, hash",
1018            "this.voice_chat_id = id; this.voice_chat_hash = hash;"
1019        );
1020        let _ = Reflect::set(&webapp, &"joinVoiceChat".into(), &join);
1021
1022        let app = TelegramWebApp::instance().unwrap();
1023        app.join_voice_chat("123", Some("hash")).unwrap();
1024
1025        assert_eq!(
1026            Reflect::get(&webapp, &"voice_chat_id".into())
1027                .unwrap()
1028                .as_string()
1029                .as_deref(),
1030            Some("123"),
1031        );
1032        assert_eq!(
1033            Reflect::get(&webapp, &"voice_chat_hash".into())
1034                .unwrap()
1035                .as_string()
1036                .as_deref(),
1037            Some("hash"),
1038        );
1039    }
1040
1041    #[wasm_bindgen_test]
1042    #[allow(dead_code, clippy::unused_unit)]
1043    fn add_to_home_screen_calls_js() {
1044        let webapp = setup_webapp();
1045        let add = Function::new_with_args("", "this.called = true; return true;");
1046        let _ = Reflect::set(&webapp, &"addToHomeScreen".into(), &add);
1047
1048        let app = TelegramWebApp::instance().unwrap();
1049        let shown = app.add_to_home_screen().unwrap();
1050        assert!(shown);
1051        let called = Reflect::get(&webapp, &"called".into())
1052            .unwrap()
1053            .as_bool()
1054            .unwrap_or(false);
1055        assert!(called);
1056    }
1057
1058    #[wasm_bindgen_test]
1059    #[allow(dead_code, clippy::unused_unit)]
1060    fn request_fullscreen_calls_js() {
1061        let webapp = setup_webapp();
1062        let called = Rc::new(Cell::new(false));
1063        let called_clone = Rc::clone(&called);
1064
1065        let cb = Closure::<dyn FnMut()>::new(move || {
1066            called_clone.set(true);
1067        });
1068        let _ = Reflect::set(
1069            &webapp,
1070            &"requestFullscreen".into(),
1071            cb.as_ref().unchecked_ref()
1072        );
1073        cb.forget();
1074
1075        let app = TelegramWebApp::instance().unwrap();
1076        app.request_fullscreen().unwrap();
1077        assert!(called.get());
1078    }
1079
1080    #[wasm_bindgen_test]
1081    #[allow(dead_code, clippy::unused_unit)]
1082    fn exit_fullscreen_calls_js() {
1083        let webapp = setup_webapp();
1084        let called = Rc::new(Cell::new(false));
1085        let called_clone = Rc::clone(&called);
1086
1087        let cb = Closure::<dyn FnMut()>::new(move || {
1088            called_clone.set(true);
1089        });
1090        let _ = Reflect::set(
1091            &webapp,
1092            &"exitFullscreen".into(),
1093            cb.as_ref().unchecked_ref()
1094        );
1095        cb.forget();
1096
1097        let app = TelegramWebApp::instance().unwrap();
1098        app.exit_fullscreen().unwrap();
1099        assert!(called.get());
1100    }
1101
1102    #[wasm_bindgen_test]
1103    #[allow(dead_code, clippy::unused_unit)]
1104    fn check_home_screen_status_invokes_callback() {
1105        let webapp = setup_webapp();
1106        let check = Function::new_with_args("cb", "cb('added');");
1107        let _ = Reflect::set(&webapp, &"checkHomeScreenStatus".into(), &check);
1108
1109        let app = TelegramWebApp::instance().unwrap();
1110        let status = Rc::new(RefCell::new(String::new()));
1111        let status_clone = Rc::clone(&status);
1112
1113        app.check_home_screen_status(move |s| {
1114            *status_clone.borrow_mut() = s;
1115        })
1116        .unwrap();
1117
1118        assert_eq!(status.borrow().as_str(), "added");
1119    }
1120
1121    #[wasm_bindgen_test]
1122    #[allow(dead_code, clippy::unused_unit)]
1123    fn lock_orientation_calls_js() {
1124        let webapp = setup_webapp();
1125        let received = Rc::new(RefCell::new(None));
1126        let rc_clone = Rc::clone(&received);
1127
1128        let cb = Closure::<dyn FnMut(JsValue)>::new(move |v: JsValue| {
1129            *rc_clone.borrow_mut() = v.as_string();
1130        });
1131        let _ = Reflect::set(
1132            &webapp,
1133            &"lockOrientation".into(),
1134            cb.as_ref().unchecked_ref()
1135        );
1136        cb.forget();
1137
1138        let app = TelegramWebApp::instance().unwrap();
1139        app.lock_orientation("portrait").unwrap();
1140        assert_eq!(received.borrow().as_deref(), Some("portrait"));
1141    }
1142
1143    #[wasm_bindgen_test]
1144    #[allow(dead_code, clippy::unused_unit)]
1145    fn unlock_orientation_calls_js() {
1146        let webapp = setup_webapp();
1147        let called = Rc::new(Cell::new(false));
1148        let called_clone = Rc::clone(&called);
1149
1150        let cb = Closure::<dyn FnMut()>::new(move || {
1151            called_clone.set(true);
1152        });
1153        let _ = Reflect::set(
1154            &webapp,
1155            &"unlockOrientation".into(),
1156            cb.as_ref().unchecked_ref()
1157        );
1158        cb.forget();
1159
1160        let app = TelegramWebApp::instance().unwrap();
1161        app.unlock_orientation().unwrap();
1162        assert!(called.get());
1163    }
1164
1165    #[wasm_bindgen_test]
1166    #[allow(dead_code, clippy::unused_unit)]
1167    fn enable_vertical_swipes_calls_js() {
1168        let webapp = setup_webapp();
1169        let called = Rc::new(Cell::new(false));
1170        let called_clone = Rc::clone(&called);
1171
1172        let cb = Closure::<dyn FnMut()>::new(move || {
1173            called_clone.set(true);
1174        });
1175        let _ = Reflect::set(
1176            &webapp,
1177            &"enableVerticalSwipes".into(),
1178            cb.as_ref().unchecked_ref()
1179        );
1180        cb.forget();
1181
1182        let app = TelegramWebApp::instance().unwrap();
1183        app.enable_vertical_swipes().unwrap();
1184        assert!(called.get());
1185    }
1186
1187    #[wasm_bindgen_test]
1188    #[allow(dead_code, clippy::unused_unit)]
1189    fn disable_vertical_swipes_calls_js() {
1190        let webapp = setup_webapp();
1191        let called = Rc::new(Cell::new(false));
1192        let called_clone = Rc::clone(&called);
1193
1194        let cb = Closure::<dyn FnMut()>::new(move || {
1195            called_clone.set(true);
1196        });
1197        let _ = Reflect::set(
1198            &webapp,
1199            &"disableVerticalSwipes".into(),
1200            cb.as_ref().unchecked_ref()
1201        );
1202        cb.forget();
1203
1204        let app = TelegramWebApp::instance().unwrap();
1205        app.disable_vertical_swipes().unwrap();
1206        assert!(called.get());
1207    }
1208
1209    #[wasm_bindgen_test]
1210    #[allow(dead_code, clippy::unused_unit)]
1211    fn request_write_access_invokes_callback() {
1212        let webapp = setup_webapp();
1213        let request = Function::new_with_args("cb", "cb(true);");
1214        let _ = Reflect::set(&webapp, &"requestWriteAccess".into(), &request);
1215
1216        let app = TelegramWebApp::instance().unwrap();
1217        let granted = Rc::new(Cell::new(false));
1218        let granted_clone = Rc::clone(&granted);
1219
1220        let res = app.request_write_access(move |g| {
1221            granted_clone.set(g);
1222        });
1223        assert!(res.is_ok());
1224
1225        assert!(granted.get());
1226    }
1227
1228    #[wasm_bindgen_test]
1229    #[allow(dead_code, clippy::unused_unit)]
1230    fn download_file_invokes_callback() {
1231        let webapp = setup_webapp();
1232        let received_url = Rc::new(RefCell::new(String::new()));
1233        let received_name = Rc::new(RefCell::new(String::new()));
1234        let url_clone = Rc::clone(&received_url);
1235        let name_clone = Rc::clone(&received_name);
1236
1237        let download = Closure::<dyn FnMut(JsValue, JsValue)>::new(move |params, cb: JsValue| {
1238            let url = Reflect::get(&params, &"url".into())
1239                .unwrap()
1240                .as_string()
1241                .unwrap_or_default();
1242            let name = Reflect::get(&params, &"file_name".into())
1243                .unwrap()
1244                .as_string()
1245                .unwrap_or_default();
1246            *url_clone.borrow_mut() = url;
1247            *name_clone.borrow_mut() = name;
1248            let func = cb.dyn_ref::<Function>().unwrap();
1249            let _ = func.call1(&JsValue::NULL, &JsValue::from_str("id"));
1250        });
1251        let _ = Reflect::set(
1252            &webapp,
1253            &"downloadFile".into(),
1254            download.as_ref().unchecked_ref()
1255        );
1256        download.forget();
1257
1258        let app = TelegramWebApp::instance().unwrap();
1259        let result = Rc::new(RefCell::new(String::new()));
1260        let result_clone = Rc::clone(&result);
1261        let params = DownloadFileParams {
1262            url:       "https://example.com/data.bin",
1263            file_name: Some("data.bin"),
1264            mime_type: None
1265        };
1266        app.download_file(params, move |id| {
1267            *result_clone.borrow_mut() = id;
1268        })
1269        .unwrap();
1270
1271        assert_eq!(
1272            received_url.borrow().as_str(),
1273            "https://example.com/data.bin"
1274        );
1275        assert_eq!(received_name.borrow().as_str(), "data.bin");
1276        assert_eq!(result.borrow().as_str(), "id");
1277    }
1278
1279    #[wasm_bindgen_test]
1280    #[allow(dead_code, clippy::unused_unit)]
1281    fn request_write_access_returns_error_when_missing() {
1282        let _webapp = setup_webapp();
1283        let app = TelegramWebApp::instance().unwrap();
1284        let res = app.request_write_access(|_| {});
1285        assert!(res.is_err());
1286    }
1287    #[wasm_bindgen_test]
1288    #[allow(dead_code, clippy::unused_unit)]
1289    fn request_emoji_status_access_invokes_callback() {
1290        let webapp = setup_webapp();
1291        let request = Function::new_with_args("cb", "cb(false);");
1292        let _ = Reflect::set(&webapp, &"requestEmojiStatusAccess".into(), &request);
1293
1294        let app = TelegramWebApp::instance().unwrap();
1295        let granted = Rc::new(Cell::new(true));
1296        let granted_clone = Rc::clone(&granted);
1297
1298        app.request_emoji_status_access(move |g| {
1299            granted_clone.set(g);
1300        })
1301        .unwrap();
1302
1303        assert!(!granted.get());
1304    }
1305
1306    #[wasm_bindgen_test]
1307    #[allow(dead_code, clippy::unused_unit)]
1308    fn set_emoji_status_invokes_callback() {
1309        let webapp = setup_webapp();
1310        let set_status = Function::new_with_args("status, cb", "this.st = status; cb(true);");
1311        let _ = Reflect::set(&webapp, &"setEmojiStatus".into(), &set_status);
1312
1313        let status = Object::new();
1314        let _ = Reflect::set(
1315            &status,
1316            &"custom_emoji_id".into(),
1317            &JsValue::from_str("321")
1318        );
1319
1320        let app = TelegramWebApp::instance().unwrap();
1321        let success = Rc::new(Cell::new(false));
1322        let success_clone = Rc::clone(&success);
1323
1324        app.set_emoji_status(&status.into(), move |s| {
1325            success_clone.set(s);
1326        })
1327        .unwrap();
1328
1329        assert!(success.get());
1330        let stored = Reflect::get(&webapp, &"st".into()).unwrap();
1331        let id = Reflect::get(&stored, &"custom_emoji_id".into())
1332            .unwrap()
1333            .as_string();
1334        assert_eq!(id.as_deref(), Some("321"));
1335    }
1336
1337    #[wasm_bindgen_test]
1338    #[allow(dead_code, clippy::unused_unit)]
1339    fn show_popup_invokes_callback() {
1340        let webapp = setup_webapp();
1341        let show_popup = Function::new_with_args("params, cb", "cb('ok');");
1342        let _ = Reflect::set(&webapp, &"showPopup".into(), &show_popup);
1343
1344        let app = TelegramWebApp::instance().unwrap();
1345        let button = Rc::new(RefCell::new(String::new()));
1346        let button_clone = Rc::clone(&button);
1347
1348        app.show_popup(&JsValue::NULL, move |id| {
1349            *button_clone.borrow_mut() = id;
1350        })
1351        .unwrap();
1352
1353        assert_eq!(button.borrow().as_str(), "ok");
1354    }
1355
1356    #[wasm_bindgen_test]
1357    #[allow(dead_code, clippy::unused_unit)]
1358    fn read_text_from_clipboard_invokes_callback() {
1359        let webapp = setup_webapp();
1360        let read_clip = Function::new_with_args("cb", "cb('clip');");
1361        let _ = Reflect::set(&webapp, &"readTextFromClipboard".into(), &read_clip);
1362
1363        let app = TelegramWebApp::instance().unwrap();
1364        let text = Rc::new(RefCell::new(String::new()));
1365        let text_clone = Rc::clone(&text);
1366
1367        app.read_text_from_clipboard(move |t| {
1368            *text_clone.borrow_mut() = t;
1369        })
1370        .unwrap();
1371
1372        assert_eq!(text.borrow().as_str(), "clip");
1373    }
1374
1375    #[wasm_bindgen_test]
1376    #[allow(dead_code, clippy::unused_unit)]
1377    fn scan_qr_popup_invokes_callback_and_close() {
1378        let webapp = setup_webapp();
1379        let show_scan = Function::new_with_args("text, cb", "cb('code');");
1380        let close_scan = Function::new_with_args("", "this.closed = true;");
1381        let _ = Reflect::set(&webapp, &"showScanQrPopup".into(), &show_scan);
1382        let _ = Reflect::set(&webapp, &"closeScanQrPopup".into(), &close_scan);
1383
1384        let app = TelegramWebApp::instance().unwrap();
1385        let text = Rc::new(RefCell::new(String::new()));
1386        let text_clone = Rc::clone(&text);
1387
1388        app.show_scan_qr_popup("scan", move |value| {
1389            *text_clone.borrow_mut() = value;
1390        })
1391        .unwrap();
1392        assert_eq!(text.borrow().as_str(), "code");
1393
1394        app.close_scan_qr_popup().unwrap();
1395        let closed = Reflect::get(&webapp, &"closed".into())
1396            .unwrap()
1397            .as_bool()
1398            .unwrap_or(false);
1399        assert!(closed);
1400    }
1401}