@applicaster/zapp-react-native-utils 14.0.0-alpha.2482261241 → 14.0.0-alpha.2802628909

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 (46) hide show
  1. package/analyticsUtils/AnalyticPlayerListener.ts +5 -2
  2. package/analyticsUtils/playerAnalyticsTracker.ts +2 -1
  3. package/appUtils/accessibilityManager/const.ts +13 -0
  4. package/appUtils/accessibilityManager/hooks.ts +35 -1
  5. package/appUtils/accessibilityManager/index.ts +151 -30
  6. package/appUtils/accessibilityManager/utils.ts +24 -0
  7. package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +4 -1
  8. package/appUtils/focusManager/events.ts +2 -0
  9. package/appUtils/focusManager/index.ios.ts +18 -1
  10. package/appUtils/focusManager/index.ts +31 -27
  11. package/appUtils/focusManagerAux/utils/index.ts +20 -14
  12. package/appUtils/focusManagerAux/utils/utils.ios.ts +35 -0
  13. package/appUtils/platform/platformUtils.ts +33 -3
  14. package/appUtils/playerManager/conts.ts +21 -0
  15. package/arrayUtils/__tests__/allTruthy.test.ts +24 -0
  16. package/arrayUtils/__tests__/anyThruthy.test.ts +24 -0
  17. package/arrayUtils/index.ts +5 -0
  18. package/configurationUtils/index.ts +1 -1
  19. package/focusManager/FocusManager.ts +78 -4
  20. package/focusManager/aux/index.ts +98 -0
  21. package/focusManager/utils.ts +12 -6
  22. package/index.d.ts +1 -1
  23. package/manifestUtils/defaultManifestConfigurations/player.js +188 -2
  24. package/manifestUtils/index.js +4 -0
  25. package/manifestUtils/keys.js +12 -0
  26. package/manifestUtils/sharedConfiguration/screenPicker/stylesFields.js +6 -0
  27. package/navigationUtils/index.ts +20 -17
  28. package/package.json +2 -2
  29. package/playerUtils/PlayerTTS/PlayerTTS.ts +359 -0
  30. package/playerUtils/PlayerTTS/index.ts +1 -0
  31. package/playerUtils/getPlayerActionButtons.ts +1 -1
  32. package/playerUtils/usePlayerTTS.ts +21 -0
  33. package/reactHooks/feed/useFeedLoader.tsx +0 -9
  34. package/reactHooks/feed/useInflatedUrl.ts +23 -29
  35. package/reactHooks/feed/useLoadPipesDataDispatch.ts +7 -1
  36. package/reactHooks/layout/index.ts +1 -1
  37. package/screenPickerUtils/index.ts +6 -0
  38. package/utils/__tests__/endsWith.test.ts +30 -0
  39. package/utils/__tests__/omit.test.ts +19 -0
  40. package/utils/__tests__/path.test.ts +33 -0
  41. package/utils/__tests__/take.test.ts +40 -0
  42. package/utils/endsWith.ts +9 -0
  43. package/utils/index.ts +12 -1
  44. package/utils/omit.ts +5 -0
  45. package/utils/path.ts +5 -0
  46. package/utils/take.ts +5 -0
