| author | |
| committer | |
| log | 47304f56e98bf841b757d16e472fb64a01c99fd2 |
| tree | 3ad08b3c5212b52b46b0f33e396d5c1928fce6fb |
| parent | 4b09ff4dba5fad5b7e2b44a0bdc2789918f74561 |
| signature |
3 files changed, 300 insertions(+), 11 deletions(-)
readme.md+2| ... | @@ -191,6 +191,7 @@ automatically implement `onRefetch` and `onRestore` callbacks. The current list | ... | @@ -191,6 +191,7 @@ automatically implement `onRefetch` and `onRestore` callbacks. The current list |
| 191 | - `arrayRemove` - remove items by a `filter` function | 191 | - `arrayRemove` - remove items by a `filter` function |
| 192 | - `arrayFilter` - preserve items by a `filter` function | 192 | - `arrayFilter` - preserve items by a `filter` function |
| 193 | - `arrayUpdate` - update items by a `filter` + `update` function | 193 | - `arrayUpdate` - update items by a `filter` + `update` function |
| 194 | - `arrayUpsert` - update items by a `filter`, or insert when there is no match | ||
| 194 | - `arrayInsertIndex` - insert an item at an index | 195 | - `arrayInsertIndex` - insert an item at an index |
| 195 | - Queries that are complex objects. Each function takes a type-safe object path to | 196 | - Queries that are complex objects. Each function takes a type-safe object path to |
| 196 | evaluate. | 197 | evaluate. |
| ... | @@ -204,6 +205,7 @@ automatically implement `onRefetch` and `onRestore` callbacks. The current list | ... | @@ -204,6 +205,7 @@ automatically implement `onRefetch` and `onRestore` callbacks. The current list |
| 204 | - `objArrayRemove` - remove items from array by `filter` | 205 | - `objArrayRemove` - remove items from array by `filter` |
| 205 | - `objArrayFilter` - preserve items from array by `filter` | 206 | - `objArrayFilter` - preserve items from array by `filter` |
| 206 | - `objArrayUpdate` - update items in array by `filter` + `update` | 207 | - `objArrayUpdate` - update items in array by `filter` + `update` |
| 208 | - `objArrayUpsert` - update items in array by `filter`, or insert when there is no match | ||
| 207 | - `objArrayInsertIndex` - insert an item in an array at an index | 209 | - `objArrayInsertIndex` - insert an item in an array at an index |
| 208 | 210 | ||
| 209 | ## Debouncing | 211 | ## Debouncing |
src/tanstack-query.ts+106-11| ... | @@ -7,6 +7,10 @@ export type QueryKeyAndFn<T = unknown, Key extends QueryKey = QueryKey> = { | ... | @@ -7,6 +7,10 @@ export type QueryKeyAndFn<T = unknown, Key extends QueryKey = QueryKey> = { |
| 7 | queryFn?: QueryFunction<T, any, never> | undefined; | 7 | queryFn?: QueryFunction<T, any, never> | undefined; |
| 8 | }; | 8 | }; |
| 9 | 9 | ||
| 10 | type RequireAtLeastOneKey<T> = { | ||
| 11 | [K in keyof T]-?: Required<Pick<T, K>> & Partial<Omit<T, K>>; | ||
| 12 | }[keyof T]; | ||
| 13 | |||
| 10 | export function boundQueryClientGet( | 14 | export function boundQueryClientGet( |
| 11 | queryClient: QueryClient, | 15 | queryClient: QueryClient, |
| 12 | ): <T>({ queryKey }: QueryKeyAndFn<T>) => T | undefined { | 16 | ): <T>({ queryKey }: QueryKeyAndFn<T>) => T | undefined { |
| ... | @@ -50,7 +54,7 @@ class TanstackQueryOptimisticHelpers { | ... | @@ -50,7 +54,7 @@ class TanstackQueryOptimisticHelpers { |
| 50 | set = <Data>( | 54 | set = <Data>( |
| 51 | queryKey: QueryKeyAndFn<Data>, | 55 | queryKey: QueryKeyAndFn<Data>, |
| 52 | value: Data | ((prev: Data | undefined) => Data | undefined), | 56 | value: Data | ((prev: Data | undefined) => Data | undefined), |
| 53 | ) => { | 57 | ): void => { |
| 54 | const prev = this.#get(queryKey); | 58 | const prev = this.#get(queryKey); |
| 55 | 59 | ||
| 56 | const newValue = typeof value === "function" | 60 | const newValue = typeof value === "function" |
| ... | @@ -378,8 +382,11 @@ class TanstackQueryOptimisticHelpers { | ... | @@ -378,8 +382,11 @@ class TanstackQueryOptimisticHelpers { |
| 378 | >( | 382 | >( |
| 379 | queryKey: QueryKeyAndFn<Data>, | 383 | queryKey: QueryKeyAndFn<Data>, |
| 380 | path: Path, | 384 | path: Path, |
| 381 | { filter, update }: { | 385 | { |
| 382 | filter: ( | 386 | filter, |
| 387 | update, | ||
| 388 | }: { | ||
| 389 | filter?: ( | ||
| 383 | item: GetObjectPath<Data, Path> extends Array<infer T> ? T : never, | 390 | item: GetObjectPath<Data, Path> extends Array<infer T> ? T : never, |
| 384 | index: number, | 391 | index: number, |
| 385 | ) => boolean; | 392 | ) => boolean; |
| ... | @@ -387,13 +394,62 @@ class TanstackQueryOptimisticHelpers { | ... | @@ -387,13 +394,62 @@ class TanstackQueryOptimisticHelpers { |
| 387 | item: GetObjectPath<Data, Path> extends Array<infer T> ? T : never, | 394 | item: GetObjectPath<Data, Path> extends Array<infer T> ? T : never, |
| 388 | ) => GetObjectPath<Data, Path> extends Array<infer T> ? T : never; | 395 | ) => GetObjectPath<Data, Path> extends Array<infer T> ? T : never; |
| 389 | }, | 396 | }, |
| 390 | ) => { | 397 | ): { inserted: boolean } => { |
| 398 | return this.objArrayUpsert(queryKey, path, { | ||
| 399 | filter: filter ?? (() => true), | ||
| 400 | update, | ||
| 401 | }); | ||
| 402 | }; | ||
| 403 | |||
| 404 | /** | ||
| 405 | * Update items in an array that match a `filter`, or insert a new one if there was no match. | ||
| 406 | * If the query or path doesn't exist, the updater is skipped. | ||
| 407 | */ | ||
| 408 | objArrayUpsert = < | ||
| 409 | Data extends object, | ||
| 410 | const Path extends AllObjectPaths<Data>, | ||
| 411 | >( | ||
| 412 | queryKey: QueryKeyAndFn<Data>, | ||
| 413 | path: Path, | ||
| 414 | { | ||
| 415 | filter, | ||
| 416 | update, | ||
| 417 | insert, | ||
| 418 | }: | ||
| 419 | & { | ||
| 420 | filter: ( | ||
| 421 | item: GetObjectPath<Data, Path> extends Array<infer T> ? T : never, | ||
| 422 | index: number, | ||
| 423 | ) => boolean; | ||
| 424 | } | ||
| 425 | & RequireAtLeastOneKey<{ | ||
| 426 | /** Defaults to the identity function */ | ||
| 427 | update: ( | ||
| 428 | item: GetObjectPath<Data, Path> extends Array<infer T> ? T : never, | ||
| 429 | ) => GetObjectPath<Data, Path> extends Array<infer T> ? T : never; | ||
| 430 | /** Defaults to not inserting */ | ||
| 431 | insert: () => GetObjectPath<Data, Path> extends Array<infer T> ? T : never; | ||
| 432 | }>, | ||
| 433 | ): { inserted: boolean } => { | ||
| 391 | const prev = this.#get(queryKey); | 434 | const prev = this.#get(queryKey); |
| 392 | if (!prev) return; | 435 | if (!prev) return { inserted: false }; |
| 393 | const { value: original, exists } = getPath(prev, path); | 436 | const { value: original, exists } = getPath(prev, path); |
| 394 | if (!exists || !Array.isArray(original)) return; | 437 | if (!exists || !Array.isArray(original)) return { inserted: false }; |
| 395 | 438 | ||
| 396 | const newArray = original.map((item, index) => filter(item, index) ? update(item) : item); | 439 | let matched = false; |
| 440 | const newArray = original.map((item, index) => { | ||
| 441 | if (filter(item, index)) { | ||
| 442 | matched = true; | ||
| 443 | return update ? update(item) : item; | ||
| 444 | } else { | ||
| 445 | return item; | ||
| 446 | } | ||
| 447 | }); | ||
| 448 | let inserted = false; | ||
| 449 | if (!matched && insert) { | ||
| 450 | newArray.push(insert()); | ||
| 451 | inserted = true; | ||
| 452 | } | ||
| 397 | this.#set( | 453 | this.#set( |
| 398 | queryKey, | 454 | queryKey, |
| 399 | (obj) => obj ? setPath(obj, path, newArray as any) : obj, | 455 | (obj) => obj ? setPath(obj, path, newArray as any) : obj, |
| ... | @@ -402,6 +458,7 @@ class TanstackQueryOptimisticHelpers { | ... | @@ -402,6 +458,7 @@ class TanstackQueryOptimisticHelpers { |
| 402 | // TODO: splice items back in case original changed | 458 | // TODO: splice items back in case original changed |
| 403 | this.#set(queryKey, (obj) => obj ? setPath(obj, path, original) : obj); | 459 | this.#set(queryKey, (obj) => obj ? setPath(obj, path, original) : obj); |
| 404 | }); | 460 | }); |
| 461 | return { inserted }; | ||
| 405 | }; | 462 | }; |
| 406 | 463 | ||
| 407 | /** | 464 | /** |
| ... | @@ -439,7 +496,7 @@ class TanstackQueryOptimisticHelpers { | ... | @@ -439,7 +496,7 @@ class TanstackQueryOptimisticHelpers { |
| 439 | }; | 496 | }; |
| 440 | 497 | ||
| 441 | /** | 498 | /** |
| 442 | * Push item(s) to the end of an array at an object path. | 499 | * Push item(s) to the end of an array. |
| 443 | * If the query or path doesn't exist, the updater is skipped. | 500 | * If the query or path doesn't exist, the updater is skipped. |
| 444 | */ | 501 | */ |
| 445 | arrayPush = <Data>(queryKey: QueryKeyAndFn<Data[] | null>, ...items: Data[]) => { | 502 | arrayPush = <Data>(queryKey: QueryKeyAndFn<Data[] | null>, ...items: Data[]) => { |
| ... | @@ -531,16 +588,54 @@ class TanstackQueryOptimisticHelpers { | ... | @@ -531,16 +588,54 @@ class TanstackQueryOptimisticHelpers { |
| 531 | filter?: (item: Data, index: number) => boolean; | 588 | filter?: (item: Data, index: number) => boolean; |
| 532 | update: (item: Data) => Data; | 589 | update: (item: Data) => Data; |
| 533 | }, | 590 | }, |
| 534 | ) => { | 591 | ): { inserted: boolean } => { |
| 592 | return this.arrayUpsert(queryKey, { filter: filter ?? (() => true), update }); | ||
| 593 | }; | ||
| 594 | |||
| 595 | /** | ||
| 596 | * Update items in an array that match a `filter`, or insert a new one if there was no match. | ||
| 597 | * If the query doesn't exist, the updater is skipped. | ||
| 598 | */ | ||
| 599 | arrayUpsert = <Data>( | ||
| 600 | queryKey: QueryKeyAndFn<Data[] | null>, | ||
| 601 | { | ||
| 602 | filter, | ||
| 603 | update, | ||
| 604 | insert, | ||
| 605 | }: | ||
| 606 | & { | ||
| 607 | filter: (item: Data, index: number) => boolean; | ||
| 608 | } | ||
| 609 | & RequireAtLeastOneKey<{ | ||
| 610 | /** Defaults to the identity function */ | ||
| 611 | update: (item: Data) => Data; | ||
| 612 | /** Defaults to not inserting */ | ||
| 613 | insert: () => Data; | ||
| 614 | }>, | ||
| 615 | ): { inserted: boolean } => { | ||
| 535 | const prev = this.#get(queryKey); | 616 | const prev = this.#get(queryKey); |
| 536 | if (!prev || !Array.isArray(prev)) return; | 617 | if (!prev || !Array.isArray(prev)) return { inserted: false }; |
| 537 | 618 | ||
| 538 | const newArray = prev.map((item, index) => (filter ? filter(item, index) : true) ? update(item) : item); | 619 | let matched = false; |
| 620 | const newArray = prev.map((item, index) => { | ||
| 621 | if (filter(item, index)) { | ||
| 622 | matched = true; | ||
| 623 | return update ? update(item) : item; | ||
| 624 | } else { | ||
| 625 | return item; | ||
| 626 | } | ||
| 627 | }); | ||
| 628 | let inserted = false; | ||
| 629 | if (!matched && insert) { | ||
| 630 | newArray.push(insert()); | ||
| 631 | inserted = true; | ||
| 632 | } | ||
| 539 | this.#set(queryKey, newArray); | 633 | this.#set(queryKey, newArray); |
| 540 | this.#onRestore(() => { | 634 | this.#onRestore(() => { |
| 541 | // TODO: splice items back in case original changed | 635 | // TODO: splice items back in case original changed |
| 542 | this.#set(queryKey, prev); | 636 | this.#set(queryKey, prev); |
| 543 | }); | 637 | }); |
| 638 | return { inserted }; | ||
| 544 | }; | 639 | }; |
| 545 | 640 | ||
| 546 | /** | 641 | /** |
test/tanstack-query-helpers.test.ts+192| ... | @@ -15,6 +15,11 @@ interface TestData { | ... | @@ -15,6 +15,11 @@ interface TestData { |
| 15 | tags: string[]; | 15 | tags: string[]; |
| 16 | } | 16 | } |
| 17 | 17 | ||
| 18 | interface TestArrayItem { | ||
| 19 | id: number; | ||
| 20 | label: string; | ||
| 21 | } | ||
| 22 | |||
| 18 | const initialData: TestData = { | 23 | const initialData: TestData = { |
| 19 | name: "Test", | 24 | name: "Test", |
| 20 | count: 10, | 25 | count: 10, |
| ... | @@ -31,6 +36,12 @@ const initialData: TestData = { | ... | @@ -31,6 +36,12 @@ const initialData: TestData = { |
| 31 | tags: ["alpha", "beta", "gamma"], | 36 | tags: ["alpha", "beta", "gamma"], |
| 32 | }; | 37 | }; |
| 33 | 38 | ||
| 39 | const initialArrayItems: TestArrayItem[] = [ | ||
| 40 | { id: 1, label: "first" }, | ||
| 41 | { id: 2, label: "second" }, | ||
| 42 | { id: 3, label: "third" }, | ||
| 43 | ]; | ||
| 44 | |||
| 34 | function createTestQueryClient() { | 45 | function createTestQueryClient() { |
| 35 | const client = new QueryClient({ | 46 | const client = new QueryClient({ |
| 36 | defaultOptions: { queries: { retry: false } }, | 47 | defaultOptions: { queries: { retry: false } }, |
| ... | @@ -46,6 +57,21 @@ function createTestQueryClient() { | ... | @@ -46,6 +57,21 @@ function createTestQueryClient() { |
| 46 | return { client, queryTest }; | 57 | return { client, queryTest }; |
| 47 | } | 58 | } |
| 48 | 59 | ||
| 60 | function createArrayQueryClient() { | ||
| 61 | const client = new QueryClient({ | ||
| 62 | defaultOptions: { queries: { retry: false } }, | ||
| 63 | }); | ||
| 64 | |||
| 65 | const queryArray = queryOptions({ | ||
| 66 | queryKey: ["test-array"], | ||
| 67 | queryFn: (): TestArrayItem[] => initialArrayItems, | ||
| 68 | }); | ||
| 69 | |||
| 70 | client.setQueryData(queryArray.queryKey, structuredClone(initialArrayItems)); | ||
| 71 | |||
| 72 | return { client, queryArray }; | ||
| 73 | } | ||
| 74 | |||
| 49 | test("helpers can be spread and retain bound this", () => { | 75 | test("helpers can be spread and retain bound this", () => { |
| 50 | const { client, queryTest } = createTestQueryClient(); | 76 | const { client, queryTest } = createTestQueryClient(); |
| 51 | 77 | ||
| ... | @@ -663,6 +689,172 @@ test("arrayUpdateItem - predicate receives index", () => { | ... | @@ -663,6 +689,172 @@ test("arrayUpdateItem - predicate receives index", () => { |
| 663 | assertEquals(result?.items[0].label, "FIRST"); | 689 | assertEquals(result?.items[0].label, "FIRST"); |
| 664 | }); | 690 | }); |
| 665 | 691 | ||
| 692 | test("arrayUpdateItem - should update all items when filter is omitted", () => { | ||
| 693 | const { client, queryTest } = createTestQueryClient(); | ||
| 694 | |||
| 695 | const helpers = queryClientOptimisticHelpers(client)({ | ||
| 696 | onRestore: () => {}, | ||
| 697 | onRefetch: () => {}, | ||
| 698 | }); | ||
| 699 | |||
| 700 | helpers.objArrayUpdate( | ||
| 701 | queryTest, | ||
| 702 | ["items"], | ||
| 703 | { | ||
| 704 | update: (item) => ({ ...item, label: item.label.toUpperCase() }), | ||
| 705 | }, | ||
| 706 | ); | ||
| 707 | |||
| 708 | const result = client.getQueryData<TestData>(queryTest.queryKey); | ||
| 709 | assertEquals(result?.items.map((item) => item.label), ["FIRST", "SECOND", "THIRD"]); | ||
| 710 | }); | ||
| 711 | |||
| 712 | // ============================================================================ | ||
| 713 | // objArrayUpsert() tests | ||
| 714 | // ============================================================================ | ||
| 715 | |||
| 716 | test("objArrayUpsert - should update matching items without inserting", () => { | ||
| 717 | const { client, queryTest } = createTestQueryClient(); | ||
| 718 | const restoreFns: Array<() => void> = []; | ||
| 719 | |||
| 720 | const helpers = queryClientOptimisticHelpers(client)({ | ||
| 721 | onRestore: (fn) => restoreFns.push(fn), | ||
| 722 | onRefetch: () => {}, | ||
| 723 | }); | ||
| 724 | |||
| 725 | const upsertResult = helpers.objArrayUpsert( | ||
| 726 | queryTest, | ||
| 727 | ["items"], | ||
| 728 | { | ||
| 729 | filter: (item) => item.id === 2, | ||
| 730 | update: (item) => ({ ...item, label: "UPDATED" }), | ||
| 731 | insert: () => ({ id: 4, label: "fourth" }), | ||
| 732 | }, | ||
| 733 | ); | ||
| 734 | |||
| 735 | const result = client.getQueryData<TestData>(queryTest.queryKey); | ||
| 736 | assertEquals(upsertResult, { inserted: false }); | ||
| 737 | assertEquals(result?.items.length, 3); | ||
| 738 | assertEquals(result?.items[1], { id: 2, label: "UPDATED" }); | ||
| 739 | |||
| 740 | restoreFns[0](); | ||
| 741 | const restored = client.getQueryData<TestData>(queryTest.queryKey); | ||
| 742 | assertEquals(restored?.items, initialData.items); | ||
| 743 | }); | ||
| 744 | |||
| 745 | test("objArrayUpsert - should insert a new item when nothing matches", () => { | ||
| 746 | const { client, queryTest } = createTestQueryClient(); | ||
| 747 | const restoreFns: Array<() => void> = []; | ||
| 748 | |||
| 749 | const helpers = queryClientOptimisticHelpers(client)({ | ||
| 750 | onRestore: (fn) => restoreFns.push(fn), | ||
| 751 | onRefetch: () => {}, | ||
| 752 | }); | ||
| 753 | |||
| 754 | const upsertResult = helpers.objArrayUpsert( | ||
| 755 | queryTest, | ||
| 756 | ["items"], | ||
| 757 | { | ||
| 758 | filter: (item) => item.id === 4, | ||
| 759 | insert: () => ({ id: 4, label: "fourth" }), | ||
| 760 | }, | ||
| 761 | ); | ||
| 762 | |||
| 763 | const result = client.getQueryData<TestData>(queryTest.queryKey); | ||
| 764 | assertEquals(upsertResult, { inserted: true }); | ||
| 765 | assertEquals(result?.items.length, 4); | ||
| 766 | assertEquals(result?.items[3], { id: 4, label: "fourth" }); | ||
| 767 | |||
| 768 | restoreFns[0](); | ||
| 769 | const restored = client.getQueryData<TestData>(queryTest.queryKey); | ||
| 770 | assertEquals(restored?.items, initialData.items); | ||
| 771 | }); | ||
| 772 | |||
| 773 | // ============================================================================ | ||
| 774 | // arrayUpsert() tests | ||
| 775 | // ============================================================================ | ||
| 776 | |||
| 777 | test("arrayUpsert - should update matching items without inserting", () => { | ||
| 778 | const { client, queryArray } = createArrayQueryClient(); | ||
| 779 | const restoreFns: Array<() => void> = []; | ||
| 780 | |||
| 781 | const helpers = queryClientOptimisticHelpers(client)({ | ||
| 782 | onRestore: (fn) => restoreFns.push(fn), | ||
| 783 | onRefetch: () => {}, | ||
| 784 | }); | ||
| 785 | |||
| 786 | const upsertResult = helpers.arrayUpsert( | ||
| 787 | queryArray, | ||
| 788 | { | ||
| 789 | filter: (item) => item.id === 2, | ||
| 790 | update: (item) => ({ ...item, label: "UPDATED" }), | ||
| 791 | insert: () => ({ id: 4, label: "fourth" }), | ||
| 792 | }, | ||
| 793 | ); | ||
| 794 | |||
| 795 | const result = client.getQueryData<TestArrayItem[]>(queryArray.queryKey); | ||
| 796 | assertEquals(upsertResult, { inserted: false }); | ||
| 797 | assertEquals(result?.length, 3); | ||
| 798 | assertEquals(result?.[1], { id: 2, label: "UPDATED" }); | ||
| 799 | |||
| 800 | restoreFns[0](); | ||
| 801 | const restored = client.getQueryData<TestArrayItem[]>(queryArray.queryKey); | ||
| 802 | assertEquals(restored, initialArrayItems); | ||
| 803 | }); | ||
| 804 | |||
| 805 | test("arrayUpsert - should insert a new item when nothing matches", () => { | ||
| 806 | const { client, queryArray } = createArrayQueryClient(); | ||
| 807 | const restoreFns: Array<() => void> = []; | ||
| 808 | |||
| 809 | const helpers = queryClientOptimisticHelpers(client)({ | ||
| 810 | onRestore: (fn) => restoreFns.push(fn), | ||
| 811 | onRefetch: () => {}, | ||
| 812 | }); | ||
| 813 | |||
| 814 | const upsertResult = helpers.arrayUpsert( | ||
| 815 | queryArray, | ||
| 816 | { | ||
| 817 | filter: (item) => item.id === 4, | ||
| 818 | insert: () => ({ id: 4, label: "fourth" }), | ||
| 819 | }, | ||
| 820 | ); | ||
| 821 | |||
| 822 | const result = client.getQueryData<TestArrayItem[]>(queryArray.queryKey); | ||
| 823 | assertEquals(upsertResult, { inserted: true }); | ||
| 824 | assertEquals(result?.length, 4); | ||
| 825 | assertEquals(result?.[3], { id: 4, label: "fourth" }); | ||
| 826 | |||
| 827 | restoreFns[0](); | ||
| 828 | const restored = client.getQueryData<TestArrayItem[]>(queryArray.queryKey); | ||
| 829 | assertEquals(restored, initialArrayItems); | ||
| 830 | }); | ||
| 831 | |||
| 832 | test("arrayUpsert - should skip if query doesn't exist", () => { | ||
| 833 | const { client } = createTestQueryClient(); | ||
| 834 | const restoreFns: Array<() => void> = []; | ||
| 835 | |||
| 836 | const helpers = queryClientOptimisticHelpers(client)({ | ||
| 837 | onRestore: (fn) => restoreFns.push(fn), | ||
| 838 | onRefetch: () => {}, | ||
| 839 | }); | ||
| 840 | |||
| 841 | const queryNonexistent = queryOptions({ | ||
| 842 | queryKey: ["nonexistent-array"], | ||
| 843 | queryFn: (): TestArrayItem[] => initialArrayItems, | ||
| 844 | }); | ||
| 845 | |||
| 846 | const upsertResult = helpers.arrayUpsert( | ||
| 847 | queryNonexistent, | ||
| 848 | { | ||
| 849 | filter: (item) => item.id === 4, | ||
| 850 | insert: () => ({ id: 4, label: "fourth" }), | ||
| 851 | }, | ||
| 852 | ); | ||
| 853 | |||
| 854 | assertEquals(upsertResult, { inserted: false }); | ||
| 855 | assertEquals(restoreFns.length, 0); | ||
| 856 | }); | ||
| 857 | |||
| 666 | // ============================================================================ | 858 | // ============================================================================ |
| 667 | // arrayInsertIndex() tests | 859 | // arrayInsertIndex() tests |
| 668 | // ============================================================================ | 860 | // ============================================================================ |