@ezez/utils 2.1.0 → 4.0.0

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 (236) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/index.d.ts +7 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +7 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/omit.d.ts +1 -1
  7. package/dist/omit.d.ts.map +1 -1
  8. package/dist/omit.js.map +1 -1
  9. package/dist/replace.d.ts.map +1 -1
  10. package/dist/replace.js.map +1 -1
  11. package/dist/replaceDeep.d.ts +5 -1
  12. package/dist/replaceDeep.d.ts.map +1 -1
  13. package/dist/replaceDeep.js +3 -20
  14. package/dist/replaceDeep.js.map +1 -1
  15. package/dist/replaceDeepByFn.d.ts +7 -0
  16. package/dist/replaceDeepByFn.d.ts.map +1 -0
  17. package/dist/replaceDeepByFn.js +47 -0
  18. package/dist/replaceDeepByFn.js.map +1 -0
  19. package/dist/safe.d.ts.map +1 -1
  20. package/dist/safe.js.map +1 -1
  21. package/dist/sample.d.ts +3 -0
  22. package/dist/sample.d.ts.map +1 -0
  23. package/dist/sample.js +8 -0
  24. package/dist/sample.js.map +1 -0
  25. package/dist/samples.d.ts +3 -0
  26. package/dist/samples.d.ts.map +1 -0
  27. package/dist/samples.js +28 -0
  28. package/dist/samples.js.map +1 -0
  29. package/dist/serialize.d.ts.map +1 -1
  30. package/dist/serialize.js +56 -18
  31. package/dist/serialize.js.map +1 -1
  32. package/dist/shuffle.d.ts +3 -0
  33. package/dist/shuffle.d.ts.map +1 -0
  34. package/dist/shuffle.js +9 -0
  35. package/dist/shuffle.js.map +1 -0
  36. package/dist/toggle.d.ts +3 -0
  37. package/dist/toggle.d.ts.map +1 -0
  38. package/dist/toggle.js +15 -0
  39. package/dist/toggle.js.map +1 -0
  40. package/dist/trim.d.ts +3 -0
  41. package/dist/trim.d.ts.map +1 -0
  42. package/dist/trim.js +10 -0
  43. package/dist/trim.js.map +1 -0
  44. package/dist/trimEnd.d.ts +3 -0
  45. package/dist/trimEnd.d.ts.map +1 -0
  46. package/dist/trimEnd.js +12 -0
  47. package/dist/trimEnd.js.map +1 -0
  48. package/dist/trimStart.d.ts +3 -0
  49. package/dist/trimStart.d.ts.map +1 -0
  50. package/dist/trimStart.js +12 -0
  51. package/dist/trimStart.js.map +1 -0
  52. package/dist/utils/utils.d.ts +6 -0
  53. package/dist/utils/utils.d.ts.map +1 -0
  54. package/dist/utils/utils.js +10 -0
  55. package/dist/utils/utils.js.map +1 -0
  56. package/dist/waitFor.d.ts +7 -1
  57. package/dist/waitFor.d.ts.map +1 -1
  58. package/dist/waitFor.js +35 -10
  59. package/dist/waitFor.js.map +1 -1
  60. package/docs/assets/search.js +1 -1
  61. package/docs/functions/cap.html +12 -5
  62. package/docs/functions/capitalize.html +12 -5
  63. package/docs/functions/coalesce.html +12 -5
  64. package/docs/functions/compareArrays.html +12 -5
  65. package/docs/functions/compareProps.html +12 -5
  66. package/docs/functions/deserialize.html +12 -5
  67. package/docs/functions/ensureArray.html +12 -5
  68. package/docs/functions/ensureDate.html +12 -5
  69. package/docs/functions/ensureError.html +12 -5
  70. package/docs/functions/ensurePrefix.html +12 -5
  71. package/docs/functions/ensureSuffix.html +12 -5
  72. package/docs/functions/ensureTimestamp.html +12 -5
  73. package/docs/functions/escapeRegExp.html +12 -5
  74. package/docs/functions/formatDate.html +12 -5
  75. package/docs/functions/get.html +12 -5
  76. package/docs/functions/getMultiple.html +12 -5
  77. package/docs/functions/insertSeparator.html +12 -5
  78. package/docs/functions/isEmpty.html +12 -5
  79. package/docs/functions/isNumericString.html +12 -5
  80. package/docs/functions/isPlainObject.html +12 -5
  81. package/docs/functions/last.html +12 -5
  82. package/docs/functions/later-1.html +12 -5
  83. package/docs/functions/mapAsync.html +12 -5
  84. package/docs/functions/mapValues.html +12 -5
  85. package/docs/functions/match.html +12 -5
  86. package/docs/functions/merge.html +20 -13
  87. package/docs/functions/mostFrequent.html +12 -5
  88. package/docs/functions/noop.html +12 -5
  89. package/docs/functions/occurrences.html +12 -5
  90. package/docs/functions/omit.html +18 -7
  91. package/docs/functions/pick.html +13 -6
  92. package/docs/functions/pull.html +12 -5
  93. package/docs/functions/remove.html +12 -5
  94. package/docs/functions/removeCommonProperties.html +12 -5
  95. package/docs/functions/replace.html +12 -5
  96. package/docs/functions/replaceDeep.html +23 -7
  97. package/docs/functions/rethrow.html +12 -5
  98. package/docs/functions/round.html +12 -5
  99. package/docs/functions/safe.html +13 -6
  100. package/docs/functions/sample.html +149 -0
  101. package/docs/functions/samples.html +159 -0
  102. package/docs/functions/scale.html +12 -5
  103. package/docs/functions/seq.html +12 -5
  104. package/docs/functions/seqEarlyBreak.html +12 -5
  105. package/docs/functions/serialize.html +12 -5
  106. package/docs/functions/set.html +12 -5
  107. package/docs/functions/setImmutable.html +12 -5
  108. package/docs/functions/shuffle.html +149 -0
  109. package/docs/functions/sortBy.html +12 -5
  110. package/docs/functions/sortProps.html +12 -5
  111. package/docs/functions/stripPrefix.html +12 -5
  112. package/docs/functions/stripSuffix.html +12 -5
  113. package/docs/functions/throttle.html +12 -5
  114. package/docs/functions/toggle.html +154 -0
  115. package/docs/functions/trim.html +152 -0
  116. package/docs/functions/trimEnd.html +152 -0
  117. package/docs/functions/trimStart.html +152 -0
  118. package/docs/functions/truthy.html +12 -5
  119. package/docs/functions/unique.html +12 -5
  120. package/docs/functions/wait.html +12 -5
  121. package/docs/functions/waitFor.html +23 -18
  122. package/docs/functions/waitSync.html +12 -5
  123. package/docs/index.html +11 -4
  124. package/docs/interfaces/ComparePropsOptions.html +6 -6
  125. package/docs/interfaces/GetMultipleSource.html +12 -5
  126. package/docs/interfaces/GetSource.html +12 -5
  127. package/docs/interfaces/IsNumericStringOptions.html +9 -9
  128. package/docs/interfaces/OccurencesOptions.html +6 -6
  129. package/docs/interfaces/SetImmutableSource.html +12 -5
  130. package/docs/interfaces/SetSource.html +12 -5
  131. package/docs/interfaces/ThrottleOptions.html +7 -7
  132. package/docs/interfaces/ThrottledFunctionExtras.html +7 -7
  133. package/docs/modules.html +18 -4
  134. package/docs/pages/CHANGELOG.html +150 -61
  135. package/docs/pages/Introduction.html +11 -4
  136. package/docs/types/CustomDeserializers.html +12 -5
  137. package/docs/types/CustomSerializers.html +12 -5
  138. package/docs/types/Later.html +12 -5
  139. package/docs/types/MapValuesFn.html +12 -5
  140. package/docs/types/MatchCallback.html +12 -5
  141. package/docs/types/SeqEarlyBreaker.html +12 -5
  142. package/docs/types/SeqFn.html +12 -5
  143. package/docs/types/SeqFunctions.html +12 -5
  144. package/docs/types/SetImmutablePath.html +12 -5
  145. package/docs/types/ThrottledFunction.html +12 -5
  146. package/docs/variables/mapValuesUNSET.html +12 -5
  147. package/docs/variables/mergeUNSET.html +12 -5
  148. package/esm/index.d.ts +7 -0
  149. package/esm/index.d.ts.map +1 -1
  150. package/esm/index.js +7 -0
  151. package/esm/index.js.map +1 -1
  152. package/esm/omit.d.ts +1 -1
  153. package/esm/omit.d.ts.map +1 -1
  154. package/esm/omit.js.map +1 -1
  155. package/esm/replace.d.ts.map +1 -1
  156. package/esm/replace.js.map +1 -1
  157. package/esm/replaceDeep.d.ts +5 -1
  158. package/esm/replaceDeep.d.ts.map +1 -1
  159. package/esm/replaceDeep.js +3 -20
  160. package/esm/replaceDeep.js.map +1 -1
  161. package/esm/replaceDeepByFn.d.ts +7 -0
  162. package/esm/replaceDeepByFn.d.ts.map +1 -0
  163. package/esm/replaceDeepByFn.js +44 -0
  164. package/esm/replaceDeepByFn.js.map +1 -0
  165. package/esm/safe.d.ts.map +1 -1
  166. package/esm/safe.js.map +1 -1
  167. package/esm/sample.d.ts +3 -0
  168. package/esm/sample.d.ts.map +1 -0
  169. package/esm/sample.js +5 -0
  170. package/esm/sample.js.map +1 -0
  171. package/esm/samples.d.ts +3 -0
  172. package/esm/samples.d.ts.map +1 -0
  173. package/esm/samples.js +25 -0
  174. package/esm/samples.js.map +1 -0
  175. package/esm/serialize.d.ts.map +1 -1
  176. package/esm/serialize.js +56 -18
  177. package/esm/serialize.js.map +1 -1
  178. package/esm/shuffle.d.ts +3 -0
  179. package/esm/shuffle.d.ts.map +1 -0
  180. package/esm/shuffle.js +6 -0
  181. package/esm/shuffle.js.map +1 -0
  182. package/esm/toggle.d.ts +3 -0
  183. package/esm/toggle.d.ts.map +1 -0
  184. package/esm/toggle.js +12 -0
  185. package/esm/toggle.js.map +1 -0
  186. package/esm/trim.d.ts +3 -0
  187. package/esm/trim.d.ts.map +1 -0
  188. package/esm/trim.js +7 -0
  189. package/esm/trim.js.map +1 -0
  190. package/esm/trimEnd.d.ts +3 -0
  191. package/esm/trimEnd.d.ts.map +1 -0
  192. package/esm/trimEnd.js +9 -0
  193. package/esm/trimEnd.js.map +1 -0
  194. package/esm/trimStart.d.ts +3 -0
  195. package/esm/trimStart.d.ts.map +1 -0
  196. package/esm/trimStart.js +9 -0
  197. package/esm/trimStart.js.map +1 -0
  198. package/esm/utils/utils.d.ts +6 -0
  199. package/esm/utils/utils.d.ts.map +1 -0
  200. package/esm/utils/utils.js +7 -0
  201. package/esm/utils/utils.js.map +1 -0
  202. package/esm/waitFor.d.ts +7 -1
  203. package/esm/waitFor.d.ts.map +1 -1
  204. package/esm/waitFor.js +35 -10
  205. package/esm/waitFor.js.map +1 -1
  206. package/package.json +4 -4
  207. package/pnpm-lock.yaml +1162 -986
  208. package/src/deserialize.spec.ts +12 -0
  209. package/src/index.ts +7 -0
  210. package/src/omit.ts +8 -2
  211. package/src/pick.ts +1 -1
  212. package/src/replace.ts +0 -1
  213. package/src/replaceDeep.spec.ts +91 -0
  214. package/src/replaceDeep.ts +22 -27
  215. package/src/replaceDeepByFn.spec.ts +162 -0
  216. package/src/replaceDeepByFn.ts +93 -0
  217. package/src/safe.ts +0 -1
  218. package/src/sample.spec.ts +31 -0
  219. package/src/sample.ts +11 -0
  220. package/src/samples.spec.ts +50 -0
  221. package/src/samples.ts +41 -0
  222. package/src/serialize.spec.ts +42 -0
  223. package/src/serialize.ts +65 -18
  224. package/src/shuffle.spec.ts +39 -0
  225. package/src/shuffle.ts +13 -0
  226. package/src/toggle.spec.ts +43 -0
  227. package/src/toggle.ts +22 -0
  228. package/src/trim.spec.ts +22 -0
  229. package/src/trim.ts +23 -0
  230. package/src/trimEnd.spec.ts +20 -0
  231. package/src/trimEnd.ts +22 -0
  232. package/src/trimStart.spec.ts +20 -0
  233. package/src/trimStart.ts +22 -0
  234. package/src/utils/utils.ts +11 -0
  235. package/src/waitFor.spec.ts +141 -0
  236. package/src/waitFor.ts +69 -18
