@applicaster/zapp-react-native-utils 15.0.0-alpha.3512356987 → 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.
- package/actionsExecutor/ActionExecutorContext.tsx +3 -6
- package/actionsExecutor/feedDecorator.ts +6 -6
- package/adsUtils/index.ts +2 -2
- package/analyticsUtils/README.md +1 -1
- package/appUtils/HooksManager/index.ts +10 -10
- package/appUtils/accessibilityManager/__tests__/utils.test.ts +360 -0
- package/appUtils/accessibilityManager/const.ts +4 -0
- package/appUtils/accessibilityManager/hooks.ts +20 -13
- package/appUtils/accessibilityManager/index.ts +28 -1
- package/appUtils/accessibilityManager/utils.ts +25 -5
- package/appUtils/focusManager/index.ios.ts +8 -2
- package/appUtils/focusManagerAux/utils/index.ts +1 -1
- package/appUtils/focusManagerAux/utils/utils.ios.ts +60 -3
- package/appUtils/keyCodes/keys/keys.web.ts +1 -4
- package/appUtils/orientationHelper.ts +2 -4
- package/appUtils/platform/platformUtils.ts +115 -16
- package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +94 -4
- package/appUtils/playerManager/OverlayObserver/utils.ts +32 -20
- package/appUtils/playerManager/player.ts +4 -0
- package/appUtils/playerManager/playerNative.ts +29 -16
- package/appUtils/playerManager/usePlayerState.tsx +14 -2
- package/cellUtils/index.ts +1 -8
- package/configurationUtils/__tests__/manifestKeyParser.test.ts +26 -26
- package/focusManager/aux/index.ts +1 -1
- package/manifestUtils/defaultManifestConfigurations/player.js +75 -1
- package/manifestUtils/keys.js +21 -0
- package/manifestUtils/sharedConfiguration/screenPicker/utils.js +1 -0
- package/manifestUtils/tvAction/container/index.js +1 -1
- package/package.json +2 -2
- package/playerUtils/usePlayerTTS.ts +8 -3
- package/pluginUtils/index.ts +4 -0
- package/reactHooks/advertising/index.ts +2 -2
- package/reactHooks/debugging/__tests__/index.test.js +4 -4
- package/reactHooks/device/useMemoizedIsTablet.ts +3 -3
- package/reactHooks/feed/__tests__/useEntryScreenId.test.tsx +3 -0
- package/reactHooks/feed/__tests__/{useInflatedUrl.test.ts → useInflatedUrl.test.tsx} +62 -7
- package/reactHooks/feed/useEntryScreenId.ts +2 -2
- package/reactHooks/feed/useInflatedUrl.ts +43 -17
- package/reactHooks/flatList/useLoadNextPageIfNeeded.ts +13 -16
- package/reactHooks/layout/useDimensions/__tests__/{useDimensions.test.ts → useDimensions.test.tsx} +105 -25
- package/reactHooks/layout/useDimensions/useDimensions.ts +2 -2
- package/reactHooks/navigation/index.ts +7 -6
- package/reactHooks/navigation/useRoute.ts +8 -6
- package/reactHooks/player/TVSeekControlller/TVSeekController.ts +27 -10
- package/reactHooks/resolvers/useCellResolver.ts +6 -2
- package/reactHooks/resolvers/useComponentResolver.ts +8 -2
- package/reactHooks/screen/__tests__/useTargetScreenData.test.tsx +10 -2
- package/reactHooks/screen/useTargetScreenData.ts +4 -2
- package/reactHooks/state/useRivers.ts +1 -1
- package/reactHooks/usePluginConfiguration.ts +2 -2
- package/testUtils/index.tsx +29 -20
- package/utils/__tests__/selectors.test.ts +124 -0
- package/utils/index.ts +10 -0
- package/utils/path.ts +6 -3
- package/utils/pathOr.ts +5 -1
- package/utils/selectors.ts +46 -0
- package/zappFrameworkUtils/HookCallback/callbackNavigationAction.ts +34 -11
- 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
|
|
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?.
|
|
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?.
|
|
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?.
|
|
144
|
+
if (!(feed.extensions?.role === "preference_editor")) {
|
|
145
145
|
return feed;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
const key = feed.extensions?.
|
|
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?.
|
|
164
|
-
feed.extensions?.
|
|
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
|
|
36
|
+
const offset = ad.offset;
|
|
37
37
|
const id = offset.toString();
|
|
38
38
|
const timestamp = convertOffset(offset);
|
|
39
|
-
const url = ad
|
|
39
|
+
const url = ad.ad_url.toString().trim();
|
|
40
40
|
|
|
41
41
|
return `
|
|
42
42
|
<vmap:AdBreak timeOffset="${timestamp}" breakType="linear" breakId="break-${id}">
|
package/analyticsUtils/README.md
CHANGED
|
@@ -388,7 +388,7 @@ export function AnalyticsProvider(props: ComponentWithChildrenProps) {
|
|
|
388
388
|
|
|
389
389
|
```ts
|
|
390
390
|
export function useAnalytics(props: any): any {
|
|
391
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) {
|