@rebasepro/client 0.17.3 → 0.18.1

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 (63) hide show
  1. package/README.md +4 -0
  2. package/dist/auth.d.ts +80 -0
  3. package/dist/functions.d.ts +6 -1
  4. package/dist/index.d.ts +8 -0
  5. package/dist/index.es.js +471 -94
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/offline-connectivity.d.ts +12 -1
  8. package/dist/offline.d.ts +23 -1
  9. package/dist/query-contract.types.d.ts +30 -0
  10. package/dist/realtime-channel.d.ts +29 -1
  11. package/dist/sdk_query_builder.d.ts +21 -2
  12. package/dist/transport.d.ts +24 -0
  13. package/package.json +28 -15
  14. package/src/admin.ts +0 -90
  15. package/src/anonymous-client-guard.test.ts +0 -190
  16. package/src/api-keys.ts +0 -87
  17. package/src/auth-listener-errors.test.ts +0 -57
  18. package/src/auth-refresh-overflow.test.ts +0 -89
  19. package/src/auth.ts +0 -982
  20. package/src/backups.ts +0 -40
  21. package/src/client-close.test.ts +0 -80
  22. package/src/collection-listen-meta.test.ts +0 -105
  23. package/src/collection-observe.test.ts +0 -138
  24. package/src/collection.test.ts +0 -293
  25. package/src/collection.ts +0 -525
  26. package/src/cron.test.ts +0 -164
  27. package/src/cron.ts +0 -62
  28. package/src/data-proxy.test.ts +0 -183
  29. package/src/errors.ts +0 -9
  30. package/src/functions.ts +0 -82
  31. package/src/index.ts +0 -639
  32. package/src/like-pattern-redos.test.ts +0 -61
  33. package/src/offline-codec.ts +0 -79
  34. package/src/offline-connectivity.test.ts +0 -191
  35. package/src/offline-connectivity.ts +0 -255
  36. package/src/offline-idb-store.test.ts +0 -340
  37. package/src/offline-integration.test.ts +0 -180
  38. package/src/offline-query.test.ts +0 -431
  39. package/src/offline-query.ts +0 -529
  40. package/src/offline-store.ts +0 -357
  41. package/src/offline-sync-engine.test.ts +0 -857
  42. package/src/offline.test.ts +0 -897
  43. package/src/offline.ts +0 -1928
  44. package/src/query-contract.types.ts +0 -206
  45. package/src/query_builder.ts +0 -1
  46. package/src/realtime-channel.test.ts +0 -542
  47. package/src/realtime-channel.ts +0 -539
  48. package/src/realtime-concurrent-subscribe.test.ts +0 -102
  49. package/src/realtime-error-surfacing.test.ts +0 -105
  50. package/src/realtime-optout.test.ts +0 -279
  51. package/src/realtime-row-identity.test.ts +0 -254
  52. package/src/realtime-subscription-key.test.ts +0 -92
  53. package/src/reviver.ts +0 -39
  54. package/src/sdk_query_builder.ts +0 -206
  55. package/src/storage-key-encoding.test.ts +0 -65
  56. package/src/storage-registry.ts +0 -102
  57. package/src/storage.ts +0 -253
  58. package/src/transport-baseurl.test.ts +0 -101
  59. package/src/transport.ts +0 -505
  60. package/src/vector-search-listen.test.ts +0 -42
  61. package/src/vector-search-query.test.ts +0 -55
  62. package/src/websocket-url.test.ts +0 -97
  63. package/src/websocket.ts +0 -1837
