@applicaster/zapp-react-native-utils 15.0.0-alpha.3514407021 → 15.0.0-alpha.3564377339

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 (61) hide show
  1. package/actionsExecutor/ActionExecutorContext.tsx +3 -6
  2. package/actionsExecutor/feedDecorator.ts +6 -6
  3. package/adsUtils/index.ts +2 -2
  4. package/analyticsUtils/README.md +1 -1
  5. package/appUtils/HooksManager/index.ts +10 -10
  6. package/appUtils/accessibilityManager/__tests__/utils.test.ts +360 -0
  7. package/appUtils/accessibilityManager/const.ts +4 -0
  8. package/appUtils/accessibilityManager/hooks.ts +20 -13
  9. package/appUtils/accessibilityManager/index.ts +28 -1
  10. package/appUtils/accessibilityManager/utils.ts +59 -8
  11. package/appUtils/focusManager/index.ios.ts +8 -2
  12. package/appUtils/focusManagerAux/utils/index.ts +1 -1
  13. package/appUtils/focusManagerAux/utils/utils.ios.ts +60 -3
  14. package/appUtils/keyCodes/keys/keys.web.ts +1 -4
  15. package/appUtils/orientationHelper.ts +2 -4
  16. package/appUtils/platform/platformUtils.ts +117 -18
  17. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +94 -4
  18. package/appUtils/playerManager/OverlayObserver/utils.ts +32 -20
  19. package/appUtils/playerManager/player.ts +4 -0
  20. package/appUtils/playerManager/playerNative.ts +29 -16
  21. package/appUtils/playerManager/usePlayerState.tsx +14 -2
  22. package/cellUtils/index.ts +32 -0
  23. package/configurationUtils/__tests__/manifestKeyParser.test.ts +26 -26
  24. package/focusManager/aux/index.ts +1 -1
  25. package/manifestUtils/defaultManifestConfigurations/player.js +75 -1
  26. package/manifestUtils/keys.js +21 -0
  27. package/manifestUtils/sharedConfiguration/screenPicker/utils.js +1 -0
  28. package/manifestUtils/tvAction/container/index.js +1 -1
  29. package/package.json +2 -2
  30. package/playerUtils/usePlayerTTS.ts +8 -3
  31. package/pluginUtils/index.ts +4 -0
  32. package/reactHooks/advertising/index.ts +2 -2
  33. package/reactHooks/debugging/__tests__/index.test.js +4 -4
  34. package/reactHooks/device/useMemoizedIsTablet.ts +3 -3
  35. package/reactHooks/feed/__tests__/useEntryScreenId.test.tsx +3 -0
  36. package/reactHooks/feed/__tests__/{useInflatedUrl.test.ts → useInflatedUrl.test.tsx} +62 -7
  37. package/reactHooks/feed/useEntryScreenId.ts +2 -2
  38. package/reactHooks/feed/useInflatedUrl.ts +43 -17
  39. package/reactHooks/flatList/useLoadNextPageIfNeeded.ts +13 -16
  40. package/reactHooks/layout/index.ts +1 -1
  41. package/reactHooks/layout/useDimensions/__tests__/{useDimensions.test.ts → useDimensions.test.tsx} +105 -25
  42. package/reactHooks/layout/useDimensions/useDimensions.ts +2 -2
  43. package/reactHooks/navigation/index.ts +7 -6
  44. package/reactHooks/navigation/useRoute.ts +8 -6
  45. package/reactHooks/player/TVSeekControlller/TVSeekController.ts +27 -10
  46. package/reactHooks/resolvers/useCellResolver.ts +6 -2
  47. package/reactHooks/resolvers/useComponentResolver.ts +8 -2
  48. package/reactHooks/screen/__tests__/useTargetScreenData.test.tsx +10 -2
  49. package/reactHooks/screen/useTargetScreenData.ts +4 -2
  50. package/reactHooks/state/useRivers.ts +1 -1
  51. package/reactHooks/usePluginConfiguration.ts +2 -2
  52. package/testUtils/index.tsx +29 -20
  53. package/utils/__tests__/mapAccum.test.ts +73 -0
  54. package/utils/__tests__/selectors.test.ts +124 -0
  55. package/utils/index.ts +14 -0
  56. package/utils/mapAccum.ts +23 -0
  57. package/utils/path.ts +6 -3
  58. package/utils/pathOr.ts +5 -1
  59. package/utils/selectors.ts +46 -0
  60. package/zappFrameworkUtils/HookCallback/callbackNavigationAction.ts +34 -11
  61. package/zappFrameworkUtils/HookCallback/hookCallbackManifestExtensions.config.js +1 -1
