@excom/kit-utils 0.1.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.
@@ -0,0 +1,376 @@
1
+ import * as utils from "../../common";
2
+ import {
3
+ describe,
4
+ expect,
5
+ it,
6
+ vi,
7
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
8
+
9
+ describe("common", () => {
10
+ it("toArray", () => {
11
+ expect(utils.toArray(1)).to.deep.equal([1]);
12
+ expect(utils.toArray([1, 2, 3])).to.deep.equal([1, 2, 3]);
13
+ });
14
+ it("isPojo", () => {
15
+ expect(utils.isPojo({})).to.be.true;
16
+ expect(utils.isPojo([])).to.be.false;
17
+ expect(utils.isPojo(null)).to.be.false;
18
+ expect(utils.isPojo(undefined)).to.be.false;
19
+ expect(utils.isPojo(1)).to.be.false;
20
+ });
21
+ it("pathJoin", () => {
22
+ expect(utils.pathJoin(["a", "b", "c"])).to.equal("a/b/c");
23
+ expect(utils.pathJoin(["a", "b", "c"], "-")).to.equal("a-b-c");
24
+ });
25
+ it("wrapInPromise", async () => {
26
+ await expect(utils.wrapInPromise(1)).resolves.to.equal(1);
27
+ const pending = Promise.resolve("ok");
28
+ expect(utils.wrapInPromise(pending)).to.equal(pending);
29
+ await expect(utils.wrapInPromise(pending)).resolves.to.equal("ok");
30
+ });
31
+ it("isNumber", () => {
32
+ expect(utils.isNumber(1)).to.be.true;
33
+ expect(utils.isNumber(1.1)).to.be.true;
34
+ expect(utils.isNumber("1")).to.be.false;
35
+ expect(utils.isNumber(null)).to.be.false;
36
+ expect(utils.isNumber(undefined)).to.be.false;
37
+ });
38
+ it("deepClone", () => {
39
+ expect(utils.deepClone({ a: 1 })).to.deep.equal({ a: 1 });
40
+ expect(utils.deepClone([1, 2, 3])).to.deep.equal([1, 2, 3]);
41
+ });
42
+ it("tc", () => {
43
+ expect(utils.tc(() => 1)).to.equal(1);
44
+ expect(
45
+ utils.tc(() => {
46
+ throw new Error();
47
+ })
48
+ ).to.be.undefined;
49
+ });
50
+ it("isPojo", () => {
51
+ expect(utils.isPojo({})).to.be.true;
52
+ expect(utils.isPojo([])).to.be.false;
53
+ expect(utils.isPojo(null)).to.be.false;
54
+ expect(utils.isPojo(undefined)).to.be.false;
55
+ expect(utils.isPojo(1)).to.be.false;
56
+ expect(utils.isPojo("")).to.be.false;
57
+ expect(utils.isPojo(true)).to.be.false;
58
+ expect(utils.isPojo(() => {})).to.be.false;
59
+ });
60
+ it("deepMerge", () => {
61
+ expect(utils.deepMerge({ a: 1 }, { b: 2 })).to.deep.equal({ a: 1, b: 2 });
62
+ expect(utils.deepMerge({ a: 1 }, { a: 2 })).to.deep.equal({ a: 2 });
63
+ expect(utils.deepMerge({ a: 1 }, { a: { b: 2 } })).to.deep.equal({
64
+ a: { b: 2 },
65
+ });
66
+ expect(
67
+ utils.deepMerge({ a: 1 }, { b: 2 }, { c: 3 }),
68
+ ).to.deep.equal({ a: 1, b: 2, c: 3 });
69
+ expect(
70
+ utils.deepMerge({ a: 1 }, { a: 2 }, { a: 3 }),
71
+ ).to.deep.equal({ a: 3 });
72
+ expect(
73
+ utils.deepMerge(
74
+ { a: { x: 1 } },
75
+ { a: { y: 2 } },
76
+ { a: { z: 3 } },
77
+ ),
78
+ ).to.deep.equal({ a: { x: 1, y: 2, z: 3 } });
79
+ expect(
80
+ utils.deepMerge(
81
+ { a: { x: 1 }, b: "keep" },
82
+ { a: { y: 2 }, c: true },
83
+ { a: { x: 10 } },
84
+ ),
85
+ ).to.deep.equal({ a: { x: 10, y: 2 }, b: "keep", c: true });
86
+ expect(
87
+ utils.deepMerge({ a: 1 }, null, { b: 2 }),
88
+ ).to.deep.equal({ a: 1, b: 2 });
89
+ expect(
90
+ utils.deepMerge(undefined, { a: 1 }, false, { b: 2 }, 0, "", { c: 3 }),
91
+ ).to.deep.equal({ a: 1, b: 2, c: 3 });
92
+ });
93
+ it("deleteUndefined", () => {
94
+ expect(utils.deleteUndefined({ a: 1, b: undefined, c: "ok" })).to.deep.equal({
95
+ a: 1,
96
+ c: "ok",
97
+ });
98
+ expect(utils.deleteUndefined({ x: undefined, y: undefined })).to.deep.equal({});
99
+ expect(utils.deleteUndefined({ a: 0, b: null, c: false, d: "" })).to.deep.equal({
100
+ a: 0,
101
+ b: null,
102
+ c: false,
103
+ d: "",
104
+ });
105
+ expect(
106
+ utils.deleteUndefined({ a: 1, b: { x: undefined, y: 2 } }),
107
+ ).to.deep.equal({ a: 1, b: { x: undefined, y: 2 } });
108
+ expect(
109
+ utils.deleteUndefined({ a: 1, b: { x: undefined, y: 2 } }, { nested: true }),
110
+ ).to.deep.equal({ a: 1, b: { y: 2 } });
111
+ expect(
112
+ utils.deleteUndefined(
113
+ { a: { b: { c: undefined, d: 3 }, e: undefined } },
114
+ { nested: true },
115
+ ),
116
+ ).to.deep.equal({ a: { b: { d: 3 } } });
117
+ });
118
+
119
+ it("toJsonSafe: defaults for primitives, functions, and POJOs", () => {
120
+ expect(utils.toJsonSafe(null)).to.equal(null);
121
+ expect(utils.toJsonSafe(1)).to.equal(1);
122
+ expect(utils.toJsonSafe("hi")).to.equal("hi");
123
+ expect(utils.toJsonSafe(true)).to.equal(true);
124
+ expect(utils.toJsonSafe(1n)).to.equal("1");
125
+ expect(utils.toJsonSafe(Symbol("s"))).to.equal("Symbol(s)");
126
+ expect(utils.toJsonSafe(() => {})).to.equal("<Function>");
127
+ expect(utils.toJsonSafe({ a: 1, b: { c: 2 } })).to.deep.equal({
128
+ a: 1,
129
+ b: { c: 2 },
130
+ });
131
+ expect(utils.toJsonSafe([1, { a: 2 }])).to.deep.equal([1, { a: 2 }]);
132
+ });
133
+
134
+ it("toJsonSafe: defaults for DOM, WeakRef, and non-POJOs", () => {
135
+ const el = document.createElement("div");
136
+ el.id = "x";
137
+ expect(utils.toJsonSafe(el)).to.equal("<HTMLDivElement>");
138
+ expect(utils.toJsonSafe(document.createTextNode("t"))).to.equal("<Text>");
139
+ expect(utils.toJsonSafe(new WeakRef(el))).to.equal("<WeakRef>");
140
+ expect(utils.toJsonSafe(Promise.resolve(1))).to.match(/^</);
141
+ expect(utils.toJsonSafe(new Map())).to.match(/^</);
142
+ });
143
+
144
+ it("toJsonSafe: circular refs and null-prototype objects", () => {
145
+ const circular: Record<string, unknown> = { a: 1 };
146
+ circular.self = circular;
147
+ expect(utils.toJsonSafe(circular)).to.deep.equal({
148
+ a: 1,
149
+ self: "<Circular>",
150
+ });
151
+ expect(utils.toJsonSafe(Object.assign(Object.create(null), { a: 1 }))).to.deep.equal({
152
+ a: 1,
153
+ });
154
+ });
155
+
156
+ it("deepCompare: primitives and identity", () => {
157
+ expect(utils.deepCompare(1, 1)).to.be.true;
158
+ expect(utils.deepCompare("a", "a")).to.be.true;
159
+ expect(utils.deepCompare(true, true)).to.be.true;
160
+ expect(utils.deepCompare(null, null)).to.be.true;
161
+ expect(utils.deepCompare(undefined, undefined)).to.be.true;
162
+ expect(utils.deepCompare(NaN, NaN)).to.be.true;
163
+ expect(utils.deepCompare(1, 2)).to.be.false;
164
+ expect(utils.deepCompare("a", "b")).to.be.false;
165
+ expect(utils.deepCompare(null, undefined)).to.be.false;
166
+ expect(utils.deepCompare(1, "1")).to.be.false;
167
+ expect(utils.deepCompare(0, false)).to.be.false;
168
+ const ref = {};
169
+ expect(utils.deepCompare(ref, ref)).to.be.true;
170
+ });
171
+
172
+ it("deepCompare: nested POJOs and arrays", () => {
173
+ expect(utils.deepCompare({ a: 1 }, { a: 1 })).to.be.true;
174
+ expect(utils.deepCompare({ a: 1 }, { a: 2 })).to.be.false;
175
+ expect(utils.deepCompare({ a: 1 }, { a: 1, b: 2 })).to.be.false;
176
+ expect(utils.deepCompare({ a: 1, b: 2 }, { b: 2, a: 1 })).to.be.true;
177
+ expect(
178
+ utils.deepCompare(
179
+ { a: { b: [1, { c: 2 }] } },
180
+ { a: { b: [1, { c: 2 }] } },
181
+ ),
182
+ ).to.be.true;
183
+ expect(
184
+ utils.deepCompare(
185
+ { a: { b: [1, { c: 2 }] } },
186
+ { a: { b: [1, { c: 3 }] } },
187
+ ),
188
+ ).to.be.false;
189
+ expect(utils.deepCompare([1, 2, 3], [1, 2, 3])).to.be.true;
190
+ expect(utils.deepCompare([1, 2, 3], [1, 2])).to.be.false;
191
+ expect(utils.deepCompare([1, [2, 3]], [1, [2, 3]])).to.be.true;
192
+ expect(utils.deepCompare([], [])).to.be.true;
193
+ expect(utils.deepCompare({}, {})).to.be.true;
194
+ expect(utils.deepCompare([], {})).to.be.false;
195
+ expect(utils.deepCompare({ 0: 1 }, [1])).to.be.false;
196
+ expect(
197
+ utils.deepCompare(Object.assign(Object.create(null), { a: 1 }), { a: 1 }),
198
+ ).to.be.true;
199
+ });
200
+
201
+ it("deepCompare: unknown constructors use ===", () => {
202
+ class MyUser {
203
+ constructor(public name: string) {}
204
+ }
205
+ const user = new MyUser("ada");
206
+ expect(utils.deepCompare(user, user)).to.be.true;
207
+ expect(utils.deepCompare(user, new MyUser("ada"))).to.be.false;
208
+ expect(utils.deepCompare({ user }, { user })).to.be.true;
209
+ expect(utils.deepCompare({ user }, { user: new MyUser("ada") })).to.be.false;
210
+ expect(utils.deepCompare([user], [user])).to.be.true;
211
+ expect(utils.deepCompare([user], [new MyUser("ada")])).to.be.false;
212
+ expect(utils.deepCompare(new Date(0), new Date(0))).to.be.false;
213
+ expect(utils.deepCompare(/a/, /a/)).to.be.false;
214
+ const el = document.createElement("div");
215
+ expect(utils.deepCompare(el, el)).to.be.true;
216
+ expect(utils.deepCompare(el, document.createElement("div"))).to.be.false;
217
+ });
218
+
219
+ it("toJsonSafe: custom serializers override defaults (devtools shape)", () => {
220
+ // Mirrors packages/neutron/src/devtools-hook.ts serializers.
221
+ const serializers = {
222
+ function: () => ({ $constructor: "Function" }),
223
+ WeakRef: (value: WeakRef<WeakKey>) => {
224
+ const deref = value.deref();
225
+ return deref === undefined ? null : deref;
226
+ },
227
+ Node: (value: Node) => ({
228
+ $node: value.nodeName,
229
+ }),
230
+ Element: (value: Element) => ({
231
+ $element: value.localName ?? value.nodeName,
232
+ id: value.id || null,
233
+ }),
234
+ UnknownObject: (value: unknown) => ({
235
+ $constructor:
236
+ (value as object).constructor?.name ||
237
+ Object.prototype.toString.call(value),
238
+ }),
239
+ };
240
+
241
+ const el = document.createElement("span");
242
+ el.id = "probe";
243
+ const text = document.createTextNode("hi");
244
+
245
+ expect(utils.toJsonSafe(() => {}, serializers)).to.deep.equal({
246
+ $constructor: "Function",
247
+ });
248
+ expect(utils.toJsonSafe(el, serializers)).to.deep.equal({
249
+ $element: "span",
250
+ id: "probe",
251
+ });
252
+ expect(utils.toJsonSafe(text, serializers)).to.deep.equal({
253
+ $node: "#text",
254
+ });
255
+ expect(utils.toJsonSafe(new WeakRef(el), serializers)).to.deep.equal({
256
+ $element: "span",
257
+ id: "probe",
258
+ });
259
+ expect(utils.toJsonSafe(new WeakRef({ a: 1 }), serializers)).to.deep.equal({
260
+ a: 1,
261
+ });
262
+ expect(utils.toJsonSafe(Promise.resolve(), serializers)).to.deep.equal({
263
+ $constructor: "Promise",
264
+ });
265
+ expect(
266
+ utils.toJsonSafe({ fn: () => {}, nested: { el } }, serializers),
267
+ ).to.deep.equal({
268
+ fn: { $constructor: "Function" },
269
+ nested: { el: { $element: "span", id: "probe" } },
270
+ });
271
+ });
272
+
273
+ it("wait: resolves true after the delay", async () => {
274
+ await expect(utils.wait(1)).resolves.to.equal(true);
275
+ await expect(utils.wait()).resolves.to.equal(true);
276
+ });
277
+
278
+ it("execWhenReady: runs the callback synchronously for plain values", () => {
279
+ const cb = (v: number) => v * 2;
280
+ expect(utils.execWhenReady(2, cb)).to.equal(4);
281
+ });
282
+
283
+ it("execWhenReady: waits for a promise before running the callback", async () => {
284
+ const cb = (v: number) => v * 2;
285
+ const result = utils.execWhenReady(Promise.resolve(3), cb);
286
+ expect(result).to.be.instanceOf(Promise);
287
+ await expect(result).resolves.to.equal(6);
288
+ });
289
+
290
+ it("unique: keeps the first occurrence of each value", () => {
291
+ expect(utils.unique([1, 2, 1, 3, 2])).to.deep.equal([1, 2, 3]);
292
+ const a = {};
293
+ const b = {};
294
+ expect(utils.unique([a, b, a])).to.deep.equal([a, b]);
295
+ expect(utils.unique([])).to.deep.equal([]);
296
+ });
297
+
298
+ it("deepClone: leaves primitives and null alone, clones nested arrays", () => {
299
+ expect(utils.deepClone(null)).to.equal(null);
300
+ expect(utils.deepClone("s")).to.equal("s");
301
+ const src = { a: [1, { b: 2 }], c: { d: [3] } };
302
+ const copy = utils.deepClone(src);
303
+ expect(copy).to.deep.equal(src);
304
+ expect(copy.a).to.not.equal(src.a);
305
+ expect(copy.a[1]).to.not.equal(src.a[1]);
306
+ expect(copy.c.d).to.not.equal(src.c.d);
307
+ });
308
+
309
+ it("di.apply: calls back with dereferenced values when all are alive", () => {
310
+ const a = { name: "a" };
311
+ const b = { name: "b" };
312
+ const result = utils.di.apply(
313
+ [new WeakRef(a), new WeakRef(b)],
314
+ (x, y) => `${x.name}${y.name}`,
315
+ );
316
+ expect(result).to.equal("ab");
317
+ });
318
+
319
+ it("di.apply: skips the callback when a ref is gone or missing", () => {
320
+ const a = { name: "a" };
321
+ const dead = { deref: () => undefined } as unknown as WeakRef<object>;
322
+ const cb = vi.fn(() => "ran");
323
+ expect(utils.di.apply([new WeakRef(a), dead], cb)).to.equal(undefined);
324
+ expect(utils.di.apply([null as unknown as WeakRef<object>], cb)).to.equal(
325
+ undefined,
326
+ );
327
+ expect(cb).not.toHaveBeenCalled();
328
+ });
329
+
330
+ it("di.apply: optional refs pass undefined through", () => {
331
+ const dead = { deref: () => undefined } as unknown as WeakRef<object>;
332
+ const cb = vi.fn((x: unknown, y: unknown) => [x, y]);
333
+ const live = { ok: true };
334
+ expect(
335
+ utils.di.apply([dead, new WeakRef(live)], cb, { optional: true }),
336
+ ).to.deep.equal([undefined, live]);
337
+ expect(cb).toHaveBeenCalledTimes(1);
338
+ });
339
+
340
+ it("di.bind: defers the call and re-checks liveness when invoked", () => {
341
+ let current: { n: number } | undefined = { n: 1 };
342
+ const ref = { deref: () => current } as unknown as WeakRef<{ n: number }>;
343
+ const cb = vi.fn((x: { n: number }) => x.n);
344
+ const bound = utils.di.bind([ref], cb)!;
345
+ expect(bound).to.be.a("function");
346
+ expect(cb).not.toHaveBeenCalled();
347
+ expect(bound()).to.equal(1);
348
+ current = undefined;
349
+ expect(bound()).to.equal(undefined);
350
+ expect(cb).toHaveBeenCalledTimes(1);
351
+ const optional = utils.di.bind([ref], cb, { optional: true })!;
352
+ expect(() => optional()).to.throw();
353
+ });
354
+
355
+ it("toJsonSafe: reports <Failed> when serialization throws", () => {
356
+ const bomb = {
357
+ toJSON() {
358
+ throw new Error("nope");
359
+ },
360
+ };
361
+ expect(utils.toJsonSafe(bomb)).to.equal("<Failed>");
362
+ expect(utils.toJsonSafe({ nested: bomb })).to.equal("<Failed>");
363
+ expect(
364
+ utils.toJsonSafe(bomb, { Failed: () => ({ failed: true }) }),
365
+ ).to.deep.equal({ failed: true });
366
+ });
367
+
368
+ it("toJsonSafe: node and element labels fall back to nodeType / nodeName", () => {
369
+ const text = document.createTextNode("t");
370
+ Object.defineProperty(text, "constructor", { value: {} });
371
+ expect(utils.toJsonSafe(text)).to.equal(`<${Node.TEXT_NODE}>`);
372
+ const el = document.createElement("div");
373
+ Object.defineProperty(el, "constructor", { value: {} });
374
+ expect(utils.toJsonSafe(el)).to.equal("<DIV>");
375
+ });
376
+ });