@@ -80,4 +80,46 @@ describe("serialize", () => {
80
80
 
81
81
  must(serialize(a)).equal(serialize(b));
82
82
  });
83
+
84
+ it("allows serializing dates and other objects containing .toJSON", async () => {
85
+ const date = new Date(1714390008941);
86
+ must(serialize(date, {
87
+ D: (value) => {
88
+ if (value instanceof Date) {
89
+ return String(value.getTime());
90
+ }
91
+ return null;
92
+ },
93
+ })).equal(`"D:1714390008941"`);
94
+ });
95
+
96
+ it("should avoid excessive calls to custom serializers", async () => {
97
+ const customSerializers: CustomSerializers = {
98
+ p: (value) => {
99
+ // console.log("Called with", typeof value, value);
100
+ if (value instanceof Person) {
101
+ return value.name;
102
+ }
103
+ return null;
104
+ },
105
+ };
106
+
107
+ const p1 = new Person("John");
108
+ const spy = jest.spyOn(customSerializers, "p");
109
+
110
+ serialize({
111
+ a: [p1],
112
+ b: 1,
113
+ c: {
114
+ d: true,
115
+ e: p1,
116
+ },
117
+ }, customSerializers);
118
+ expect(spy).toHaveBeenCalledTimes(4);
119
+ });
120
+
121
+ it("should not crash on unknown instances but serialize as plain object", async () => {
122
+ const p1 = new Person("John");
123
+ must(serialize(p1)).equal(`{"name":"s:John"}`);
124
+ });
83
125
  });