@@ -0,0 +1,359 @@
1
+ import uuidv4 from "uuid/v4";
2
+ import { AccessibilityManager } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager";
3
+ import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
4
+ import { Player } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/player";
5
+
6
+ const { log_debug, log_error } = createLogger({
7
+ subsystem: "Player",
8
+ category: "PlayerTTS",
9
+ });
10
+
11
+ enum SEEK_DIRECTION {
12
+ FORWARD = "forward",
13
+ REWIND = "back",
14
+ }
15
+
16
+ const hasPrerollAds = (entry: ZappEntry): boolean => {
17
+ const videoAds = entry?.extensions?.video_ads;
18
+
19
+ if (!videoAds) {
20
+ return false;
21
+ }
22
+
23
+ // If it's a string (VMAP URL), assume it might have preroll
24
+ if (typeof videoAds === "string") {
25
+ return true;
26
+ }
27
+
28
+ // If it's an array, check for preroll offset
29
+ if (Array.isArray(videoAds)) {
30
+ return videoAds.some(
31
+ (ad: ZappVideoAdExtension) => ad.offset === "preroll" || ad.offset === 0
32
+ );
33
+ }
34
+
35
+ return false;
36
+ };
37
+
38
+ export class PlayerTTS {
39
+ private player: Player;
40
+ private accessibilityManager: AccessibilityManager;
41
+ private seekStartPosition: number | null = null;
42
+ private isSeeking: boolean = false;
43
+ private listenerId: string;
44
+ private isInitialPlayerOpen: boolean = true;
45
+ private isPrerollActive: boolean = false;
46
+ private hasPrerollAds: boolean = false; // Track if preroll ads are expected
47
+
48
+ constructor(player: Player, accessibilityManager: AccessibilityManager) {
49
+ this.player = player;
50
+ this.accessibilityManager = accessibilityManager;
51
+ this.listenerId = `player-tts-${uuidv4()}`;
52
+ this.hasPrerollAds = hasPrerollAds(player.entry);
53
+
54
+ log_debug("PlayerTTS initialized", {
55
+ hasPrerollAds: this.hasPrerollAds,
56
+ listenerId: this.listenerId,
57
+ entryTitle: player.entry.title,
58
+ });
59
+ }
60
+
61
+ private numberToWords(num: number): string {
62
+ const ones = [
63
+ "",
64
+ "one",
65
+ "two",
66
+ "three",
67
+ "four",
68
+ "five",
69
+ "six",
70
+ "seven",
71
+ "eight",
72
+ "nine",
73
+ "ten",
74
+ "eleven",
75
+ "twelve",
76
+ "thirteen",
77
+ "fourteen",
78
+ "fifteen",
79
+ "sixteen",
80
+ "seventeen",
81
+ "eighteen",
82
+ "nineteen",
83
+ ];
84
+
85
+ const tens = [
86
+ "",
87
+ "",
88
+ "twenty",
89
+ "thirty",
90
+ "forty",
91
+ "fifty",
92
+ "sixty",
93
+ "seventy",
94
+ "eighty",
95
+ "ninety",
96
+ ];
97
+
98
+ if (num === 0) return "zero";
99
+ if (num < 20) return ones[num];
100
+
101
+ const ten = Math.floor(num / 10);
102
+ const one = num % 10;
103
+
104
+ return one === 0 ? tens[ten] : `${tens[ten]} ${ones[one]}`;
105
+ }
106
+
107
+ private secondsToTime(
108
+ seconds: number,
109
+ format: "natural" | "standard" = "natural"
110
+ ): string {
111
+ if (seconds < 0) return format === "natural" ? "zero" : "0";
112
+
113
+ const minutes = Math.floor(seconds / 60);
114
+ const remainingSeconds = Math.floor(seconds % 60);
115
+
116
+ if (format === "standard") {
117
+ const parts = [];
118
+
119
+ if (minutes > 0) {
120
+ parts.push(`${minutes} minute${minutes !== 1 ? "s" : ""}`);
121
+ }
122
+
123
+ if (remainingSeconds > 0) {
124
+ parts.push(
125
+ `${remainingSeconds} second${remainingSeconds !== 1 ? "s" : ""}`
126
+ );
127
+ }
128
+
129
+ return parts.length > 0 ? parts.join(" ") : "0";
130
+ } else {
131
+ if (minutes === 0) {
132
+ if (remainingSeconds === 0) return "zero";
133
+
134
+ if (remainingSeconds < 10) {
135
+ return `zero o ${this.numberToWords(remainingSeconds)}`;
136
+ }
137
+
138
+ return `zero ${this.numberToWords(remainingSeconds)}`;
139
+ }
140
+
141
+ if (remainingSeconds === 0) {
142
+ return `${this.numberToWords(minutes)}`;
143
+ }
144
+
145
+ if (remainingSeconds < 10) {
146
+ return `${this.numberToWords(minutes)} o ${this.numberToWords(remainingSeconds)}`;
147
+ }
148
+
149
+ return `${this.numberToWords(minutes)} ${this.numberToWords(remainingSeconds)}`;
150
+ }
151
+ }
152
+
153
+ private announcePause = () => {
154
+ if (!this.isSeeking) {
155
+ this.accessibilityManager.addHeading(
156
+ `Paused - ${this.secondsToTime(this.player.playerState.contentPosition, "standard")}`
157
+ );
158
+ }
159
+ };
160
+
161
+ private announceContentStart(
162
+ options: {
163
+ currentTime?: number;
164
+ duration?: number;
165
+ useReadText?: boolean;
166
+ } = {}
167
+ ): void {
168
+ const { currentTime, duration, useReadText = false } = options;
169
+ const state = this.player.playerState;
170
+
171
+ const timeRemaining =
172
+ (duration || state?.contentDuration || 0) -
173
+ (currentTime || state?.contentPosition || 0);
174
+
175
+ const title = (this.player.entry.title as string) || "";
176
+ const summary = (this.player.entry.summary as string) || "";
177
+
178
+ log_debug("Announcing content start", {
179
+ title,
180
+ currentTime: currentTime || state?.contentPosition || 0,
181
+ duration: duration || state?.contentDuration || 0,
182
+ timeRemaining,
183
+ useReadText,
184
+ });
185
+
186
+ this.accessibilityManager.addHeading(`Playing - ${title}`);
187
+ if (summary) this.accessibilityManager.addHeading(summary);
188
+
189
+ this.accessibilityManager.addHeading(
190
+ `Playing from ${this.secondsToTime(currentTime || state?.contentPosition || 0, "standard")}`
191
+ );
192
+
193
+ const remainingText = `${this.secondsToTime(Math.max(0, Math.floor(timeRemaining)), "standard")} remaining.`;
194
+
195
+ if (useReadText) {
196
+ this.accessibilityManager.readText({ text: remainingText });
197
+ } else {
198
+ this.accessibilityManager.addHeading(remainingText);
199
+ }
200
+
201
+ this.accessibilityManager.setInitialPlayerAnnouncementReady();
202
+ this.isInitialPlayerOpen = false;
203
+ }
204
+
205
+ private announceBufferComplete = (event: any) => {
206
+ // If preroll ads are expected, wait for them to finish before announcing content
207
+ if (this.hasPrerollAds && this.isInitialPlayerOpen) {
208
+ log_debug("Waiting for preroll ads to finish", {
209
+ hasPrerollAds: this.hasPrerollAds,
210
+ isInitialPlayerOpen: this.isInitialPlayerOpen,
211
+ });
212
+
213
+ return;
214
+ }
215
+
216
+ // Gate content announcement until preroll finishes
217
+ if (this.isInitialPlayerOpen && !this.isPrerollActive) {
218
+ log_debug("Buffer complete - announcing content", {
219
+ currentTime: event.currentTime,
220
+ duration: event.duration,
221
+ isPrerollActive: this.isPrerollActive,
222
+ });
223
+
224
+ this.announceContentStart({
225
+ currentTime: event.currentTime,
226
+ duration: event.duration,
227
+ });
228
+ }
229
+ };
230
+
231
+ private announceResume = () => {
232
+ if (!this.isSeeking && !this.isInitialPlayerOpen) {
233
+ log_debug("Player resumed", {
234
+ contentPosition: this.player.playerState.contentPosition,
235
+ isSeeking: this.isSeeking,
236
+ isInitialPlayerOpen: this.isInitialPlayerOpen,
237
+ });
238
+
239
+ this.accessibilityManager.addHeading(
240
+ `Playing - ${this.secondsToTime(this.player.playerState.contentPosition, "standard")}`
241
+ );
242
+ }
243
+ };
244
+
245
+ private handleVideoProgress = (event: any) => {
246
+ if (event.currentTime > 0) {
247
+ this.seekStartPosition = event.currentTime;
248
+ }
249
+ };
250
+
251
+ private handleSeekComplete = (event: any) => {
252
+ if (this.seekStartPosition !== null) {
253
+ const seekDirection =
254
+ event.currentTime > this.seekStartPosition
255
+ ? SEEK_DIRECTION.FORWARD
256
+ : SEEK_DIRECTION.REWIND;
257
+
258
+ const seekAmount = Math.round(
259
+ Math.abs(event.currentTime - this.seekStartPosition)
260
+ );
261
+
262
+ log_debug("Seek completed", {
263
+ seekDirection,
264
+ seekAmount,
265
+ fromPosition: this.seekStartPosition,
266
+ toPosition: event.currentTime,
267
+ });
268
+
269
+ this.accessibilityManager.readText({
270
+ text: `Skipped ${seekDirection} ${this.secondsToTime(seekAmount, "standard")}`,
271
+ });
272
+
273
+ this.seekStartPosition = event.currentTime;
274
+ }
275
+
276
+ this.isSeeking = false;
277
+ };
278
+
279
+ private handleSeekStart = () => {
280
+ log_debug("Seek started");
281
+ this.isSeeking = true;
282
+ };
283
+
284
+ private handlePlayerClose = () => {
285
+ log_debug("Player closed - resetting state");
286
+ this.isInitialPlayerOpen = true;
287
+ this.accessibilityManager.resetInitialPlayerAnnouncementReady();
288
+ };
289
+
290
+ private announceAdBegin = (event: any) => {
291
+ this.isPrerollActive = true;
292
+
293
+ log_debug("Ad started", {
294
+ adDuration: event?.ad?.data?.duration,
295
+ isPrerollActive: this.isPrerollActive,
296
+ });
297
+
298
+ if (event?.ad?.data?.duration) {
299
+ this.accessibilityManager.readText({
300
+ text: `Sponsored. Ends in ${this.secondsToTime(event.ad.data.duration, "standard")}`,
301
+ });
302
+ }
303
+ };
304
+
305
+ private handleAdEnd = (_event: any) => {
306
+ this.isPrerollActive = false;
307
+
308
+ log_debug("Ad ended", {
309
+ isPrerollActive: this.isPrerollActive,
310
+ isInitialPlayerOpen: this.isInitialPlayerOpen,
311
+ });
312
+
313
+ // If initial entry still pending, trigger content announcement using latest player state
314
+ if (this.isInitialPlayerOpen) {
315
+ this.announceContentStart({ useReadText: true });
316
+ }
317
+ };
318
+
319
+ public init(): () => void {
320
+ if (!this.player) {
321
+ log_error("Failed to initialize PlayerTTS - no player provided");
322
+
323
+ return () => {};
324
+ }
325
+
326
+ log_debug("Initializing PlayerTTS listeners", {
327
+ listenerId: this.listenerId,
328
+ });
329
+
330
+ return this.player.addListener({
331
+ id: this.listenerId,
332
+ listener: {
333
+ onBufferComplete: this.announceBufferComplete,
334
+ onPlayerResume: this.announceResume,
335
+ onPlayerPause: this.announcePause,
336
+ onVideoProgress: this.handleVideoProgress,
337
+ onPlayerSeekStart: this.handleSeekStart,
338
+ onPlayerSeekComplete: this.handleSeekComplete,
339
+ onPlayerClose: this.handlePlayerClose,
340
+ onAdBegin: this.announceAdBegin,
341
+ onAdEnd: this.handleAdEnd,
342
+ onAdBreakEnd: this.handleAdEnd,
343
+ },
344
+ });
345
+ }
346
+
347
+ public destroy(): void {
348
+ log_debug("Destroying PlayerTTS", {
349
+ listenerId: this.listenerId,
350
+ });
351
+
352
+ if (this.player) {
353
+ this.player.removeListener(this.listenerId);
354
+ }
355
+
356
+ this.seekStartPosition = null;
357
+ this.handlePlayerClose();
358
+ }
359
+ }
@@ -0,0 +1 @@
1
+ export { PlayerTTS } from "./PlayerTTS";
@@ -13,5 +13,5 @@ export const getPlayerActionButtons = (configuration: any) => {
13
13
  return [];
14
14
  }
