@tma.js/sdk 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/dts/index.d.ts +1 -0
  2. package/dist/dts/launch-params/index.d.ts +1 -0
  3. package/dist/dts/launch-params/retrieveFromUrl.d.ts +6 -0
  4. package/dist/dts/launch-params/types.d.ts +12 -8
  5. package/dist/index.cjs +1 -1
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.iife.js +1 -1
  8. package/dist/index.iife.js.map +1 -1
  9. package/dist/index.mjs +253 -238
  10. package/dist/index.mjs.map +1 -1
  11. package/package.json +2 -2
  12. package/src/__tests__/globals.ts +39 -0
  13. package/src/back-button/__tests__/BackButton.ts +129 -0
  14. package/src/bridge/__tests__/request.ts +236 -0
  15. package/src/bridge/env/__tests__/hasExternalNotify.ts +15 -0
  16. package/src/bridge/env/__tests__/hasWebviewProxy.ts +15 -0
  17. package/src/bridge/env/__tests__/isIframe.ts +30 -0
  18. package/src/bridge/events/__tests__/createEmitter.ts +143 -0
  19. package/src/bridge/events/__tests__/off.ts +34 -0
  20. package/src/bridge/events/__tests__/on.ts +49 -0
  21. package/src/bridge/events/__tests__/onTelegramEvent.ts +51 -0
  22. package/src/bridge/events/__tests__/once.ts +64 -0
  23. package/src/bridge/events/__tests__/singletonEmitter.ts +22 -0
  24. package/src/bridge/events/__tests__/subscribe.ts +49 -0
  25. package/src/bridge/events/__tests__/unsubscribe.ts +34 -0
  26. package/src/bridge/events/parsers/__tests__/clipboardTextReceived.ts +21 -0
  27. package/src/bridge/events/parsers/__tests__/invoiceClosed.ts +12 -0
  28. package/src/bridge/events/parsers/__tests__/popupClosed.ts +10 -0
  29. package/src/bridge/events/parsers/__tests__/qrTextReceived.ts +9 -0
  30. package/src/bridge/events/parsers/__tests__/theme-changed.ts +42 -0
  31. package/src/bridge/events/parsers/__tests__/viewportChanged.ts +49 -0
  32. package/src/bridge/methods/__tests__/createPostEvent.ts +37 -0
  33. package/src/bridge/methods/__tests__/postEvent.ts +137 -0
  34. package/src/classnames/__tests__/classNames.ts +20 -0
  35. package/src/classnames/__tests__/mergeClassNames.ts +21 -0
  36. package/src/closing-behavior/__tests__/ClosingBehavior.ts +86 -0
  37. package/src/colors/__tests__/isColorDark.ts +12 -0
  38. package/src/colors/__tests__/isRGB.ts +13 -0
  39. package/src/colors/__tests__/isRGBShort.ts +13 -0
  40. package/src/colors/__tests__/toRGB.ts +23 -0
  41. package/src/event-emitter/__tests__/EventEmitter.ts +145 -0
  42. package/src/haptic-feedback/__tests__/HapticFeedback.ts +68 -0
  43. package/src/index.ts +12 -0
  44. package/src/init/creators/__tests__/createViewport.ts +96 -0
  45. package/src/init-data/__tests__/InitData.ts +98 -0
  46. package/src/init-data/__tests__/chatParser.ts +102 -0
  47. package/src/init-data/__tests__/initDataParser.ts +136 -0
  48. package/src/init-data/__tests__/parseInitData.ts +136 -0
  49. package/src/init-data/__tests__/userParser.ts +96 -0
  50. package/src/launch-params/__tests__/retrieveFromUrl.ts +19 -0
  51. package/src/launch-params/index.ts +1 -0
  52. package/src/launch-params/launchParamsParser.ts +4 -0
  53. package/src/launch-params/retrieveFromLocation.ts +2 -2
  54. package/src/launch-params/retrieveFromPerformance.ts +2 -7
  55. package/src/launch-params/retrieveFromUrl.ts +19 -0
  56. package/src/launch-params/types.ts +13 -8
  57. package/src/logger/__tests__/Logger.ts +107 -0
  58. package/src/main-button/__tests__/MainButton.ts +346 -0
  59. package/src/mini-app/__tests__/MiniApp.ts +140 -0
  60. package/src/misc/__tests__/isRecord.ts +21 -0
  61. package/src/navigation/HashNavigator/__tests__/HashNavigator.ts +144 -0
  62. package/src/navigation/HashNavigator/__tests__/drop.ts +42 -0
  63. package/src/navigation/HashNavigator/__tests__/go.ts +9 -0
  64. package/src/parsing/__tests__/ArrayValueParser.ts +18 -0
  65. package/src/parsing/__tests__/toRecord.ts +10 -0
  66. package/src/parsing/parsers/__tests__/array.ts +39 -0
  67. package/src/parsing/parsers/__tests__/boolean.ts +31 -0
  68. package/src/parsing/parsers/__tests__/date.ts +25 -0
  69. package/src/parsing/parsers/__tests__/json.ts +80 -0
  70. package/src/parsing/parsers/__tests__/number.ts +23 -0
  71. package/src/parsing/parsers/__tests__/rgb.ts +22 -0
  72. package/src/parsing/parsers/__tests__/searchParams.ts +105 -0
  73. package/src/parsing/parsers/__tests__/string.ts +25 -0
  74. package/src/popup/__tests__/Popup.ts +130 -0
  75. package/src/popup/__tests__/preparePopupParams.ts +85 -0
  76. package/src/supports/__tests__/supports.ts +123 -0
  77. package/src/theme-params/__tests__/keys.ts +19 -0
  78. package/src/theme-params/__tests__/parseThemeParams.ts +29 -0
  79. package/src/theme-params/__tests__/serializeThemeParams.ts +29 -0
  80. package/src/theme-params/__tests__/themeParamsParser.ts +29 -0
  81. package/src/timeout/__tests__/isTimeoutError.ts +9 -0
  82. package/src/timeout/__tests__/withTimeout.ts +28 -0
  83. package/src/version/__tests__/compareVersions.ts +19 -0
  84. package/src/viewport/__tests__/isStableViewportPlatform.ts +15 -0
  85. package/src/viewport/__tests__/utils.ts +12 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tma.js/sdk",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "TypeScript Source Development Kit for Telegram Mini Apps client application.",