@@ -23,12 +23,9 @@ import {
23
23
  EntryResolver,
24
24
  resolveObjectValues,
25
25
  } from "../appUtils/contextKeysManager/contextResolver";
26
- import { useNavigation } from "../reactHooks";
26
+ import { useNavigation, useRivers } from "../reactHooks";
27
27
 
28
- import {
29
- useContentTypes,
30
- usePickFromState,
31
- } from "@applicaster/zapp-react-native-redux/hooks";
28
+ import { useContentTypes } from "@applicaster/zapp-react-native-redux/hooks";
32
29
  import { useSubscriberFor } from "../reactHooks/useSubscriberFor";
33
30
  import { APP_EVENTS } from "../appUtils/events";
34
31
  import {
@@ -278,7 +275,7 @@ export function withActionExecutor(Component) {
278
275
 
279
276
  return function ActionExecutorComponent(props: Props) {
280
277
  const navigator = useNavigation();
281
- const { rivers } = usePickFromState(["rivers"]);
278
+ const rivers = useRivers();
282
279
  const contentTypes = useContentTypes();
283
280
 
284
281
  const handlers = useMemo(() => {
@@ -27,7 +27,7 @@ function makeMultiSelect(feed: ZappFeed, key, decoratedFeed) {
27
27
  );
28
28
 
29
29
  const behavior = {
30
- ...feed.extensions?.["behavior"],
30
+ ...feed.extensions?.behavior,
31
31
  select_mode: "multi",
32
32
  current_selection: `@{${scope}/${key}}`,
33
33
  };
@@ -75,7 +75,7 @@ function makeSingleSelect(feed: ZappFeed, key, decoratedFeed) {
75
75
  );
76
76
 
77
77
  const behavior = {
78
- ...feed.extensions?.["behavior"],
78
+ ...feed.extensions?.behavior,
79
79
  select_mode: "single",
80
80
  current_selection: `@{${scope}/${key}}`,
81
81
  };
@@ -141,11 +141,11 @@ function makeSingleSelect(feed: ZappFeed, key, decoratedFeed) {
141
141
  }
142
142
 
143
143
  export const decorateFeed = (feed: ZappFeed) => {
144
- if (!(feed.extensions?.["role"] === "preference_editor")) {
144
+ if (!(feed.extensions?.role === "preference_editor")) {
145
145
  return feed;
146
146
  }
147
147
 
148
- const key = feed.extensions?.["preference_editor_options"]?.["key"];
148
+ const key = feed.extensions?.preference_editor_options?.key;
149
149
 
150
150
  if (!key) {
151
151
  log_error(
@@ -160,8 +160,8 @@ export const decorateFeed = (feed: ZappFeed) => {
160
160
  const decoratedFeed = R.clone(feed);
161
161
 
162
162
  const isSingleSelect =
163
- (feed.extensions?.["preference_editor_options"]?.select_mode ||
164
- feed.extensions?.["behavior"]?.select_mode) === "single";
163
+ (feed.extensions?.preference_editor_options?.select_mode ||
164
+ feed.extensions?.behavior?.select_mode) === "single";
165
165
 
166
166
  if (isSingleSelect) {
167
167
  return makeSingleSelect(feed, key, decoratedFeed);
package/adsUtils/index.ts CHANGED
@@ -33,10 +33,10 @@ function convertOffset(offset: any): string {
33
33
  }
34
34
 
35
35
  function createAdBreak(ad: AdMap): string {
36
- const offset = ad["offset"];
36
+ const offset = ad.offset;
37
37
  const id = offset.toString();
38
38
  const timestamp = convertOffset(offset);
39
- const url = ad["ad_url"].toString().trim();
39
+ const url = ad.ad_url.toString().trim();
40
40
 
41
41
  return `
42
42
  <vmap:AdBreak timeOffset="${timestamp}" breakType="linear" breakId="break-${id}">
@@ -388,7 +388,7 @@ export function AnalyticsProvider(props: ComponentWithChildrenProps) {
388
388
 
389
389
  ```ts
390
390
  export function useAnalytics(props: any): any {
391
- const { appData } = usePickFromState(["appData"]);
391
+ const appData = useAppData();
392
392
  const getAnalyticsFunctions = React.useContext(AnalyticsContext);
393
393
 
394
394
  const analyticsFunctions = React.useMemo(
@@ -230,7 +230,7 @@ export function HooksManager({
230
230
  function completeHook(hookPlugin, payload, callback) {
231
231
  logHookEvent(
232
232
  hooksManagerLogger.info,
233
- `completeHook: hook sequence completed successfully: ${hookPlugin["identifier"]}`,
233
+ `completeHook: hook sequence completed successfully: ${hookPlugin.identifier}`,
234
234
  {
235
235
  payload,
236
236
  hook: hookPlugin,
@@ -276,7 +276,7 @@ export function HooksManager({
276
276
  if (hookPlugin.isCancelled()) {
277
277
  logHookEvent(
278
278
  hooksManagerLogger.info,
279
- `hookCallback: hook was cancelled: ${hookPlugin["identifier"]}`,
279
+ `hookCallback: hook was cancelled: ${hookPlugin.identifier}`,
280
280
  {}
281
281
  );
282
282
 
@@ -305,7 +305,7 @@ export function HooksManager({
305
305
  if (!success) {
306
306
  logHookEvent(
307
307
  hooksManagerLogger.info,
308
- `hookCallback: hook was cancelled: ${hookPlugin["identifier"]}`,
308
+ `hookCallback: hook was cancelled: ${hookPlugin.identifier}`,
309
309
  {
310
310
  payload,
311
311
  hook: hookPlugin,
@@ -334,7 +334,7 @@ export function HooksManager({
334
334
  if (isHookInHomescreen && isHookFlowBlocker && cancelled) {
335
335
  logHookEvent(
336
336
  hooksManagerLogger.info,
337
- `hookCallback: send app to background, cancelled flow blocker hook ${hookPlugin["identifier"]} on home screen`,
337
+ `hookCallback: send app to background, cancelled flow blocker hook ${hookPlugin.identifier} on home screen`,
338
338
  {
339
339
  payload,
340
340
  hook: hookPlugin,
@@ -349,7 +349,7 @@ export function HooksManager({
349
349
  } else {
350
350
  logHookEvent(
351
351
  hooksManagerLogger.info,
352
- `hookCallback: hook successfully finished: ${hookPlugin["identifier"]}`,
352
+ `hookCallback: hook successfully finished: ${hookPlugin.identifier}`,
353
353
  {
354
354
  payload,
355
355
  hook: hookPlugin,
@@ -359,7 +359,7 @@ export function HooksManager({
359
359
  if (!callback) {
360
360
  logHookEvent(
361
361
  hooksManagerLogger.warn,
362
- `hookCallback: ${hookPlugin["identifier"]} is missing \`callback\`, using hookCallback(default one)`,
362
+ `hookCallback: ${hookPlugin.identifier} is missing \`callback\`, using hookCallback(default one)`,
363
363
  {
364
364
  hookPlugin,
365
365
  }
@@ -401,7 +401,7 @@ export function HooksManager({
401
401
 
402
402
  logHookEvent(
403
403
  hooksManagerLogger.info,
404
- `presentScreenHook: Presenting screen hook: ${hookPlugin["identifier"]}`,
404
+ `presentScreenHook: Presenting screen hook: ${hookPlugin.identifier}`,
405
405
  {
406
406
  hook: hookPlugin,
407
407
  payload,
@@ -421,7 +421,7 @@ export function HooksManager({
421
421
  hooksManager.executeHook = function (hookPlugin, payload, callback) {
422
422
  logHookEvent(
423
423
  hooksManagerLogger.info,
424
- `executeHook: ${hookPlugin["identifier"]}`,
424
+ `executeHook: ${hookPlugin.identifier}`,
425
425
  {
426
426
  hook: hookPlugin,
427
427
  payload,
@@ -433,7 +433,7 @@ export function HooksManager({
433
433
  } catch (error) {
434
434
  logHookEvent(
435
435
  hooksManagerLogger.error,
436
- `executeHook: error executing hook: ${hookPlugin["identifier"]} error: ${error.message}`,
436
+ `executeHook: error executing hook: ${hookPlugin.identifier} error: ${error.message}`,
437
437
  {
438
438
  hook: hookPlugin,
439
439
  payload,
@@ -460,7 +460,7 @@ export function HooksManager({
460
460
  try {
461
461
  logHookEvent(
462
462
  hooksManagerLogger.info,
463
- `runInBackground: Executing hook: ${hookPlugin["identifier"]}`,
463
+ `runInBackground: Executing hook: ${hookPlugin.identifier}`,
464
464
  {
465
465
  hook: hookPlugin,
466
466
  payload,
@@ -0,0 +1,360 @@
1
+ jest.mock("../../../logger", () => {
2
+ const mockLogError = jest.fn();
3
+
4
+ return {
5
+ createLogger: jest.fn(() => ({
6
+ log_error: mockLogError,
7
+ })),
8
+ __mockLogError: mockLogError, // Export for test access
9
+ };
10
+ });
11
+
12
+ import { calculateReadingTime } from "../utils";
13
+ // @ts-ignore - Access the mock
14
+ import { __mockLogError } from "../../../logger";
15
+
16
+ describe("calculateReadingTime", () => {
17
+ // Default parameters for reference
18
+ const DEFAULT_WPM = 140;
19
+ const DEFAULT_MIN_PAUSE = 500;
20
+ const DEFAULT_DELAY = 700;
21
+
22
+ beforeEach(() => {
23
+ (__mockLogError as jest.Mock).mockClear();
24
+ });
25
+
26
+ describe("Type Safety", () => {
27
+ it("should accept and process string input", () => {
28
+ const result = calculateReadingTime("Hello world");
29
+ expect(result).toBeGreaterThan(0);
30
+ expect(__mockLogError).not.toHaveBeenCalled();
31
+ });
32
+
33
+ it("should accept and process number input", () => {
34
+ const result = calculateReadingTime(12345);
35
+ expect(result).toBeGreaterThan(0);
36
+ expect(__mockLogError).not.toHaveBeenCalled();
37
+ });
38
+
39
+ it("should return 0 and log error for null", () => {
40
+ expect(calculateReadingTime(null as any)).toBe(0);
41
+
42
+ expect(__mockLogError).toHaveBeenCalledWith(
43
+ "Invalid text input for reading time calculation got: null"
44
+ );
45
+ });
46
+
47
+ it("should return 0 and log error for undefined", () => {
48
+ expect(calculateReadingTime(undefined as any)).toBe(0);
49
+
50
+ expect(__mockLogError).toHaveBeenCalledWith(
51
+ "Invalid text input for reading time calculation got: undefined"
52
+ );
53
+ });
54
+
55
+ it("should return 0 and log error for boolean", () => {
56
+ calculateReadingTime(true as any);
57
+
58
+ expect(__mockLogError).toHaveBeenCalledWith(
59
+ "Invalid text input for reading time calculation got: true"
60
+ );
61
+
62
+ (__mockLogError as jest.Mock).mockClear();
63
+
64
+ calculateReadingTime(false as any);
65
+
66
+ expect(__mockLogError).toHaveBeenCalledWith(
67
+ "Invalid text input for reading time calculation got: false"
68
+ );
69
+ });
70
+
71
+ it("should return 0 and log error for object", () => {
72
+ const obj = { text: "hello" };
73
+ calculateReadingTime(obj as any);
74
+
75
+ expect(__mockLogError).toHaveBeenCalledWith(
76
+ `Invalid text input for reading time calculation got: ${obj}`
77
+ );
78
+ });
79
+
80
+ it("should return 0 and log error for array", () => {
81
+ const arr = [1, 2, 3];
82
+ calculateReadingTime(arr as any);
83
+
84
+ expect(__mockLogError).toHaveBeenCalledWith(
85
+ `Invalid text input for reading time calculation got: ${arr}`
86
+ );
87
+ });
88
+
89
+ it("should return 0 and log error for function", () => {
90
+ const fn = () => "text";
91
+ calculateReadingTime(fn as any);
92
+ expect(__mockLogError).toHaveBeenCalled();
93
+
94
+ expect((__mockLogError as jest.Mock).mock.calls[0][0]).toContain(
95
+ "Invalid text input for reading time calculation got:"
96
+ );
97
+ });
98
+
99
+ it("should return 0 and log error for symbol", () => {
100
+ const sym = Symbol("test");
101
+ calculateReadingTime(sym as any);
102
+ expect(__mockLogError).toHaveBeenCalled();
103
+
104
+ expect((__mockLogError as jest.Mock).mock.calls[0][0]).toBe(
105
+ `Invalid text input for reading time calculation got: ${String(sym)}`
106
+ );
107
+ });
108
+ });
109
+
110
+ describe("Empty and Whitespace Handling", () => {
111
+ it("should return 0 for empty string", () => {
112
+ expect(calculateReadingTime("")).toBe(0);
113
+ });
114
+
115
+ it("should return 0 for whitespace-only string", () => {
116
+ expect(calculateReadingTime(" ")).toBe(0);
117
+ expect(calculateReadingTime("\n")).toBe(0);
118
+ expect(calculateReadingTime("\t")).toBe(0);
119
+ expect(calculateReadingTime(" \n\t ")).toBe(0);
120
+ });
121
+
122
+ it("should handle leading and trailing whitespace", () => {
123
+ const withWhitespace = calculateReadingTime(" hello ");
124
+ const withoutWhitespace = calculateReadingTime("hello");
125
+ expect(withWhitespace).toBe(withoutWhitespace);
126
+ });
127
+ });
128
+
129
+ describe("Number Input Handling", () => {
130
+ it("should convert number 0 to string and process", () => {
131
+ const result = calculateReadingTime(0);
132
+ // "0" is one word
133
+ expect(result).toBeGreaterThan(0);
134
+ });
135
+
136
+ it("should convert positive numbers to string", () => {
137
+ const result = calculateReadingTime(123);
138
+ // "123" is one word
139
+ expect(result).toBeGreaterThan(0);
140
+ });
141
+
142
+ it("should convert negative numbers to string", () => {
143
+ const result = calculateReadingTime(-456);
144
+ // "-456" is processed as words
145
+ expect(result).toBeGreaterThan(0);
146
+ });
147
+
148
+ it("should convert decimal numbers to string", () => {
149
+ const result = calculateReadingTime(3.14);
150
+ // "3.14" is processed as words
151
+ expect(result).toBeGreaterThan(0);
152
+ });
153
+
154
+ it("should handle NaN", () => {
155
+ const result = calculateReadingTime(NaN);
156
+ // NaN is typeof "number", so it converts to "NaN" string
157
+ expect(result).toBeGreaterThan(0);
158
+ });
159
+
160
+ it("should handle Infinity", () => {
161
+ const result = calculateReadingTime(Infinity);
162
+ // Infinity is typeof "number", converts to "Infinity" string
163
+ expect(result).toBeGreaterThan(0);
164
+ });
165
+ });
166
+
167
+ describe("Word Counting", () => {
168
+ it("should count single word", () => {
169
+ const result = calculateReadingTime("Hello");
170
+
171
+ const expectedTime = Math.max(
172
+ DEFAULT_MIN_PAUSE,
173
+ (1 / DEFAULT_WPM) * 60 * 1000
174
+ );
175
+
176
+ expect(result).toBe(expectedTime + DEFAULT_DELAY);
177
+ });
178
+
179
+ it("should count multiple words separated by spaces", () => {
180
+ const result = calculateReadingTime("Hello world test");
181
+
182
+ // 3 words
183
+ const expectedTime = Math.max(
184
+ DEFAULT_MIN_PAUSE,
185
+ (3 / DEFAULT_WPM) * 60 * 1000
186
+ );
187
+
188
+ expect(result).toBe(expectedTime + DEFAULT_DELAY);
189
+ });
190
+
191
+ it("should handle words with punctuation", () => {
192
+ const result = calculateReadingTime("Hello, world! How are you?");
193
+ // Should split on punctuation and count words
194
+ expect(result).toBeGreaterThan(DEFAULT_MIN_PAUSE + DEFAULT_DELAY);
195
+ });
196
+
197
+ it("should handle alphanumeric boundaries", () => {
198
+ const result = calculateReadingTime("test123abc");
199
+ // Should split on alphanumeric boundaries
200
+ expect(result).toBeGreaterThan(DEFAULT_MIN_PAUSE + DEFAULT_DELAY);
201
+ });
202
+
203
+ it("should handle long text", () => {
204
+ const longText = "word ".repeat(200); // 200 words
205
+ const result = calculateReadingTime(longText);
206
+ const expectedTime = (200 / DEFAULT_WPM) * 60 * 1000 + DEFAULT_DELAY;
207
+ expect(result).toBeCloseTo(expectedTime, -1); // Within 10ms
208
+ });
209
+ });
210
+
211
+ describe("Minimum Pause", () => {
212
+ it("should return minimum pause + delay for very short text", () => {
213
+ const result = calculateReadingTime("Hi");
214
+ // 1 word, calculation would be less than minimum pause
215
+ expect(result).toBe(DEFAULT_MIN_PAUSE + DEFAULT_DELAY);
216
+ });
217
+
218
+ it("should respect custom minimum pause", () => {
219
+ const customMinPause = 1000;
220
+ const result = calculateReadingTime("Hi", DEFAULT_WPM, customMinPause);
221
+ expect(result).toBeGreaterThanOrEqual(customMinPause);
222
+ });
223
+
224
+ it("should exceed minimum pause for longer text", () => {
225
+ const longText = "word ".repeat(50); // 50 words
226
+ const result = calculateReadingTime(longText);
227
+ const calculatedTime = (50 / DEFAULT_WPM) * 60 * 1000;
228
+ expect(result).toBe(calculatedTime + DEFAULT_DELAY);
229
+ expect(result).toBeGreaterThan(DEFAULT_MIN_PAUSE + DEFAULT_DELAY);
230
+ });
231
+ });
232
+
233
+ describe("Custom Parameters", () => {
234
+ it("should respect custom words per minute", () => {
235
+ const text = "word ".repeat(140); // 140 words
236
+ const fastReading = calculateReadingTime(text, 280); // 2x speed
237
+ const normalReading = calculateReadingTime(text, 140); // normal speed
238
+
239
+ // Faster reading should take less time
240
+ expect(fastReading).toBeLessThan(normalReading);
241
+ });
242
+
243
+ it("should respect custom announcement delay", () => {
244
+ const text = "Hello world";
245
+
246
+ const shortDelay = calculateReadingTime(
247
+ text,
248
+ DEFAULT_WPM,
249
+ DEFAULT_MIN_PAUSE,
250
+ 100
251
+ );
252
+
253
+ const longDelay = calculateReadingTime(
254
+ text,
255
+ DEFAULT_WPM,
256
+ DEFAULT_MIN_PAUSE,
257
+ 1000
258
+ );
259
+
260
+ expect(longDelay - shortDelay).toBe(900);
261
+ });
262
+
263
+ it("should work with all custom parameters", () => {
264
+ const result = calculateReadingTime("test", 200, 1000, 500);
265
+ expect(result).toBeGreaterThanOrEqual(1500); // minimum pause + delay
266
+ });
267
+ });
268
+
269
+ describe("Real-world Use Cases", () => {
270
+ it("should handle accessibility announcement text", () => {
271
+ const announcement = "New message from John Doe";
272
+ const result = calculateReadingTime(announcement);
273
+ expect(result).toBeGreaterThan(0);
274
+ expect(result).toBeLessThan(10000); // Less than 10 seconds
275
+ });
276
+
277
+ it("should handle button labels", () => {
278
+ const label = "Submit";
279
+ const result = calculateReadingTime(label);
280
+ expect(result).toBe(DEFAULT_MIN_PAUSE + DEFAULT_DELAY);
281
+ });
282
+
283
+ it("should handle form error messages", () => {
284
+ const error = "Please enter a valid email address";
285
+ const result = calculateReadingTime(error);
286
+ expect(result).toBeGreaterThan(DEFAULT_MIN_PAUSE);
287
+ });
288
+
289
+ it("should handle article titles", () => {
290
+ const title = "Breaking News: Major Update Released";
291
+ const result = calculateReadingTime(title);
292
+ expect(result).toBeGreaterThan(DEFAULT_MIN_PAUSE + DEFAULT_DELAY);
293
+ });
294
+
295
+ it("should handle notification text", () => {
296
+ const notification = "You have 3 new messages";
297
+ const result = calculateReadingTime(notification);
298
+ expect(result).toBeGreaterThan(0);
299
+ });
300
+ });
301
+
302
+ describe("Edge Cases", () => {
303
+ it("should handle text with special characters", () => {
304
+ const result = calculateReadingTime("@#$%^&*()");
305
+ expect(result).toBeGreaterThanOrEqual(0);
306
+ });
307
+
308
+ it("should handle text with emojis", () => {
309
+ const result = calculateReadingTime("Hello 👋 World 🌍");
310
+ expect(result).toBeGreaterThan(0);
311
+ });
312
+
313
+ it("should handle text with newlines", () => {
314
+ const result = calculateReadingTime("Line 1\nLine 2\nLine 3");
315
+ expect(result).toBeGreaterThan(0);
316
+ });
317
+
318
+ it("should handle mixed alphanumeric text", () => {
319
+ const result = calculateReadingTime(
320
+ "Version 1.2.3 released on 2024-01-01"
321
+ );
322
+
323
+ expect(result).toBeGreaterThan(0);
324
+ });
325
+
326
+ it("should handle very large numbers", () => {
327
+ const result = calculateReadingTime(Number.MAX_SAFE_INTEGER);
328
+ expect(result).toBeGreaterThan(0);
329
+ });
330
+
331
+ it("should return consistent results for same input", () => {
332
+ const text = "Consistent test";
333
+ const result1 = calculateReadingTime(text);
334
+ const result2 = calculateReadingTime(text);
335
+ const result3 = calculateReadingTime(text);
336
+
337
+ expect(result1).toBe(result2);
338
+ expect(result2).toBe(result3);
339
+ });
340
+ });
341
+
342
+ describe("Performance Characteristics", () => {
343
+ it("should handle empty input efficiently", () => {
344
+ const start = Date.now();
345
+ calculateReadingTime("");
346
+ const duration = Date.now() - start;
347
+
348
+ expect(duration).toBeLessThan(10); // Should be nearly instant
349
+ });
350
+
351
+ it("should handle large text efficiently", () => {
352
+ const largeText = "word ".repeat(10000);
353
+ const start = Date.now();
354
+ calculateReadingTime(largeText);
355
+ const duration = Date.now() - start;
356
+
357
+ expect(duration).toBeLessThan(100); // Should complete in less than 100ms
358
+ });
359
+ });
360
+ });
@@ -31,6 +31,10 @@ export const BUTTON_ACCESSIBILITY_KEYS = {
31
31
  hint: "accessibility_close_mini_hint",
32
32
  },
33
33
  },
34
+ back_to_live: {
35
+ label: "back_to_live_label",
36
+ hint: "",
37
+ },
34
38
  maximize: {
35
39
  label: "accessibility_maximize_label",
36
40
  hint: "accessibility_maximize_hint",
@@ -23,19 +23,6 @@ export const useAccessibilityManager = (
23
23
  }
24
24
  }, [pluginConfiguration, accessibilityManager]);
25
25
 
26
- useEffect(() => {
27
- const subscription = accessibilityManager.getStateAsObservable().subscribe({
28
- next: () => {
29
- // TODO: handle accessibility states
30
- // screenReaderEnabled: false
31
- // reduceMotionEnabled: false
32
- // boldTextEnabled: false
33
- },
34
- });
35
-
36
- return () => subscription.unsubscribe();
37
- }, [accessibilityManager]);
38
-
39
26
  return accessibilityManager;
40
27
  };
41
28
 
@@ -72,3 +59,23 @@ export const useAnnouncementActive = (
72
59
 
73
60
  return isActive;
74
61
  };
62
+
63
+ export const useAccessibilityState = (
64
+ pluginConfiguration: Record<string, any> = {}
65
+ ) => {
66
+ const accessibilityManager = useAccessibilityManager(pluginConfiguration);
67
+
68
+ const [state, setState] = useState<AccessibilityState>(
69
+ accessibilityManager.getState()
70
+ );
71
+
72
+ useEffect(() => {
73
+ const subscription = accessibilityManager
74
+ .getStateAsObservable()
75
+ .subscribe(setState);
76
+
77
+ return () => subscription.unsubscribe();
78
+ }, [accessibilityManager]);
79
+
80
+ return state;
81
+ };
@@ -36,7 +36,20 @@ export class AccessibilityManager {
36
36
  false
37
37
  );
38
38
 
39
- private constructor() {}
39
+ private constructor() {
40
+ this.ttsManager
41
+ .getScreenReaderEnabledAsObservable()
42
+ .subscribe((enabled) => {
43
+ const state = this.state$.getValue();
44
+
45
+ if (state.screenReaderEnabled !== enabled) {
46
+ this.state$.next({
47
+ ...state,
48
+ screenReaderEnabled: enabled,
49
+ });
50
+ }
51
+ });
52
+ }
40
53
 
41
54
  public static getInstance(): AccessibilityManager {
42
55
  if (!AccessibilityManager._instance) {
@@ -92,8 +105,15 @@ export class AccessibilityManager {
92
105
  /**
93
106
  * Adds a heading to the queue, headings will be read before the next text
94
107
  * Each heading will be read once and removed from the queue
108
+ * Does nothing if screen reader is not enabled
95
109
  */
96
110
  public addHeading(heading: string) {
111
+ const state = this.state$.getValue();
112
+
113
+ if (!state.screenReaderEnabled) {
114
+ return;
115
+ }
116
+
97
117
  if (!this.pendingFocusId) {
98
118
  this.pendingFocusId = Date.now().toString();
99
119
  }
@@ -108,6 +128,7 @@ export class AccessibilityManager {
108
128
  *
109
129
  * Implements a delay mechanism to reduce noise during rapid navigation.
110
130
  * Only the most recent announcement will be read after the delay period.
131
+ * Does nothing if screen reader is not enabled
111
132
  */
112
133
  public readText({
113
134
  text,
@@ -116,6 +137,12 @@ export class AccessibilityManager {
116
137
  text: string;
117
138
  keyOfLocalizedText?: string;
118
139
  }) {
140
+ const state = this.state$.getValue();
141
+
142
+ if (!state.screenReaderEnabled) {
143
+ return;
144
+ }
145
+
119
146
  let textToRead = text;
120
147
 
121
148
  if (keyOfLocalizedText) {