package/src/serialize.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import { sortProps } from "./sortProps.js";
2
+ import { replaceDeepByFn } from "./replaceDeepByFn.js";
3
+ import { isPlainObject } from "./isPlainObject.js";
4
+ import { DataWrapper } from "./utils/utils.js";
2
5
 
3
6
  type CustomSerializers = {
4
7
  [key: string]: (data: unknown) => (string | null);
@@ -33,6 +36,32 @@ type Options = {
33
36
  * @param options - options
34
37
  */
35
38
  const serialize = (data: unknown, customSerializers?: CustomSerializers, options?: Options) => { // eslint-disable-line max-lines-per-function,max-len
39
+ const sourceData = Object.keys(customSerializers ?? {}).length
40
+ ? replaceDeepByFn(
41
+ data,
42
+ value => {
43
+ if (["string", "number", "bigint", "undefined", "boolean"].includes(typeof value)) {
44
+ return false;
45
+ }
46
+ if (value === null || Array.isArray(value)) {
47
+ return false;
48
+ }
49
+ if (isPlainObject(value)) {
50
+ return false;
51
+ }
52
+
53
+ const serializerKeys = Object.keys(customSerializers ?? {});
54
+ for (const key of serializerKeys) {
55
+ const serialized = customSerializers![key]!(value);
56
+ if (typeof serialized === "string") {
57
+ return true;
58
+ }
59
+ }
60
+ return false;
61
+ },
62
+ value => new DataWrapper(value),
63
+ )
64
+ : data;
36
65
  const replacer = (_key: string, value: unknown) => { // eslint-disable-line max-statements
37
66
  if (typeof value === "string") {
38
67
  return `s:${value}`;
@@ -52,12 +81,22 @@ const serialize = (data: unknown, customSerializers?: CustomSerializers, options
52
81
  if (value === null) {
53
82
  return "l:";
54
83
  }
84
+ if (Array.isArray(value)) {
85
+ return value as unknown[];
86
+ }
87
+ if (isPlainObject(value)) {
88
+ return value;
89
+ }
55
90
  const serializerKeys = Object.keys(customSerializers ?? {});
56
91
  for (const key of serializerKeys) {
57
- const serialized = customSerializers![key]!(value);
92
+ const serialized = customSerializers![key]!(value instanceof DataWrapper ? value.data : value);
58
93
  if (typeof serialized === "string") {
59
94
  return `${key}:${serialized}`;
60
95
  }
96
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
97
+ if (serialized !== null) {
98
+ throw new Error(`Custom serializer for key ${key} returned a non-string value: ${String(serialized)}`);
99
+ }
61
100
  }
62
101
  if (typeof value === "object") {
63
102
  return value;
@@ -67,35 +106,43 @@ const serialize = (data: unknown, customSerializers?: CustomSerializers, options
67
106
  };
68
107
 
69
108
  if (
70
- data == null
71
- || typeof data === "string"
72
- || typeof data === "number"
73
- || typeof data === "bigint"
74
- || typeof data === "undefined"
75
- || typeof data === "boolean"
76
- || Array.isArray(data)
109
+ sourceData == null
110
+ || typeof sourceData === "string"
111
+ || typeof sourceData === "number"
112
+ || typeof sourceData === "bigint"
113
+ || typeof sourceData === "undefined"
114
+ || typeof sourceData === "boolean"
115
+ || Array.isArray(sourceData)
77
116
  ) {
78
- return JSON.stringify(data, replacer);
117
+ return JSON.stringify(sourceData, replacer);
79
118
  }
80
119
 
81
- const serializerKeysGlobal = Object.keys(customSerializers ?? {});
82
- for (const key of serializerKeysGlobal) {
83
- const serialized = customSerializers![key]!(data);
84
- if (typeof serialized === "string") {
85
- return `"${key}:${serialized}"`;
120
+ if (!isPlainObject(sourceData)) {
121
+ const serializerKeysGlobal = Object.keys(customSerializers ?? {});
122
+ for (const key of serializerKeysGlobal) {
123
+ const serialized = customSerializers![key]!(
124
+ sourceData instanceof DataWrapper ? sourceData.data : sourceData,
125
+ );
126
+ if (typeof serialized === "string") {
127
+ return `"${key}:${serialized}"`;
128
+ }
129
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
130
+ if (serialized !== null) {
131
+ throw new Error(`Custom serializer for key ${key} returned a non-string value: ${String(serialized)}`);
132
+ }
86
133
  }
87
134
  }
88
135
 
89
- if (typeof data === "object") {
136
+ if (typeof sourceData === "object") {
90
137
  return JSON.stringify(
91
138
  options?.sortProps === false
92
- ? data
93
- : sortProps(data as Record<string, unknown>),
139
+ ? sourceData
140
+ : sortProps(sourceData as Record<string, unknown>),
94
141
  replacer,
95
142
  );
96
143
  }
97
144
 
98
- throw new Error(`Unsupported data type: ${typeof data}`);
145
+ throw new Error(`Unsupported data type: ${typeof sourceData}`);
99
146
  };
100
147
 
101
148
  export type { CustomSerializers };
@@ -0,0 +1,39 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-shadow
2
+ import must from "must";
3
+
4
+ import { shuffle } from "./shuffle";
5
+
6
+ describe("shuffle", () => {
7
+ it("should shuffle the array", async () => {
8
+ for (let i = 0; i < 100; i++) {
9
+ {
10
+ const elements = [1, 2, 3];
11
+
12
+ const result = shuffle(elements);
13
+ must(result).have.length(3);
14
+
15
+ for (const item of result) {
16
+ must(elements).include(item);
17
+ }
18
+ must([...new Set(result)]).have.length(3);
19
+ }
20
+ }
21
+ });
22
+
23
+ it("should shuffle the array with duplicates", async () => {
24
+ for (let i = 0; i < 100; i++) {
25
+ {
26
+ const elements = [1, 2, 2, 3];
27
+
28
+ const result = shuffle(elements);
29
+ must(result).have.length(4);
30
+
31
+ for (const item of result) {
32
+ must(elements).include(item);
33
+ }
34
+ must([...new Set(result)]).have.length(3);
35
+ must(result.filter(r => r === 2).length).equal(2);
36
+ }
37
+ }
38
+ });
39
+ });
package/src/shuffle.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { samples } from "./samples.js";
2
+
3
+ /**
4
+ * Shuffles an array, returning a new array.
5
+ * @param array source array
6
+ */
7
+ const shuffle = <T>(array: T[]): T[] => {
8
+ return samples(array, array.length, true);
9
+ };
10
+
11
+ export {
12
+ shuffle,
13
+ };
@@ -0,0 +1,43 @@
1
+ import { toggle } from "./toggle.js";
2
+
3
+ describe("toggle", () => {
4
+ it("adds the value if it doesn't exist", async () => {
5
+ must(toggle([1], 2)).eql([1, 2]);
6
+ });
7
+
8
+ it("removes the value if it exists", async () => {
9
+ must(toggle([1, 2], 2)).eql([1]);
10
+ });
11
+
12
+ it("removes only one occurrence", async () => {
13
+ must(toggle([1, 2, 2], 2)).eql([1, 2]);
14
+ });
15
+
16
+ it("modifies the original array when adding", async () => {
17
+ const array = [1];
18
+ toggle(array, 2);
19
+ must(array).eql([1, 2]);
20
+ });
21
+
22
+ it("modifies the original array when removing", async () => {
23
+ const array = [1, 2];
24
+ toggle(array, 2);
25
+ must(array).eql([1]);
26
+ });
27
+
28
+ it("returns the original array", async () => {
29
+ const array = [1];
30
+ // adding
31
+ must(toggle(array, 2)).equal(array);
32
+ // removing
33
+ must(toggle(array, 2)).equal(array);
34
+ });
35
+
36
+ it("adds NaN values correctly", async () => {
37
+ must(toggle([1], NaN)).eql([1, NaN]);
38
+ });
39
+
40
+ it("removes NaN values correctly", async () => {
41
+ must(toggle([1, NaN], NaN)).eql([1]);
42
+ });
43
+ });
package/src/toggle.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Toggles an item in an array. If it exists, remove it. If it doesn't exist, add it.
3
+ * If the array contains the given value more than once, this function will remove only one occurrence.
4
+ *
5
+ * @param array - source array
6
+ * @param item - item to toggle
7
+ */
8
+ const toggle = <T>(array: T[], item: T): T[] => {
9
+ const index = array.findIndex((i) => Object.is(i, item));
10
+ // eslint-disable-next-line @typescript-eslint/no-magic-numbers
11
+ if (index === -1) {
12
+ array.push(item);
13
+ }
14
+ else {
15
+ array.splice(index, 1);
16
+ }
17
+ return array;
18
+ };
19
+
20
+ export {
21
+ toggle,
22
+ };
@@ -0,0 +1,22 @@
1
+ import { trim } from "./trim";
2
+
3
+ describe("trim", () => {
4
+ it("should trim single character from both sides of the string", async () => {
5
+ must(trim("aaacataaa", "a")).equal("cat");
6
+ must(trim("abacteria", "a")).equal("bacteri");
7
+ must(trim("abc", "a")).equal("bc");
8
+ must(trim("abc", "c")).equal("ab");
9
+ must(trim("aajjja", "a")).equal("jjj");
10
+ });
11
+
12
+ it("should trim multiple characters as a whole", async () => {
13
+ must(trim("abopopab", "ab")).equal("opop");
14
+ must(trim("atitleb", "ab")).equal("atitleb");
15
+ must(trim("abtitleab", "ab")).equal("title");
16
+ });
17
+
18
+ it("can trim into empty string", async () => {
19
+ must(trim("abc", "abc")).equal("");
20
+ must(trim("a", "a")).equal("");
21
+ });
22
+ });
package/src/trim.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { trimStart } from "./trimStart.js";
2
+ import { trimEnd } from "./trimEnd.js";
3
+
4
+ /**
5
+ * Removes given characters from both sides of the string.
6
+ * If you want to remove from one side of the string see {@link trimStart} and {@link trimEnd}.
7
+ *
8
+ * @param source - Source string.
9
+ * @param characters - Characters to remove, taken as a whole.
10
+ *
11
+ * @example
12
+ * trim("abcb", "ab"); // "cb"
13
+ * trim("!aaa!", "!"); // "aaa"
14
+ * trim("!aaa!!!", "!"); // "aaa"
15
+ * trim("!aaa!!!", "!!!"); // "!aaa"
16
+ */
17
+ const trim = (source: string, characters: string) => {
18
+ return trimStart(trimEnd(source, characters), characters);
19
+ };
20
+
21
+ export {
22
+ trim,
23
+ };
@@ -0,0 +1,20 @@
1
+ import { trimEnd } from "./trimEnd";
2
+
3
+ describe("trimEnd", () => {
4
+ it("should trim single character from the end of the string", async () => {
5
+ must(trimEnd("abc?", "?")).equal("abc");
6
+ must(trimEnd("a? b?c???", "?")).equal("a? b?c");
7
+ });
8
+
9
+ it("should trim multiple characters as a whole", async () => {
10
+ must(trimEnd("abc", "bc")).equal("a");
11
+
12
+ must(trimEnd("abcc", "bc")).equal("abcc");
13
+ must(trimEnd("abcb", "bc")).equal("abcb");
14
+ });
15
+
16
+ it("can trim into empty string", async () => {
17
+ must(trimEnd("abc", "abc")).equal("");
18
+ must(trimEnd("a", "a")).equal("");
19
+ });
20
+ });
package/src/trimEnd.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Removes given characters from the end of the string.
3
+ * See also: {@link trim} and {@link trimStart}.
4
+ *
5
+ * @param source - Source string.
6
+ * @param characters - Characters to remove, taken as a whole.
7
+ *
8
+ * @example
9
+ * trimEnd("abcxzyz", "yz"); // "abcxz"
10
+ * trimEnd("!aaa!!", "!"); // "!aaa"
11
+ */
12
+ const trimEnd = (source: string, characters: string) => {
13
+ let s = source;
14
+ while (s.endsWith(characters)) {
15
+ s = s.slice(0, -characters.length);
16
+ }
17
+ return s;
18
+ };
19
+
20
+ export {
21
+ trimEnd,
22
+ };
@@ -0,0 +1,20 @@
1
+ import { trimStart } from "./trimStart";
2
+
3
+ describe("trimStart", () => {
4
+ it("should trim single character from the start of the string", async () => {
5
+ must(trimStart("?abc", "?")).equal("abc");
6
+ must(trimStart("aaabc", "a")).equal("bc");
7
+ });
8
+
9
+ it("should trim multiple characters as a whole", async () => {
10
+ must(trimStart("abc", "ab")).equal("c");
11
+
12
+ must(trimStart("aabcc", "ab")).equal("aabcc");
13
+ must(trimStart("babcb", "ab")).equal("babcb");
14
+ });
15
+
16
+ it("should trim into empty string if needed", async () => {
17
+ must(trimStart("abc", "abc")).equal("");
18
+ must(trimStart("aaa", "a")).equal("");
19
+ });
20
+ });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Removes given characters from the start of the string.
3
+ * See also: {@link trim} and {@link trimEnd}.
4
+ *
5
+ * @param source - Source string.
6
+ * @param characters - Characters to remove, taken as a whole.
7
+ *
8
+ * @example
9
+ * trimStart("abbcb", "ab"); // "bcb"
10
+ * trimStart("!!aaa!", "!"); // "aaa!"
11
+ */
12
+ const trimStart = (source: string, characters: string) => {
13
+ let s = source;
14
+ while (s.startsWith(characters)) {
15
+ s = s.slice(characters.length);
16
+ }
17
+ return s;
18
+ };
19
+
20
+ export {
21
+ trimStart,
22
+ };
@@ -0,0 +1,11 @@
1
+ class DataWrapper<T> {
2
+ public data: T;
3
+
4
+ public constructor(data: T) {
5
+ this.data = data;
6
+ }
7
+ }
8
+
9
+ export {
10
+ DataWrapper,
11
+ };
@@ -0,0 +1,141 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-shadow
2
+ import must from "must";
3
+
4
+ import createSpy from "../test/createSpy";
5
+
6
+ import { waitFor } from "./waitFor";
7
+
8
+ describe("waitFor", () => {
9
+ it("calls function multiple times until it returns truthy value", async () => {
10
+ let calls = 0;
11
+ const spy = createSpy(() => {
12
+ calls++;
13
+ return calls === 3;
14
+ });
15
+
16
+ const start = Date.now();
17
+ await waitFor(spy);
18
+
19
+ must(calls).equal(3);
20
+ // waited for 3 tries, should be 100ms (0 for first try) in total
21
+ must(Date.now() - start).be.gte(100);
22
+ });
23
+
24
+ it("returns value if check passes", async () => {
25
+ const x = await waitFor(() => 5);
26
+ must(x).equal(5);
27
+ });
28
+
29
+ it("returns value if check passes in non-first try", async () => {
30
+ let calls = 0;
31
+ const spy = createSpy(() => {
32
+ calls++;
33
+ return calls === 3;
34
+ });
35
+
36
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
37
+ const res = await waitFor(spy);
38
+ must(res).equal(true);
39
+ });
40
+
41
+ it("allows to adjust interval", async () => {
42
+ let calls = 0;
43
+ const spy = createSpy(() => {
44
+ calls++;
45
+ return calls === 3;
46
+ });
47
+
48
+ const start = Date.now();
49
+ await waitFor(spy, { interval: 150 });
50
+
51
+ must(calls).equal(3);
52
+ must(Date.now() - start).be.gte(300);
53
+ });
54
+
55
+ it("time outs after given time", async () => {
56
+ // eslint-disable-next-line @typescript-eslint/await-thenable
57
+ await must(waitFor(() => null, {
58
+ interval: 40,
59
+ timeout: 300,
60
+ })).reject.to.instanceOf(Error);
61
+ });
62
+
63
+ it("time outs after given amout of retries", async () => {
64
+ const spy = createSpy(() => false);
65
+
66
+ // eslint-disable-next-line @typescript-eslint/await-thenable
67
+ await must(waitFor(spy, {
68
+ interval: 40,
69
+ maxTries: 3,
70
+ })).reject.to.instanceOf(Error);
71
+
72
+ must(spy.__spy.calls).have.length(3);
73
+ });
74
+
75
+ it("validates max tries", async () => {
76
+ // eslint-disable-next-line @typescript-eslint/await-thenable
77
+ await must(waitFor(() => null, {
78
+ maxTries: 0,
79
+ })).reject.to.instanceOf(TypeError);
80
+
81
+ // eslint-disable-next-line @typescript-eslint/await-thenable
82
+ await must(waitFor(() => null, {
83
+ maxTries: -666,
84
+ })).reject.to.instanceOf(TypeError);
85
+ });
86
+
87
+ it("crashes if check function crashes", async () => {
88
+ await waitFor(() => {
89
+ throw new Error(5);
90
+ }, 40).then(() => {
91
+ throw new Error("Should not resolve");
92
+ }, (e) => {
93
+ must(e).instanceOf(Error);
94
+ must(e.message).equal("[waitFor] check function threw an error");
95
+ });
96
+ });
97
+
98
+ describe("treats most falsy values as succeeded check", () => {
99
+ it("numeric zero", async () => {
100
+ const result = await waitFor(() => 0);
101
+ must(result).equal(0);
102
+ });
103
+
104
+ it("empty string", async () => {
105
+ const result = await waitFor(() => "");
106
+ must(result).equal("");
107
+ });
108
+ });
109
+
110
+ describe("supports promises", () => {
111
+ it("truthy value", async () => {
112
+ const result = await waitFor(() => true);
113
+ must(result).be.true();
114
+ });
115
+
116
+ it("by retrying if Promise returns false", async () => {
117
+ let calls = 0;
118
+ const spy = createSpy(() => {
119
+ calls++;
120
+ return calls === 3;
121
+ });
122
+
123
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
124
+ const result = await waitFor(spy);
125
+ must(result).be.true();
126
+ must(calls).equal(3);
127
+ });
128
+
129
+ it("by crashing if check function returns rejected promise", async () => {
130
+ await waitFor(() => {
131
+ return Promise.reject(new Error("oops"));
132
+ }, 40).then(() => {
133
+ throw new Error("Should not resolve");
134
+ }, (e) => {
135
+ must(e).instanceOf(Error);
136
+ must(e.message).equal("[waitFor] check function threw an error");
137
+ });
138
+ });
139
+ });
140
+ });
141
+