15
15
 
16
- return take(map(buttonsString.split(","), trim), 2);
16
+ return take(2, map(buttonsString.split(","), trim));
17
17
  };
@@ -0,0 +1,21 @@
1
+ import * as React from "react";
2
+ import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer";
3
+ import { useAccessibilityManager } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager/hooks";
4
+ import { PlayerTTS } from "@applicaster/zapp-react-native-utils/playerUtils/PlayerTTS";
5
+
6
+ export const usePlayerTTS = () => {
7
+ const player = usePlayer();
8
+ const accessibilityManager = useAccessibilityManager({});
9
+
10
+ React.useEffect(() => {
11
+ if (player && accessibilityManager) {
12
+ const playerTTS = new PlayerTTS(player, accessibilityManager);
13
+ const unsubscribe = playerTTS.init();
14
+
15
+ return () => {
16
+ unsubscribe();
17
+ playerTTS.destroy();
18
+ };
19
+ }
20
+ }, [player, accessibilityManager]);
21
+ };
@@ -37,15 +37,6 @@ export const useFeedLoader = ({
37
37
  mapping,
38
38
  pipesOptions = {},
39
39
  }: Props): FeedLoaderResponse => {
40
- useEffect(() => {
41
- if (!feedUrl) {
42
- logger.warning({
43
- message: "Required parameter feedUrl is missing",
44
- data: { feedUrl },
45
- });
46
- }
47
- }, []);
48
-
49
40
  const isInitialRender = useIsInitialRender();
50
41
 
51
42
  const callableFeedUrl = useInflatedUrl({ feedUrl, mapping });
@@ -18,6 +18,7 @@ import {
18
18
  } from "@applicaster/zapp-pipes-v2-client";
19
19
  import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
20
20
  import { ENDPOINT_TAGS } from "../../types";
21
+ import { isNilOrEmpty } from "../../reactUtils/helpers";
21
22
 
22
23
  /**
23
24
  * will match any occurrence in a string of one or more word characters
@@ -75,15 +76,19 @@ export const getInflatedDataSourceUrl: GetInflatedDataSourceUrl = ({
75
76
  * https://foo.com/shows/A1234
76
77
  */
77
78
 
78
- if (!source) {
79
- // eslint-disable-next-line no-console
80
- console.error("source is empty", {
81
- source,
82
- contexts,
83
- mapping,
84
- });
79
+ if (!isNilOrEmpty(mapping)) {
80
+ if (!source) {
81
+ if (__DEV__) {
82
+ // eslint-disable-next-line no-console
83
+ throw new Error(
84
+ "getInflatedDataSourceUrl: source is empty while mapping is provided"
85
+ );
86
+ }
85
87
 
86
- return null;
88
+ return null;
89
+ }
90
+ } else {
91
+ return source || null;
87
92
  }
88
93
 
89
94
  // Hack because in tv we expect to get key names instead of values from the fake entry
@@ -193,28 +198,17 @@ export function useInflatedUrl({
193
198
 
194
199
  const url = useMemo(
195
200
  () =>
196
- mapping
197
- ? getInflatedDataSourceUrl({
198
- source: feedUrl,
199
- contexts: {
200
- entry: entryContext,
201
- screen: screenContext,
202
- search: getSearchContext(searchContext, mapping),
203
- },
204
- mapping,
205
- })
206
- : feedUrl,
207
- [feedUrl, mapping]
201
+ getInflatedDataSourceUrl({
202
+ source: feedUrl,
203
+ contexts: {
204
+ entry: entryContext,
205
+ screen: screenContext,
206
+ search: getSearchContext(searchContext, mapping),
207
+ },
208
+ mapping,
209
+ }),
210
+ [entryContext, feedUrl, mapping, screenContext, searchContext]
208
211
  );
209
212
 
210
- if (!feedUrl) {
211
- logger.warning({
212
- message: "Required parameter feedUrl is missing",
213
- data: { feedUrl },
214
- });
215
-
216
- return null;
217
- }
218
-
219
213
  return url;
220
214
  }
@@ -30,7 +30,13 @@ export const useLoadPipesDataDispatch = () => {
30
30
  return React.useCallback(
31
31
  (
32
32
  url: string,
33
- options = {},
33
+ options: {
34
+ callback?: (data: unknown, error?: Error | null | undefined) => void;
35
+ riverId?: string;
36
+ clearCache?: boolean;
37
+ silentRefresh?: boolean;
38
+ parentFeed?: string;
39
+ } = {},
34
40
  {
35
41
  withResolvers = false,
36
42
  withScreenRouteMapping = false,
@@ -44,7 +44,7 @@ export function useStatusBarHeight() {
44
44
 
45
45
  return platformSelect({
46
46
  ios: StatusBarHeight,
47
- android: StatusBar.currentHeight,
47
+ android: StatusBar.currentHeight ?? 0,
48
48
  default: 0,
49
49
  });
50
50
  }
@@ -5,3 +5,9 @@ export const getPickerSelectorId = (id) => `PickerSelector.${id}`;
5
5
  export const SCREEN_PICKER_CONTAINER = "ScreenPickerContainer";
6
6
 
7
7
  export const getScreenPickerId = (id) => `${SCREEN_PICKER_CONTAINER}.${id}`;
8
+
9
+ export const getScreenPickerSelectorContainerId = (id) =>
10
+ `${getScreenPickerId(id)}-screen-selector`;
11
+
12
+ export const getScreenPickerContentContainerId = (id) =>
13
+ `${getScreenPickerId(id)}-screen-container`;
@@ -0,0 +1,30 @@
1
+ import { endsWith } from "../endsWith";
2
+
3
+ describe("endsWith", () => {
4
+ it("returns false when str is null", () => {
5
+ expect(endsWith("a", null)).toBe(false);
6
+ });
7
+
8
+ it("returns false when str is undefined", () => {
9
+ expect(endsWith("a", undefined)).toBe(false);
10
+ });
11
+
12
+ it("returns true when string ends with target", () => {
13
+ expect(endsWith("lo", "hello")).toBe(true);
14
+ expect(endsWith("", "hello")).toBe(true); // empty target always matches
15
+ });
16
+
17
+ it("returns false when string does not end with target", () => {
18
+ expect(endsWith("yo", "hello")).toBe(false);
19
+ });
20
+
21
+ it("works with single character target", () => {
22
+ expect(endsWith("o", "hello")).toBe(true);
23
+ expect(endsWith("x", "hello")).toBe(false);
24
+ });
25
+
26
+ it("is case-sensitive", () => {
27
+ expect(endsWith("Lo", "hello")).toBe(false);
28
+ expect(endsWith("lo", "hello")).toBe(true);
29
+ });
30
+ });
@@ -0,0 +1,19 @@
1
+ import { omit } from "../omit";
2
+
3
+ test("example 1", () => {
4
+ const path = ["a", "b", "c"];
5
+ const record = { a: 1, b: 2, c: 3 };
6
+
7
+ const output = {};
8
+
9
+ expect(omit(path, record)).toEqual(output);
10
+ });
11
+
12
+ test("example 2", () => {
13
+ const path = ["a", "b"];
14
+ const record = { a: 1, b: 2, c: 3 };
15
+
16
+ const output = { c: 3 };
17
+
18
+ expect(omit(path, record)).toEqual(output);
19
+ });
@@ -0,0 +1,33 @@
1
+ import { path } from "../path";
2
+
3
+ test("example 1", () => {
4
+ const route = ["a", "b", "c"];
5
+ const xs = { a: { b: { c: 1 } } };
6
+
7
+ const output = 1;
8
+
9
+ expect(path(route, xs)).toEqual(output);
10
+ });
11
+
12
+ test("example 2", () => {
13
+ const route = ["a", "b"];
14
+ const xs = { a: { b: { c: 1 } } };
15
+
16
+ const output = { c: 1 };
17
+
18
+ expect(path(route, xs)).toEqual(output);
19
+ });
20
+
21
+ test("example 3", () => {
22
+ const route = ["a", "b", "x"];
23
+ const xs = { a: { b: { c: 1 } } };
24
+
25
+ expect(path(route, xs)).toBeUndefined();
26
+ });
27
+
28
+ test("example 4", () => {
29
+ const route = ["a", "b", "c"];
30
+ const xs = undefined;
31
+
32
+ expect(path(route, xs)).toBeUndefined();
33
+ });
@@ -0,0 +1,40 @@
1
+ import { take } from "../take";
2
+
3
+ describe("take", () => {
4
+ it("takes n elements from the beginning", () => {
5
+ expect(take(2, [1, 2, 3])).toEqual([1, 2]);
6
+ });
7
+
8
+ it("returns the whole array if n is larger than length", () => {
9
+ expect(take(5, [1, 2, 3])).toEqual([1, 2, 3]);
10
+ });
11
+
12
+ it("returns empty array if n is 0", () => {
13
+ expect(take(0, [1, 2, 3])).toEqual([]);
14
+ });
15
+
16
+ it("returns empty array for empty input array", () => {
17
+ expect(take(2, [])).toEqual([]);
18
+ });
19
+
20
+ it("returns empty array if n is negative", () => {
21
+ expect(take(-1, [1, 2, 3])).toEqual([]);
22
+ });
23
+
24
+ it("works with strings in array", () => {
25
+ expect(take(2, ["a", "b", "c"])).toEqual(["a", "b"]);
26
+ });
27
+
28
+ it("works with objects in array", () => {
29
+ const arr = [{ id: 1 }, { id: 2 }];
30
+ expect(take(1, arr)).toEqual([{ id: 1 }]);
31
+ });
32
+
33
+ it("returns empty array if input is not an array", () => {
34
+ // @ts-expect-error testing non-array input
35
+ expect(take(2, null)).toEqual([]);
36
+
37
+ // @ts-expect-error testing non-array input
38
+ expect(take(2, undefined)).toEqual([]);
39
+ });
40
+ });
@@ -0,0 +1,9 @@
1
+ import { isNil } from "lodash";
2
+
3
+ export const endsWith = (target, str) => {
4
+ if (isNil(str)) {
5
+ return false;
6
+ }
7
+
8
+ return str.endsWith(target);
9
+ };
package/utils/index.ts CHANGED
@@ -8,6 +8,14 @@ export { find } from "./find";
8
8
 
9
9
  export { pathOr } from "./pathOr";
10
10
 
11
+ export { path } from "./path";
12
+
13
+ export { omit } from "./omit";
14
+
15
+ export { endsWith } from "./endsWith";
16
+
17
+ export { take } from "./take";
18
+
11
19
  export {
12
20
  cloneDeep as clone,
13
21
  flatten,
@@ -19,10 +27,13 @@ export {
19
27
  has,
20
28
  flatMap,
21
29
  difference,
22
- take,
23
30
  pick,
24
31
  map,
25
32
  trim,
26
33
  toString,
27
34
  last,
35
+ toLower,
36
+ isEqual as equals,
37
+ flowRight as compose,
38
+ partial,
28
39
  } from "lodash";
package/utils/omit.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { omit as Lodash_omit } from "lodash";
2
+
3
+ export const omit = (path, record) => {
4
+ return Lodash_omit(record, path);
5
+ };