@@ -1,431 +0,0 @@
1
- import {
2
- compareValues,
3
- isExactlyEvaluable,
4
- isLocallySortable,
5
- looseEquals,
6
- matchesLogical,
7
- matchesOperator,
8
- matchesParams,
9
- matchesWhere,
10
- resolvePagination,
11
- runLocalQuery,
12
- sortRows
13
- } from "./offline-query";
14
- import { DEFAULT_LIST_LIMIT, EntityRelation } from "@rebasepro/types";
15
- import { resolveFindWindow } from "@rebasepro/common";
16
- import type { FindParams } from "./transport";
17
-
18
- type Row = Record<string, unknown>;
19
-
20
- /**
21
- * The local evaluator is what lets a row written offline appear in the
22
- * filtered lists it belongs to, so every divergence from the Postgres driver's
23
- * semantics shows up as a list that is subtly wrong only when the network is.
24
- * These pin the cases where SQL and JavaScript disagree.
25
- */
26
- describe("local query engine", () => {
27
- describe("operators", () => {
28
- it("compares numbers written as strings by the wire as numbers", () => {
29
- // The wire format carries no types, so `["<", "10"]` arrives as a
30
- // string. Comparing it as text would order "10" before "9".
31
- expect(matchesOperator(9, "<", "10")).toBe(true);
32
- expect(matchesOperator("9", "<", 10)).toBe(true);
33
- expect(matchesOperator(10, "<", "9")).toBe(false);
34
- });
35
-
36
- it("treats a comparison against NULL as unknown, not as false-or-true", () => {
37
- // `WHERE status != 'done'` does not return rows with a NULL status.
38
- expect(matchesOperator(null, "!=", "done")).toBe(false);
39
- expect(matchesOperator(undefined, "!=", "done")).toBe(false);
40
- expect(matchesOperator(null, "<", 5)).toBe(false);
41
- expect(matchesOperator(null, ">", 5)).toBe(false);
42
- expect(matchesOperator(null, "in", ["a"])).toBe(false);
43
- expect(matchesOperator(null, "not-in", ["a"])).toBe(false);
44
- });
45
-
46
- it("tests null with the operators meant for it", () => {
47
- expect(matchesOperator(null, "is-null", null)).toBe(true);
48
- expect(matchesOperator(undefined, "is-null", null)).toBe(true);
49
- expect(matchesOperator(0, "is-null", null)).toBe(false);
50
- expect(matchesOperator("", "is-not-null", null)).toBe(true);
51
- expect(matchesOperator(null, "is-not-null", null)).toBe(false);
52
- });
53
-
54
- it("matches dates against instants and ISO strings alike", () => {
55
- const row = new Date("2026-07-01T00:00:00Z");
56
- expect(matchesOperator(row, ">=", new Date("2026-06-01T00:00:00Z"))).toBe(true);
57
- expect(matchesOperator(row, "<", "2026-08-01T00:00:00Z")).toBe(true);
58
- expect(matchesOperator(row, "==", new Date("2026-07-01T00:00:00Z"))).toBe(true);
59
- });
60
-
61
- it("compares a relation by the id it points at", () => {
62
- const relation = new EntityRelation("u1", "users");
63
- expect(matchesOperator(relation, "==", "u1")).toBe(true);
64
- expect(matchesOperator(relation, "in", ["u1", "u2"])).toBe(true);
65
- expect(matchesOperator(relation, "==", "u2")).toBe(false);
66
- });
67
-
68
- it("handles list membership and array columns", () => {
69
- expect(matchesOperator("editor", "in", ["admin", "editor"])).toBe(true);
70
- expect(matchesOperator("viewer", "not-in", ["admin", "editor"])).toBe(true);
71
- expect(matchesOperator(["a", "b"], "array-contains", "b")).toBe(true);
72
- expect(matchesOperator(["a", "b"], "array-contains", "c")).toBe(false);
73
- expect(matchesOperator(["a", "b"], "array-contains-any", ["c", "b"])).toBe(true);
74
- expect(matchesOperator("not-an-array", "array-contains", "a")).toBe(false);
75
- });
76
-
77
- it("reads LIKE patterns as SQL does, wildcards and escapes included", () => {
78
- expect(matchesOperator("post-1", "like", "post-%")).toBe(true);
79
- expect(matchesOperator("Post-1", "like", "post-%")).toBe(false);
80
- expect(matchesOperator("Post-1", "ilike", "post-%")).toBe(true);
81
- expect(matchesOperator("abc", "like", "a_c")).toBe(true);
82
- expect(matchesOperator("abbc", "like", "a_c")).toBe(false);
83
- expect(matchesOperator("50%", "like", "50\\%")).toBe(true);
84
- expect(matchesOperator("500", "like", "50\\%")).toBe(false);
85
- expect(matchesOperator("a.c", "like", "a.c")).toBe(true);
86
- // A regex metacharacter in the pattern is a literal, not a wildcard.
87
- expect(matchesOperator("abc", "like", "a.c")).toBe(false);
88
- expect(matchesOperator("Post-1", "not-ilike", "post-%")).toBe(false);
89
- expect(matchesOperator("other", "not-like", "post-%")).toBe(true);
90
- });
91
-
92
- it("accepts REST short-codes wherever canonical operators are accepted", () => {
93
- // Filters that came off the wire carry `eq`/`gte`, not `==`/`>=`.
94
- expect(matchesWhere({ status: "active" }, { status: ["eq" as "==", "active"] })).toBe(true);
95
- expect(matchesWhere({ age: 20 }, { age: ["gte" as ">=", 18] })).toBe(true);
96
- });
97
-
98
- it("keeps an unknown operator from silently dropping rows", () => {
99
- // Better a list that is too broad than one that quietly hides rows
100
- // because this build predates an operator.
101
- expect(matchesOperator("x", "spaceship" as never, "y")).toBe(true);
102
- });
103
- });
104
-
105
- describe("where clauses", () => {
106
- const row: Row = { status: "active", age: 30, tags: ["a"] };
107
-
108
- it("ANDs across fields and across tuples on one field", () => {
109
- expect(matchesWhere(row, { status: ["==", "active"], age: [">=", 18] })).toBe(true);
110
- expect(matchesWhere(row, { status: ["==", "active"], age: [">=", 40] })).toBe(false);
111
- expect(matchesWhere(row, { age: [[">=", 18], ["<", 65]] })).toBe(true);
112
- expect(matchesWhere(row, { age: [[">=", 18], ["<", 25]] })).toBe(false);
113
- });
114
-
115
- it("is vacuously true with no clause", () => {
116
- expect(matchesWhere(row, undefined)).toBe(true);
117
- expect(matchesWhere(row, {})).toBe(true);
118
- });
119
- });
120
-
121
- describe("logical trees", () => {
122
- const row: Row = { status: "draft", author: "me" };
123
-
124
- it("evaluates nested and/or", () => {
125
- expect(matchesLogical(row, {
126
- type: "or",
127
- conditions: [
128
- { column: "status", operator: "==", value: "published" },
129
- { column: "author", operator: "==", value: "me" }
130
- ]
131
- })).toBe(true);
132
-
133
- expect(matchesLogical(row, {
134
- type: "and",
135
- conditions: [
136
- { column: "status", operator: "==", value: "draft" },
137
- {
138
- type: "or",
139
- conditions: [
140
- { column: "author", operator: "==", value: "you" },
141
- { column: "author", operator: "==", value: "me" }
142
- ]
143
- }
144
- ]
145
- })).toBe(true);
146
-
147
- expect(matchesLogical(row, {
148
- type: "or",
149
- conditions: [{ column: "status", operator: "==", value: "published" }]
150
- })).toBe(false);
151
- });
152
- });
153
-
154
- describe("ordering", () => {
155
- it("puts nulls last ascending and first descending, as Postgres does", () => {
156
- const rows: Row[] = [{ id: 1, n: 2 }, { id: 2, n: null }, { id: 3, n: 1 }];
157
- expect(sortRows([...rows], ["n", "asc"]).map((r) => r.id)).toEqual([3, 1, 2]);
158
- expect(sortRows([...rows], ["n", "desc"]).map((r) => r.id)).toEqual([2, 1, 3]);
159
- });
160
-
161
- it("breaks ties on id descending, which is what the server orders by", () => {
162
- // Not an arbitrary direction: `FetchService.buildOrderExpressions`
163
- // appends `id DESC` to every sort it builds, and the keyset cursor
164
- // is written to match it. Ascending here meant the overlay handed
165
- // back tied rows in the opposite order to the network answer for
166
- // the same query — while `isLocallySortable` reported the page as
167
- // exactly reproducible.
168
- const rows: Row[] = [{ id: "a", n: 1 }, { id: "c", n: 1 }, { id: "b", n: 1 }];
169
- expect(sortRows(rows, ["n", "asc"]).map((r) => r.id)).toEqual(["c", "b", "a"]);
170
- expect(sortRows(rows, ["n", "desc"]).map((r) => r.id)).toEqual(["c", "b", "a"]);
171
- });
172
-
173
- it("lets a later key decide before the id tiebreak does", () => {
174
- const rows: Row[] = [
175
- { id: 1, role: "admin", at: 2 },
176
- { id: 2, role: "admin", at: 1 },
177
- { id: 3, role: "user", at: 9 }
178
- ];
179
- expect(sortRows(rows, [["role", "asc"], ["at", "desc"]]).map((r) => r.id))
180
- .toEqual([1, 2, 3]);
181
- });
182
-
183
- it("orders dates chronologically, not lexicographically", () => {
184
- const rows: Row[] = [
185
- { id: 1, at: new Date("2026-01-02T00:00:00Z") },
186
- { id: 2, at: new Date("2025-12-31T00:00:00Z") }
187
- ];
188
- expect(sortRows(rows, ["at", "asc"]).map((r) => r.id)).toEqual([2, 1]);
189
- });
190
- });
191
-
192
- describe("pagination", () => {
193
- it("resolves page over offset the way the server does", () => {
194
- expect(resolvePagination({ limit: 10, offset: 30 })).toEqual({ limit: 10, offset: 30 });
195
- expect(resolvePagination({ limit: 10, page: 3 })).toEqual({ limit: 10, offset: 20 });
196
- // `page` wins over `offset`, and the first page is not negative.
197
- expect(resolvePagination({ limit: 10, page: 1, offset: 99 })).toEqual({ limit: 10, offset: 0 });
198
- });
199
-
200
- it("defaults to the same page size the server would apply", () => {
201
- // Asserted against the constant, not against a number written here.
202
- // This test used to say `{ limit: 20 }` — restating a default this
203
- // module had invented, while `/api/data` paged by 50. The local
204
- // answer and the network answer to one `observe()` were therefore
205
- // different lengths, and the test agreed with the wrong one.
206
- expect(resolvePagination()).toEqual({ limit: DEFAULT_LIST_LIMIT, offset: 0 });
207
- });
208
-
209
- it("agrees with the shared resolver on every shape of window", () => {
210
- // The one that matters: the local evaluator and every other
211
- // transport must resolve identical windows, or a cached list and a
212
- // fetched list disagree about which rows page two holds.
213
- for (const params of [
214
- undefined,
215
- { limit: 10 },
216
- { offset: 30 },
217
- { page: 4 },
218
- { limit: 25, page: 2 },
219
- { limit: 10, page: 1, offset: 99 }
220
- ]) {
221
- const { limit, offset } = resolveFindWindow(params);
222
- expect(resolvePagination(params)).toEqual({ limit, offset });
223
- }
224
- });
225
- });
226
-
227
- describe("runLocalQuery", () => {
228
- const rows: Row[] = [
229
- { id: "a", n: 3, status: "draft" },
230
- { id: "b", n: 1, status: "draft" },
231
- { id: "c", n: 2, status: "published" }
232
- ];
233
-
234
- it("filters, sorts and paginates, reporting the unpaginated total", () => {
235
- const result = runLocalQuery(rows, {
236
- where: { status: ["==", "draft"] },
237
- orderBy: ["n", "asc"],
238
- limit: 1
239
- });
240
- expect(result.data.map((r) => r.id)).toEqual(["b"]);
241
- expect(result.meta).toEqual({ total: 2, limit: 1, offset: 0, hasMore: true });
242
- });
243
-
244
- it("reports no more rows on the last page", () => {
245
- const result = runLocalQuery(rows, { orderBy: ["n", "asc"], limit: 2, offset: 2 });
246
- expect(result.data.map((r) => r.id)).toEqual(["a"]);
247
- expect(result.meta).toMatchObject({ total: 3, offset: 2, hasMore: false });
248
- });
249
-
250
- it("approximates search as a substring scan over string and number fields", () => {
251
- expect(runLocalQuery(rows, { searchString: "publi" }).data.map((r) => r.id)).toEqual(["c"]);
252
- expect(runLocalQuery(rows, { searchString: "PUBLI" }).data.map((r) => r.id)).toEqual(["c"]);
253
- expect(runLocalQuery(rows, { searchString: " " }).data).toHaveLength(3);
254
- });
255
-
256
- it("combines where, logical and search with AND", () => {
257
- const params: FindParams = {
258
- where: { status: ["==", "draft"] },
259
- logical: { type: "or", conditions: [{ column: "n", operator: "==", value: 3 }] }
260
- };
261
- expect(runLocalQuery(rows, params).data.map((r) => r.id)).toEqual(["a"]);
262
- expect(matchesParams({ id: "b", n: 1, status: "draft" }, params)).toBe(false);
263
- });
264
- });
265
-
266
- describe("isExactlyEvaluable", () => {
267
- it("admits what it cannot answer as well as the server would", () => {
268
- expect(isExactlyEvaluable(undefined)).toBe(true);
269
- expect(isExactlyEvaluable({ where: { a: ["==", 1] }, orderBy: ["a", "asc"] })).toBe(true);
270
- // Real full-text search is not a substring scan…
271
- expect(isExactlyEvaluable({ searchString: "x" })).toBe(false);
272
- // …and related rows live in collections this query never loaded.
273
- expect(isExactlyEvaluable({ include: ["author"] })).toBe(false);
274
- expect(isExactlyEvaluable({ include: [] })).toBe(true);
275
- });
276
-
277
- /**
278
- * An ordering comparison is answered here by `compareValues`, which
279
- * falls back to an `Intl.Collator`, and by Postgres using the
280
- * *database's* collation — a property of the server this process has
281
- * never been told. `'apple' < 'Banana'` is false under the C collation
282
- * and true under `en_US.UTF-8`; the collator says true. So the two
283
- * select different sets, in whichever direction the deployment happened
284
- * to be created.
285
- */
286
- it("refuses an ordering comparison, whose answer depends on the server's collation", () => {
287
- for (const op of ["<", "<=", ">", ">="] as const) {
288
- expect({ op, exact: isExactlyEvaluable({ where: { name: [op, "Banana"] } }) })
289
- .toEqual({ op, exact: false });
290
- }
291
- // Equality, membership and pattern matching are unaffected — all
292
- // verified against a real database in
293
- // `server-postgres/test/e2e/offline-query-agreement.test.ts`.
294
- for (const op of ["==", "!=", "in", "not-in", "like", "ilike"] as const) {
295
- expect({ op, exact: isExactlyEvaluable({ where: { name: [op, "x"] } }) })
296
- .toEqual({ op, exact: true });
297
- }
298
- expect(isExactlyEvaluable({ where: { a: ["is-null", null] } })).toBe(true);
299
- });
300
-
301
- it("refuses a numeric range too, because the operand type does not settle it", () => {
302
- // `compareValues` deliberately reads numeric strings as numbers, so
303
- // it cannot tell an integer column from a text column of digits —
304
- // and on the latter Postgres orders "10" before "9" while this
305
- // orders 9 before 10. Conservative on purpose; the schema would be
306
- // needed to do better.
307
- expect(isExactlyEvaluable({ where: { qty: [">", 10] } })).toBe(false);
308
- });
309
-
310
- it("looks inside and/or groups, not just the top-level where", () => {
311
- expect(isExactlyEvaluable({
312
- logical: { type: "or", conditions: [
313
- { column: "a", operator: "==", value: 1 },
314
- { column: "b", operator: "==", value: 2 }
315
- ] }
316
- } as never)).toBe(true);
317
- expect(isExactlyEvaluable({
318
- logical: { type: "or", conditions: [
319
- { column: "a", operator: "==", value: 1 },
320
- { type: "and", conditions: [{ column: "b", operator: "<", value: "m" }] }
321
- ] }
322
- } as never)).toBe(false);
323
- });
324
-
325
- it("still refuses several conditions on one field when any of them orders", () => {
326
- expect(isExactlyEvaluable({ where: { a: [["==", 1], ["<", "m"]] } } as never)).toBe(false);
327
- expect(isExactlyEvaluable({ where: { a: [["==", 1], ["!=", 2]] } } as never)).toBe(true);
328
- });
329
-
330
- /**
331
- * A dotted key reaches through a relation — `applications.status` asks
332
- * about rows in another table. `matchesWhere` reads `row[field]` flat,
333
- * so the key resolves to `undefined` on every cached row and the
334
- * condition excludes all of them: a 200 with an empty list, which is
335
- * indistinguishable from "nothing matched". The cache holds one
336
- * collection and cannot answer a question about a second.
337
- */
338
- it("refuses a filter that reaches through a relation", () => {
339
- expect(isExactlyEvaluable({
340
- where: { "applications.status": ["in", ["applied"]] }
341
- } as never)).toBe(false);
342
- // The undotted sibling is unaffected.
343
- expect(isExactlyEvaluable({ where: { status: ["in", ["applied"]] } } as never)).toBe(true);
344
- });
345
-
346
- it("finds a relation path inside an and/or group too", () => {
347
- expect(isExactlyEvaluable({
348
- logical: { type: "or", conditions: [
349
- { column: "a", operator: "==", value: 1 },
350
- { column: "applications.status", operator: "==", value: "applied" }
351
- ] }
352
- } as never)).toBe(false);
353
- });
354
- });
355
-
356
- describe("isLocallySortable", () => {
357
- /**
358
- * Asked of the rows rather than the query, because unlike a filter this
359
- * one is decidable from the data in hand: the collator is reachable
360
- * only when a value cannot be read as a number.
361
- */
362
- it("accepts a column this page holds only as numbers", () => {
363
- expect(isLocallySortable([{ id: 1, n: 3 }, { id: 2, n: 10 }], ["n", "asc"])).toBe(true);
364
- // The wire's type erasure is already undone by `compareValues`.
365
- expect(isLocallySortable([{ id: 1, n: "3" }, { id: 2, n: "10" }], ["n", "asc"])).toBe(true);
366
- // Dates normalise to instants before any comparison happens.
367
- expect(isLocallySortable(
368
- [{ id: 1, at: new Date(1) }, { id: 2, at: new Date(2) }], ["at", "desc"]
369
- )).toBe(true);
370
- });
371
-
372
- it("refuses a column holding text, whose order is the database's to decide", () => {
373
- expect(isLocallySortable([{ id: 1, name: "apple" }, { id: 2, name: "Banana" }], ["name", "asc"]))
374
- .toBe(false);
375
- // One text value is enough — a column is one type, and the page that
376
- // happens to be cached does not get to vote.
377
- expect(isLocallySortable([{ id: 1, n: 3 }, { id: 2, n: "x" }], ["n", "asc"])).toBe(false);
378
- });
379
-
380
- it("ignores nulls, which are ordered by an explicit rule", () => {
381
- expect(isLocallySortable([{ id: 1, n: null }, { id: 2, n: 5 }], ["n", "asc"])).toBe(true);
382
- expect(isLocallySortable([{ id: 1 }, { id: 2 }], ["missing", "asc"])).toBe(true);
383
- });
384
-
385
- it("is vacuously true with no sort", () => {
386
- expect(isLocallySortable([{ id: 1, name: "a" }], undefined)).toBe(true);
387
- });
388
-
389
- /**
390
- * An aggregate over a relation is computed by the database and is not a
391
- * field on the row, so every cached row reads `undefined` for it. To
392
- * the null rule above that looks exactly like a column of nulls — which
393
- * it would call reproducible, and then hand back rows in id order: a
394
- * queue sorted by nothing at all, presented as the server's answer.
395
- */
396
- it("refuses an aggregate sort, in either spelling", () => {
397
- const rows = [{ id: 1, name: "a" }, { id: 2, name: "b" }];
398
- expect(isLocallySortable(rows, ["min(applications.created_at)", "asc"])).toBe(false);
399
- expect(isLocallySortable(rows, ["count(applications)", "desc"])).toBe(false);
400
- expect(isLocallySortable(
401
- rows,
402
- [{ relation: "applications", field: "created_at", agg: "min" }, "asc"]
403
- )).toBe(false);
404
- });
405
-
406
- it("refuses a multi-key sort whose second key is an aggregate", () => {
407
- // A sort the local side can only agree with down to its second
408
- // column is one it disagrees with.
409
- expect(isLocallySortable(
410
- [{ id: 1, n: 1 }, { id: 2, n: 2 }],
411
- [["n", "asc"], ["count(applications)", "desc"]]
412
- )).toBe(false);
413
- });
414
- });
415
-
416
- describe("primitives", () => {
417
- it("compareValues returns undefined rather than an order across NULL", () => {
418
- expect(compareValues(null, 1)).toBeUndefined();
419
- expect(compareValues(1, undefined)).toBeUndefined();
420
- expect(compareValues(1, 2)).toBeLessThan(0);
421
- expect(compareValues("b", "a")).toBeGreaterThan(0);
422
- });
423
-
424
- it("looseEquals treats null and undefined as the same absence", () => {
425
- expect(looseEquals(null, undefined)).toBe(true);
426
- expect(looseEquals(null, 0)).toBe(false);
427
- expect(looseEquals(true, "true")).toBe(true);
428
- expect(looseEquals(1, "1")).toBe(true);
429
- });
430
- });
431
- });