5
5
  "author": "Vladislav Kibenko <wolfram.deus@gmail.com>",
6
6
  "homepage": "https://github.com/Telegram-Mini-Apps/tma.js#readme",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "scripts": {
49
49
  "test": "vitest --run",
50
- "lint": "eslint src/**/*",
50
+ "lint": "eslint src",
51
51
  "lint:fix": "pnpm run lint --fix",
52
52
  "typecheck": "tsc --noEmit -p tsconfig.build.json",
53
53
  "build": "pnpm run build:default && pnpm run build:iife",
@@ -0,0 +1,39 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import {
4
+ logger,
5
+ setDebug,
6
+ setTargetOrigin,
7
+ targetOrigin,
8
+ } from '../globals';
9
+
10
+ afterEach(() => {
11
+ vi.restoreAllMocks();
12
+ });
13
+
14
+ describe('logger', () => {
15
+ it('should log message in case, debug mode is enabled. Otherwise no output should be shown', () => {
16
+ const spy = vi
17
+ .spyOn(console, 'log')
18
+ .mockImplementation(() => {
19
+ });
20
+
21
+ logger.log(123);
22
+ expect(spy).not.toHaveBeenCalled();
23
+
24
+ setDebug(true);
25
+ logger.log('Some log');
26
+ expect(spy).toHaveBeenCalledTimes(1);
27
+
28
+ setDebug(false);
29
+ logger.log('Another log');
30
+ expect(spy).toHaveBeenCalledTimes(1);
31
+ });
32
+ });
33
+
34
+ describe('setTargetOrigin', () => {
35
+ it('should return set value via targetOrigin() function', () => {
36
+ setTargetOrigin('my test');
37
+ expect(targetOrigin()).toEqual('my test');
38
+ });
39
+ });
@@ -0,0 +1,129 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { BackButton } from '../BackButton';
4
+
5
+ describe('hide', () => {
6
+ it('should call "web_app_setup_back_button" method with "is_visible" equal to false', () => {
7
+ const postEvent = vi.fn();
8
+ const backButton = new BackButton(false, '', postEvent);
9
+
10
+ expect(postEvent).toHaveBeenCalledTimes(0);
11
+ backButton.hide();
12
+ expect(postEvent).toHaveBeenCalledTimes(1);
13
+ expect(postEvent).toHaveBeenCalledWith('web_app_setup_back_button', { is_visible: false });
14
+ });
15
+
16
+ it('should emit "isVisibleChanged" event with false value', () => {
17
+ const backButton = new BackButton(true, '', vi.fn());
18
+ const listener = vi.fn();
19
+
20
+ backButton.on('change:isVisible', listener);
21
+ expect(listener).toHaveBeenCalledTimes(0);
22
+ backButton.hide();
23
+ expect(listener).toHaveBeenCalledTimes(1);
24
+ expect(listener).toHaveBeenCalledWith(false);
25
+ });
26
+ });
27
+
28
+ describe('show', () => {
29
+ it('should call "web_app_setup_back_button" method with "is_visible" equal to true', () => {
30
+ const postEvent = vi.fn();
31
+ const backButton = new BackButton(false, '', postEvent);
32
+
33
+ expect(postEvent).toHaveBeenCalledTimes(0);
34
+ backButton.show();
35
+ expect(postEvent).toHaveBeenCalledTimes(1);
36
+ expect(postEvent).toHaveBeenCalledWith('web_app_setup_back_button', { is_visible: true });
37
+ });
38
+
39
+ it('should emit "isVisibleChanged" event with true value', () => {
40
+ const backButton = new BackButton(false, '', vi.fn());
41
+ const listener = vi.fn();
42
+
43
+ backButton.on('change:isVisible', listener);
44
+ expect(listener).toHaveBeenCalledTimes(0);
45
+ backButton.show();
46
+ expect(listener).toHaveBeenCalledTimes(1);
47
+ expect(listener).toHaveBeenCalledWith(true);
48
+ });
49
+ });
50
+
51
+ // FIXME
52
+ // it('on', () => {
53
+ // it('"click" event', () => {
54
+ // it('should add event listener to bridge\'s '
55
+ // + '"back_button_pressed" event', () => {
56
+ // const on = vi.fn();
57
+ // const backButton = new BackButton({ on } as any, '');
58
+ // const listener = vi.fn();
59
+ //
60
+ // expect(on).toHaveBeenCalledTimes(0);
61
+ // backButton.on('click', listener);
62
+ // expect(on).toHaveBeenCalledTimes(1);
63
+ // expect(on).toHaveBeenCalledWith('back_button_pressed', listener);
64
+ // });
65
+ // });
66
+ // });
67
+
68
+ describe('off', () => {
69
+ // FIXME
70
+ // it('"click" event', () => {
71
+ // it('should remove event listener from bridge\'s '
72
+ // + '"back_button_pressed" event', () => {
73
+ // const on = vi.fn();
74
+ // const off = vi.fn();
75
+ // const backButton = new BackButton({ on, off } as any, '');
76
+ // const listener = vi.fn();
77
+ //
78
+ // expect(on).toHaveBeenCalledTimes(0);
79
+ // backButton.on('click', listener);
80
+ // expect(on).toHaveBeenCalledTimes(1);
81
+ // expect(on).toHaveBeenCalledWith('back_button_pressed', listener);
82
+ //
83
+ // // TODO: We should probably emit back_button_pressed event to
84
+ // // make sure, listener was removed.
85
+ //
86
+ // expect(off).toHaveBeenCalledTimes(0);
87
+ // backButton.off('click', listener);
88
+ // expect(off).toHaveBeenCalledTimes(1);
89
+ // expect(off).toHaveBeenCalledWith('back_button_pressed', listener);
90
+ // });
91
+ // });
92
+
93
+ describe('"isVisibleChanged" event', () => {
94
+ it('should remove event listener from event', () => {
95
+ const listener = vi.fn();
96
+ const backButton = new BackButton(false, '', vi.fn());
97
+
98
+ backButton.on('change:isVisible', listener);
99
+
100
+ expect(listener).toHaveBeenCalledTimes(0);
101
+ backButton.show();
102
+ expect(listener).toHaveBeenCalledTimes(1);
103
+
104
+ backButton.off('change:isVisible', listener);
105
+
106
+ expect(listener).toHaveBeenCalledTimes(1);
107
+ backButton.hide();
108
+ expect(listener).toHaveBeenCalledTimes(1);
109
+ });
110
+ });
111
+ });
112
+
113
+ describe('supports', () => {
114
+ describe('show / hide', () => {
115
+ it('should return true in case, BackButton version is 6.1 or higher. False, otherwise', () => {
116
+ const backButton1 = new BackButton(false, '6.0');
117
+ expect(backButton1.supports('show')).toBe(false);
118
+ expect(backButton1.supports('hide')).toBe(false);
119
+
120
+ const backButton2 = new BackButton(false, '6.1');
121
+ expect(backButton2.supports('show')).toBe(true);
122
+ expect(backButton2.supports('hide')).toBe(true);
123
+
124
+ const backButton3 = new BackButton(false, '6.2');
125
+ expect(backButton3.supports('show')).toBe(true);
126
+ expect(backButton3.supports('hide')).toBe(true);
127
+ });
128
+ });
129
+ });
@@ -0,0 +1,236 @@
1
+ /* eslint-disable */
2
+ import {
3
+ expect,
4
+ it,
5
+ vi,
6
+ SpyInstance,
7
+ beforeEach,
8
+ afterEach,
9
+ describe,
10
+ beforeAll,
11
+ afterAll,
12
+ } from 'vitest';
13
+
14
+ import { request } from '../request';
15
+ import { type PostEvent, postEvent as globalPostEvent } from '../methods/postEvent';
16
+ import { createWindow } from '../../../test-utils/createWindow';
17
+ import { dispatchWindowMessageEvent } from '../../../test-utils/dispatchWindowMessageEvent';
18
+
19
+ vi.mock('../methods/postEvent.js', async () => {
20
+ const { postEvent: actualPostEvent } = await vi
21
+ .importActual('../methods/postEvent.js') as { postEvent: PostEvent };
22
+
23
+ return {
24
+ postEvent: vi.fn(actualPostEvent),
25
+ };
26
+ });
27
+
28
+ let windowSpy: SpyInstance<[], Window & typeof globalThis>;
29
+
30
+ beforeAll(() => {
31
+ vi.useFakeTimers();
32
+ });
33
+
34
+ afterAll(() => {
35
+ vi.useRealTimers();
36
+ });
37
+
38
+ beforeEach(() => {
39
+ windowSpy = createWindow({ env: 'iframe' });
40
+ });
41
+
42
+ afterEach(() => {
43
+ windowSpy.mockRestore();
44
+ });
45
+
46
+ function emptyCatch() {
47
+
48
+ }
49
+
50
+ describe('options', () => {
51
+ describe('timeout', () => {
52
+ it('should throw an error in case, timeout was reached', () => {
53
+ const promise = request('web_app_request_phone', 'phone_requested', {
54
+ timeout: 1000,
55
+ });
56
+
57
+ vi.advanceTimersByTime(1500);
58
+
59
+ return promise.catch(emptyCatch).finally(() => {
60
+ expect(promise).rejects.toEqual(new Error('Async call timeout exceeded. Timeout: 1000'));
61
+ });
62
+ });
63
+
64
+ it('should not throw an error in case, data was received before timeout', () => {
65
+ const promise = request('web_app_request_phone', 'phone_requested', {
66
+ timeout: 1000,
67
+ });
68
+
69
+ vi.advanceTimersByTime(500);
70
+ dispatchWindowMessageEvent('phone_requested', { status: 'allowed' });
71
+ vi.advanceTimersByTime(1000);
72
+
73
+ return promise.catch(emptyCatch).finally(() => {
74
+ expect(promise).resolves.toStrictEqual({ status: 'allowed' });
75
+ });
76
+ });
77
+ });
78
+
79
+ describe('postEvent', () => {
80
+ it('should use specified postEvent property', () => {
81
+ const postEvent = vi.fn();
82
+ request('web_app_request_phone', 'phone_requested', { postEvent });
83
+ expect(postEvent).toHaveBeenCalledWith('web_app_request_phone', undefined);
84
+ });
85
+
86
+ it('should use global postEvent function if according property was not specified', () => {
87
+ request('web_app_request_phone', 'phone_requested');
88
+ expect(globalPostEvent).toHaveBeenCalledWith('web_app_request_phone', undefined);
89
+ });
90
+
91
+ it('should reject promise in case, postEvent threw an error', () => {
92
+ const promise = request('web_app_request_phone', 'phone_requested', {
93
+ postEvent: () => {
94
+ throw new Error('Nope!');
95
+ },
96
+ });
97
+ expect(promise).rejects.toStrictEqual(Error('Nope!'));
98
+ });
99
+ });
100
+
101
+ describe('capture', () => {
102
+ it('should capture an event in case, capture method returned true', () => {
103
+ const promise = request('web_app_request_phone', 'phone_requested', {
104
+ timeout: 1000,
105
+ capture: ({ status }) => status === 'allowed',
106
+ });
107
+
108
+ vi.advanceTimersByTime(500);
109
+ dispatchWindowMessageEvent('phone_requested', { status: 'allowed' });
110
+ vi.advanceTimersByTime(1000);
111
+
112
+ return promise.catch(emptyCatch).finally(() => {
113
+ expect(promise).resolves.toStrictEqual({ status: 'allowed' });
114
+ });
115
+ });
116
+
117
+ it('should not capture an event in case, capture method returned false', () => {
118
+ const promise = request('web_app_request_phone', 'phone_requested', {
119
+ timeout: 500,
120
+ capture: ({ status }) => status === 'allowed',
121
+ });
122
+
123
+ dispatchWindowMessageEvent('phone_requested', { status: 'declined' });
124
+ vi.advanceTimersByTime(1000);
125
+
126
+ return promise.catch(emptyCatch).finally(() => {
127
+ expect(promise).rejects.toEqual(new Error('Async call timeout exceeded. Timeout: 500'));
128
+ });
129
+ });
130
+ });
131
+ });
132
+
133
+ describe('with request id', () => {
134
+ it('should ignore event with the different request id', () => {
135
+ const promise = request('web_app_read_text_from_clipboard', { req_id: 'a' }, 'clipboard_text_received', {
136
+ timeout: 1000,
137
+ });
138
+
139
+ dispatchWindowMessageEvent('clipboard_text_received', { req_id: 'b' });
140
+ vi.advanceTimersByTime(1500);
141
+
142
+ return promise.catch(emptyCatch).finally(() => {
143
+ expect(promise).rejects.toEqual(new Error('Async call timeout exceeded. Timeout: 1000'));
144
+ });
145
+ });
146
+
147
+ it('should capture event with the same request id', () => {
148
+ const promise = request('web_app_read_text_from_clipboard', { req_id: 'a' }, 'clipboard_text_received', {
149
+ timeout: 1000,
150
+ });
151
+
152
+ dispatchWindowMessageEvent('clipboard_text_received', {
153
+ req_id: 'a',
154
+ data: 'from clipboard',
155
+ });
156
+ vi.advanceTimersByTime(1500);
157
+
158
+ return promise.catch(emptyCatch).finally(() => {
159
+ expect(promise).resolves.toStrictEqual({
160
+ req_id: 'a',
161
+ data: 'from clipboard',
162
+ });
163
+ });
164
+ });
165
+ });
166
+
167
+ describe('multiple events', () => {
168
+ describe('no params', () => {
169
+ it('should handle any of the specified events', () => {
170
+ const promise = request(
171
+ 'web_app_request_phone',
172
+ ['phone_requested', 'write_access_requested'],
173
+ { timeout: 1000 },
174
+ );
175
+ const promise2 = request(
176
+ 'web_app_request_phone',
177
+ ['phone_requested', 'write_access_requested'],
178
+ { timeout: 1000 },
179
+ );
180
+
181
+ dispatchWindowMessageEvent('phone_requested', { status: 'allowed' });
182
+ dispatchWindowMessageEvent('write_access_requested', { status: 'declined' });
183
+ vi.advanceTimersByTime(1500);
184
+
185
+ return Promise
186
+ .all([promise, promise2])
187
+ .catch(emptyCatch)
188
+ .finally(() => {
189
+ expect(promise).resolves.toStrictEqual({ status: 'allowed' });
190
+ expect(promise2).resolves.toStrictEqual({ status: 'declined' });
191
+ });
192
+ });
193
+ });
194
+
195
+ describe('with params', () => {
196
+ it('should handle any of the specified events', () => {
197
+ const promise = request(
198
+ 'web_app_data_send',
199
+ { data: 'abc' },
200
+ ['phone_requested', 'write_access_requested'],
201
+ { timeout: 1000 },
202
+ );
203
+ const promise2 = request(
204
+ 'web_app_data_send',
205
+ { data: 'abc' },
206
+ ['phone_requested', 'write_access_requested'],
207
+ { timeout: 1000 },
208
+ );
209
+
210
+ dispatchWindowMessageEvent('phone_requested', { status: 'allowed' });
211
+ dispatchWindowMessageEvent('write_access_requested', { status: 'declined' });
212
+ vi.advanceTimersByTime(1500);
213
+
214
+ return Promise
215
+ .all([promise, promise2])
216
+ .catch(emptyCatch)
217
+ .finally(() => {
218
+ expect(promise).resolves.toStrictEqual({ status: 'allowed' });
219
+ expect(promise2).resolves.toStrictEqual({ status: 'declined' });
220
+ });
221
+ });
222
+ });
223
+ });
224
+
225
+ // it('no params methods', () => {
226
+ // it('should properly handle ')
227
+ // });
228
+
229
+ // it('with request id', () => {
230
+ // const promise = request('web_app_read_text_from_clipboard', { req_id: 'a' }, 'clipboard_text_received');
231
+ //
232
+ // dispatchWindowEvent('clipboard_text_received', { req_id: 'b' });
233
+ // expect(promise).resolves.toHaveLength(0);
234
+ // dispatchWindowEvent('clipboard_text_received', { req_id: 'a' });
235
+ // expect(promise).resolves.toHaveLength(1);
236
+ // });
@@ -0,0 +1,15 @@
1
+ import { expect, it } from 'vitest';
2
+
3
+ import { hasExternalNotify } from '../hasExternalNotify.js';
4
+
5
+ it('should return true if passed object contains path property "external.notify" and "notify" is a function property.', () => {
6
+ expect(hasExternalNotify({})).toBe(false);
7
+ expect(hasExternalNotify({ external: {} })).toBe(false);
8
+ expect(hasExternalNotify({ external: { notify: [] } })).toBe(false);
9
+ expect(hasExternalNotify({
10
+ external: {
11
+ notify: () => {
12
+ },
13
+ },
14
+ })).toBe(true);
15
+ });
@@ -0,0 +1,15 @@
1
+ import { expect, it } from 'vitest';
2
+
3
+ import { hasWebviewProxy } from '../hasWebviewProxy.js';
4
+
5
+ it('should return true if passed object contains path property "TelegramWebviewProxy.postEvent" and "postEvent" is a function property.', () => {
6
+ expect(hasWebviewProxy({})).toBe(false);
7
+ expect(hasWebviewProxy({ TelegramWebviewProxy: {} })).toBe(false);
8
+ expect(hasWebviewProxy({ TelegramWebviewProxy: { postEvent: [] } })).toBe(false);
9
+ expect(hasWebviewProxy({
10
+ TelegramWebviewProxy: {
11
+ postEvent: () => {
12
+ },
13
+ },
14
+ })).toBe(true);
15
+ });
@@ -0,0 +1,30 @@
1
+ import { afterAll, afterEach, expect, it, vi } from 'vitest';
2
+
3
+ import { isIframe } from '../isIframe.js';
4
+
5
+ const windowSpy = vi.spyOn(window, 'window', 'get');
6
+
7
+ afterEach(() => {
8
+ windowSpy.mockReset();
9
+ });
10
+
11
+ afterAll(() => {
12
+ windowSpy.mockRestore();
13
+ });
14
+
15
+ it('should return true in case window.self !== window.top. Otherwise, false.', () => {
16
+ windowSpy.mockImplementation(() => ({ self: 900, top: 1000 }) as any);
17
+ expect(isIframe()).toBe(true);
18
+
19
+ windowSpy.mockImplementation(() => ({ self: 900, top: 900 }) as any);
20
+ expect(isIframe()).toBe(false);
21
+ });
22
+
23
+ it('should return true in case window.self getter threw an error', () => {
24
+ windowSpy.mockImplementation(() => ({
25
+ get self() {
26
+ throw new Error();
27
+ },
28
+ }) as any);
29
+ expect(isIframe()).toBe(true);
30
+ });
@@ -0,0 +1,143 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { createWindow, type WindowSpy } from '../../../../test-utils/createWindow';
4
+ import { dispatchWindowMessageEvent } from '../../../../test-utils/dispatchWindowMessageEvent';
5
+ import { createEmitter } from '../createEmitter';
6
+ import type { MiniAppsEventName, MiniAppsEventParams } from '../events';
7
+
8
+ type TestCase<E extends MiniAppsEventName> =
9
+ | [input: any, expected: MiniAppsEventParams<E>]
10
+ | MiniAppsEventParams<E>;
11
+
12
+ type TestCases = {
13
+ [E in MiniAppsEventName]: MiniAppsEventParams<E> extends undefined
14
+ ? [E]
15
+ : [E, TestCase<E> | TestCase<E>[]];
16
+ }[MiniAppsEventName][];
17
+
18
+ let windowSpy: WindowSpy;
19
+
20
+ beforeEach(() => {
21
+ windowSpy = createWindow({
22
+ innerWidth: 1920,
23
+ innerHeight: 1080,
24
+ });
25
+ });
26
+
27
+ afterEach(() => {
28
+ windowSpy.mockRestore();
29
+ });
30
+
31
+ it('should emit "viewport_changed" event in case, window changed its size', () => {
32
+ const emitter = createEmitter();
33
+ const spy = vi.fn();
34
+
35
+ emitter.on('viewport_changed', spy);
36
+
37
+ window.dispatchEvent(new CustomEvent('resize'));
38
+
39
+ expect(spy).toHaveBeenCalledTimes(1);
40
+ expect(spy).toHaveBeenCalledWith({
41
+ width: 1920,
42
+ height: 1080,
43
+ is_state_stable: true,
44
+ is_expanded: true,
45
+ });
46
+ });
47
+
48
+ describe('events handling', () => {
49
+ const testCases: TestCases = [
50
+ ['viewport_changed', {
51
+ height: 120,
52
+ width: 300,
53
+ is_expanded: true,
54
+ is_state_stable: false,
55
+ }],
56
+ ['theme_changed', {
57
+ theme_params: {
58
+ bg_color: '#aabbdd',
59
+ text_color: '#113322',
60
+ hint_color: '#132245',
61
+ link_color: '#133322',
62
+ button_color: '#a23135',
63
+ button_text_color: '#aa213f',
64
+ },
65
+ }],
66
+ ['popup_closed', [
67
+ [{ button_id: 'ok' }, { button_id: 'ok' }],
68
+ [{ button_id: null }, {}],
69
+ [{ button_id: undefined }, {}],
70
+ [null, {}],
71
+ [undefined, {}],
72
+ ]],
73
+ ['set_custom_style', '.scroll {}'],
74
+ ['qr_text_received', { data: 'some QR data' }],
75
+ ['main_button_pressed'],
76
+ ['back_button_pressed'],
77
+ ['settings_button_pressed'],
78
+ ['scan_qr_popup_closed'],
79
+ ['clipboard_text_received', {
80
+ req_id: 'request id',
81
+ data: 'clipboard value',
82
+ }],
83
+ ['invoice_closed', { slug: '&&*Sh1j213kx', status: 'PAID' }],
84
+ ['phone_requested', { status: 'sent' }],
85
+ ['custom_method_invoked', [
86
+ [{ req_id: '1', result: 'My result' }],
87
+ [{ req_id: '2', error: 'Something is wrong' }],
88
+ ]],
89
+ ['write_access_requested', { status: 'allowed' }],
90
+ ['unknown_event', [
91
+ ['hello', 'hello'],
92
+ [{ there: true }, { there: true }],
93
+ ]] as any,
94
+ ];
95
+
96
+ testCases.forEach(([event, inputOrCaseOrCases]) => {
97
+ it(`should correctly handle "${event}" event data`, () => {
98
+ const spy = vi.fn();
99
+ const emitter = createEmitter();
100
+
101
+ emitter.on(event, spy);
102
+
103
+ // No expected data to be passed to listener.
104
+ if (inputOrCaseOrCases === undefined) {
105
+ dispatchWindowMessageEvent(event);
106
+ expect(spy).toBeCalledWith();
107
+ return;
108
+ }
109
+
110
+ // Input is equal to expected result.
111
+ if (!Array.isArray(inputOrCaseOrCases)) {
112
+ dispatchWindowMessageEvent(event, inputOrCaseOrCases);
113
+ expect(spy).toBeCalledWith(inputOrCaseOrCases);
114
+ return;
115
+ }
116
+
117
+ // Input differs from expected result.
118
+ if (!Array.isArray(inputOrCaseOrCases[0])) {
119
+ const [input, expected] = inputOrCaseOrCases;
120
+ dispatchWindowMessageEvent(event, input);
121
+ expect(spy).toBeCalledWith(expected);
122
+ return;
123
+ }
124
+
125
+ // List of cases.
126
+ inputOrCaseOrCases.forEach(([input, expected = input]) => {
127
+ dispatchWindowMessageEvent(event, input);
128
+ expect(spy).toBeCalledWith(expected);
129
+ });
130
+ });
131
+ });
132
+
133
+ it('should not emit event in case, it contains incorrect payload', () => {
134
+ const spy = vi.fn();
135
+ const emitter = createEmitter();
136
+
137
+ emitter.on('viewport_changed', spy);
138
+
139
+ dispatchWindowMessageEvent('viewport_changed', 'broken data');
140
+
141
+ expect(spy).not.toBeCalled();
142
+ });
143
+ });
@@ -0,0 +1,34 @@
1
+ import { afterEach, beforeEach, expect, it, vi } from 'vitest';
2
+
3
+ import { createWindow, type WindowSpy } from '../../../../test-utils/createWindow';
4
+ import { dispatchWindowMessageEvent } from '../../../../test-utils/dispatchWindowMessageEvent';
5
+ import { off } from '../off';
6
+ import { on } from '../on';
7
+
8
+ let windowSpy: WindowSpy;
9
+
10
+ beforeEach(() => {
11
+ windowSpy = createWindow();
12
+ });
13
+
14
+ afterEach(() => {
15
+ windowSpy.mockRestore();
16
+ });
17
+
18
+ it('should remove listener', () => {
19
+ const listener = vi.fn();
20
+ const emit = () => dispatchWindowMessageEvent('viewport_changed', {
21
+ height: 123,
22
+ width: 321,
23
+ is_expanded: false,
24
+ is_state_stable: false,
25
+ });
26
+
27
+ on('viewport_changed', listener);
28
+ emit();
29
+ expect(listener).toHaveBeenCalledTimes(1);
30
+
31
+ off('viewport_changed', listener);
32
+ emit();
33
+ expect(listener).toHaveBeenCalledTimes(1);
34
+ });