placementt-core 1.400.1200 → 1.400.1201
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.
- package/lib/__tests__/useDataViewerPaginator.test.js +178 -11
- package/lib/__tests__/useDataViewerPaginator.test.js.map +1 -1
- package/lib/hooks.js +74 -21
- package/lib/hooks.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/useDataViewerPaginator.test.tsx +204 -11
- package/src/hooks.tsx +74 -22
|
@@ -478,7 +478,7 @@ describe('Suite 2 – Firestore query mode', () => {
|
|
|
478
478
|
await (0, react_1.waitFor)(() => expect(result.current.loading).toBe('loaded'));
|
|
479
479
|
});
|
|
480
480
|
// ── Backward pagination ─────────────────────────────────────────────────
|
|
481
|
-
it('2.13 pageDown
|
|
481
|
+
it('2.13 pageDown reloads the previous page forward from where it started', async () => {
|
|
482
482
|
mockGetDocs
|
|
483
483
|
.mockResolvedValueOnce(makeSnap(makeDocs(10, 'a'))) // page 1
|
|
484
484
|
.mockResolvedValueOnce(makeSnap(makeDocs(10, 'b'))) // page 2
|
|
@@ -488,11 +488,16 @@ describe('Suite 2 – Firestore query mode', () => {
|
|
|
488
488
|
(0, react_1.act)(() => result.current.pageUp());
|
|
489
489
|
await (0, react_1.waitFor)(() => expect(result.current.page).toBe(2));
|
|
490
490
|
await (0, react_1.waitFor)(() => expect(result.current.loading).not.toBe(true));
|
|
491
|
+
mockLimitToLast.mockClear();
|
|
492
|
+
mockEndBefore.mockClear();
|
|
491
493
|
(0, react_1.act)(() => result.current.pageDown());
|
|
492
494
|
await (0, react_1.waitFor)(() => expect(result.current.page).toBe(1));
|
|
493
|
-
expect(
|
|
494
|
-
//
|
|
495
|
-
expect(
|
|
495
|
+
await (0, react_1.waitFor)(() => expect(mockGetDocs).toHaveBeenCalledTimes(3));
|
|
496
|
+
// Page 1 is reloaded with limit() from the start, not queried backwards.
|
|
497
|
+
expect(mockLimitToLast).not.toHaveBeenCalled();
|
|
498
|
+
expect(mockEndBefore).not.toHaveBeenCalled();
|
|
499
|
+
await (0, react_1.waitFor)(() => expect(Object.keys(result.current.tableData ?? {})[0]).toBe('a0'));
|
|
500
|
+
expect(Object.keys(result.current.tableData ?? {}).length).toBe(10);
|
|
496
501
|
});
|
|
497
502
|
it('2.14 pageDown from page 1 sets page to 0 (UI must guard this externally)', async () => {
|
|
498
503
|
// The hook itself has no floor on page — pageDown() from page 1 sets page[0]=0.
|
|
@@ -581,7 +586,7 @@ describe('Suite 2 – Firestore query mode', () => {
|
|
|
581
586
|
expect(keys[0]).toBe('a0');
|
|
582
587
|
expect(keys[keys.length - 1]).toBe('b5');
|
|
583
588
|
});
|
|
584
|
-
it('2.20 pageDown
|
|
589
|
+
it('2.20 pageDown to a page spanning the boundary reloads it from query A', async () => {
|
|
585
590
|
const queryA = { path: ['schools'] };
|
|
586
591
|
const queryB = { path: ['employers'] };
|
|
587
592
|
// Page 1: cross-boundary (4 from A, 6 from B)
|
|
@@ -595,15 +600,19 @@ describe('Suite 2 – Firestore query mode', () => {
|
|
|
595
600
|
(0, react_1.act)(() => result.current.pageUp());
|
|
596
601
|
await (0, react_1.waitFor)(() => expect(result.current.page).toBe(2));
|
|
597
602
|
await (0, react_1.waitFor)(() => expect(result.current.loading).not.toBe(true));
|
|
598
|
-
// Page back:
|
|
603
|
+
// Page back: page 1 is reloaded forward from query A, then spills into B as it did.
|
|
604
|
+
FirestoreMod.collection.mockClear();
|
|
599
605
|
mockGetDocs
|
|
600
|
-
.mockResolvedValueOnce(makeSnap(makeDocs(
|
|
601
|
-
.mockResolvedValueOnce(makeSnap(makeDocs(
|
|
606
|
+
.mockResolvedValueOnce(makeSnap(makeDocs(4, 'a')))
|
|
607
|
+
.mockResolvedValueOnce(makeSnap(makeDocs(6, 'b')));
|
|
602
608
|
(0, react_1.act)(() => result.current.pageDown());
|
|
603
609
|
await (0, react_1.waitFor)(() => expect(result.current.page).toBe(1));
|
|
604
|
-
await (0, react_1.waitFor)(() => expect(
|
|
605
|
-
|
|
606
|
-
expect(
|
|
610
|
+
await (0, react_1.waitFor)(() => expect(FirestoreMod.collection.mock.calls.length).toBe(2));
|
|
611
|
+
const paths = FirestoreMod.collection.mock.calls.map((c) => c[1]);
|
|
612
|
+
expect(paths).toEqual(['schools', 'employers']);
|
|
613
|
+
const keys = Object.keys(result.current.tableData ?? {});
|
|
614
|
+
expect(keys[0]).toBe('a0');
|
|
615
|
+
expect(keys[keys.length - 1]).toBe('b5');
|
|
607
616
|
});
|
|
608
617
|
it('2.21 going back with sparse results fires a second getDocs call (backward spillover path)', async () => {
|
|
609
618
|
// Verifies that the cursorPos-decrements-and-recurses code path fires for backward navigation.
|
|
@@ -633,6 +642,164 @@ describe('Suite 2 – Firestore query mode', () => {
|
|
|
633
642
|
// Both queries were called during the backward pass
|
|
634
643
|
expect(mockGetDocs.mock.calls.length).toBeGreaterThan(callsBeforeBack + 1);
|
|
635
644
|
});
|
|
645
|
+
it('2.21d additionalEntryProcessing runs for every row at once, not one after another', async () => {
|
|
646
|
+
mockGetDocs.mockResolvedValueOnce(makeSnap(makeDocs(5, 'p')));
|
|
647
|
+
let inFlight = 0;
|
|
648
|
+
let maxInFlight = 0;
|
|
649
|
+
const processing = jest.fn(async (k, v) => {
|
|
650
|
+
inFlight += 1;
|
|
651
|
+
maxInFlight = Math.max(maxInFlight, inFlight);
|
|
652
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
653
|
+
inFlight -= 1;
|
|
654
|
+
return [k, v];
|
|
655
|
+
});
|
|
656
|
+
const { result } = (0, react_1.renderHook)(() => (0, hooks_1.useDataViewerPaginator)({ view: 'table', data: [q()], queryLimit: 10, additionalEntryProcessing: processing }));
|
|
657
|
+
await (0, react_1.waitFor)(() => expect(Object.keys(result.current.tableData ?? {}).length).toBe(5));
|
|
658
|
+
expect(processing).toHaveBeenCalledTimes(5);
|
|
659
|
+
expect(maxInFlight).toBe(5);
|
|
660
|
+
});
|
|
661
|
+
it('2.21e rows keep query order when lookups finish out of order, and dropped rows stay dropped', async () => {
|
|
662
|
+
mockGetDocs.mockResolvedValueOnce(makeSnap(makeDocs(5, 'p')));
|
|
663
|
+
// Later rows finish first; p2 is dropped by processing.
|
|
664
|
+
const processing = jest.fn(async (k, v) => {
|
|
665
|
+
await new Promise((r) => setTimeout(r, 50 - Number(k.slice(1)) * 10));
|
|
666
|
+
return k === 'p2' ? undefined : [k, v];
|
|
667
|
+
});
|
|
668
|
+
const { result } = (0, react_1.renderHook)(() => (0, hooks_1.useDataViewerPaginator)({ view: 'table', data: [q()], queryLimit: 10, additionalEntryProcessing: processing }));
|
|
669
|
+
await (0, react_1.waitFor)(() => expect(Object.keys(result.current.tableData ?? {}).length).toBe(4));
|
|
670
|
+
expect(Object.keys(result.current.tableData ?? {})).toEqual(['p0', 'p1', 'p3', 'p4']);
|
|
671
|
+
});
|
|
672
|
+
it('2.21b back from an empty page after an exactly-full cross-query page restores it', async () => {
|
|
673
|
+
// The careers hub Schools table: institutes + careersInvites, exactly queryLimit rows
|
|
674
|
+
// in total, so "next" lands on an empty page. Going back used to show nothing.
|
|
675
|
+
const queryA = { path: ['schools'] };
|
|
676
|
+
const queryB = { path: ['employers'] };
|
|
677
|
+
mockGetDocs
|
|
678
|
+
.mockResolvedValueOnce(makeSnap(makeDocs(4, 'a')))
|
|
679
|
+
.mockResolvedValueOnce(makeSnap(makeDocs(6, 'b')));
|
|
680
|
+
const { result } = (0, react_1.renderHook)(() => (0, hooks_1.useDataViewerPaginator)({ view: 'table', data: [queryA, queryB], queryLimit: 10 }));
|
|
681
|
+
await (0, react_1.waitFor)(() => expect(result.current.loading).not.toBe(true));
|
|
682
|
+
const firstPage = Object.keys(result.current.tableData ?? {});
|
|
683
|
+
expect(firstPage.length).toBe(10);
|
|
684
|
+
mockGetDocs.mockResolvedValueOnce(makeSnap([]));
|
|
685
|
+
(0, react_1.act)(() => result.current.pageUp());
|
|
686
|
+
await (0, react_1.waitFor)(() => expect(result.current.page).toBe(2));
|
|
687
|
+
await (0, react_1.waitFor)(() => expect(result.current.loading).toBe('loaded'));
|
|
688
|
+
expect(Object.keys(result.current.tableData ?? {}).length).toBe(0);
|
|
689
|
+
mockGetDocs
|
|
690
|
+
.mockResolvedValueOnce(makeSnap(makeDocs(4, 'a')))
|
|
691
|
+
.mockResolvedValueOnce(makeSnap(makeDocs(6, 'b')));
|
|
692
|
+
(0, react_1.act)(() => result.current.pageDown());
|
|
693
|
+
await (0, react_1.waitFor)(() => expect(result.current.page).toBe(1));
|
|
694
|
+
await (0, react_1.waitFor)(() => expect(mockGetDocs).toHaveBeenCalledTimes(5));
|
|
695
|
+
await (0, react_1.waitFor)(() => expect(Object.keys(result.current.tableData ?? {})).toEqual(firstPage));
|
|
696
|
+
});
|
|
697
|
+
it('2.21c back from page 3 to a page 2 that spans both queries', async () => {
|
|
698
|
+
const queryA = { path: ['schools'] };
|
|
699
|
+
const queryB = { path: ['employers'] };
|
|
700
|
+
mockGetDocs.mockResolvedValueOnce(makeSnap(makeDocs(10, 'a')));
|
|
701
|
+
const { result } = (0, react_1.renderHook)(() => (0, hooks_1.useDataViewerPaginator)({ view: 'table', data: [queryA, queryB], queryLimit: 10 }));
|
|
702
|
+
await (0, react_1.waitFor)(() => expect(result.current.loading).not.toBe(true));
|
|
703
|
+
mockGetDocs
|
|
704
|
+
.mockResolvedValueOnce(makeSnap(makeDocs(3, 'a2-')))
|
|
705
|
+
.mockResolvedValueOnce(makeSnap(makeDocs(7, 'b')));
|
|
706
|
+
(0, react_1.act)(() => result.current.pageUp());
|
|
707
|
+
await (0, react_1.waitFor)(() => expect(result.current.page).toBe(2));
|
|
708
|
+
// Page 1's rows stay up while page 2 loads, so wait for page 2's own rows.
|
|
709
|
+
await (0, react_1.waitFor)(() => expect(Object.keys(result.current.tableData ?? {})[0]).toBe('a2-0'));
|
|
710
|
+
const secondPage = Object.keys(result.current.tableData ?? {});
|
|
711
|
+
expect(secondPage.length).toBe(10);
|
|
712
|
+
mockGetDocs.mockResolvedValueOnce(makeSnap(makeDocs(2, 'b3-')));
|
|
713
|
+
(0, react_1.act)(() => result.current.pageUp());
|
|
714
|
+
await (0, react_1.waitFor)(() => expect(result.current.page).toBe(3));
|
|
715
|
+
await (0, react_1.waitFor)(() => expect(result.current.loading).toBe('loaded'));
|
|
716
|
+
mockStartAfter.mockClear();
|
|
717
|
+
mockGetDocs
|
|
718
|
+
.mockResolvedValueOnce(makeSnap(makeDocs(3, 'a2-')))
|
|
719
|
+
.mockResolvedValueOnce(makeSnap(makeDocs(7, 'b')));
|
|
720
|
+
(0, react_1.act)(() => result.current.pageDown());
|
|
721
|
+
await (0, react_1.waitFor)(() => expect(result.current.page).toBe(2));
|
|
722
|
+
// Reloaded from where page 2 started: after the last row of page 1.
|
|
723
|
+
await (0, react_1.waitFor)(() => expect(mockStartAfter).toHaveBeenCalledWith('a9'));
|
|
724
|
+
await (0, react_1.waitFor)(() => expect(Object.keys(result.current.tableData ?? {})).toEqual(secondPage));
|
|
725
|
+
});
|
|
726
|
+
it('2.21f a revisited page shows its cached rows at once, then the refreshed rows', async () => {
|
|
727
|
+
mockGetDocs
|
|
728
|
+
.mockResolvedValueOnce(makeSnap(makeDocs(10, 'a')))
|
|
729
|
+
.mockResolvedValueOnce(makeSnap(makeDocs(10, 'b')));
|
|
730
|
+
const { result } = (0, react_1.renderHook)(() => (0, hooks_1.useDataViewerPaginator)({ view: 'table', data: [q()], queryLimit: 10 }));
|
|
731
|
+
await (0, react_1.waitFor)(() => expect(result.current.loading).not.toBe(true));
|
|
732
|
+
(0, react_1.act)(() => result.current.pageUp());
|
|
733
|
+
await (0, react_1.waitFor)(() => expect(Object.keys(result.current.tableData ?? {})[0]).toBe('b0'));
|
|
734
|
+
// Hold the refresh of page 1 so the cached rows can be seen first.
|
|
735
|
+
let release = () => undefined;
|
|
736
|
+
mockGetDocs.mockImplementationOnce(() => new Promise((r) => { release = r; }));
|
|
737
|
+
(0, react_1.act)(() => result.current.pageDown());
|
|
738
|
+
await (0, react_1.waitFor)(() => expect(result.current.page).toBe(1));
|
|
739
|
+
await (0, react_1.waitFor)(() => expect(Object.keys(result.current.tableData ?? {})[0]).toBe('a0'));
|
|
740
|
+
expect(result.current.loading).not.toBe(true); // Not flagged as loading while refreshing.
|
|
741
|
+
expect(result.current.tableData?.a0?.val).toBe(0);
|
|
742
|
+
// The refresh lands with a changed value on the first row.
|
|
743
|
+
await (0, react_1.act)(async () => release(makeSnap([makeDoc('a0', { val: 'updated' }), ...makeDocs(10, 'a').slice(1)])));
|
|
744
|
+
await (0, react_1.waitFor)(() => expect(result.current.tableData?.a0?.val).toBe('updated'));
|
|
745
|
+
expect(Object.keys(result.current.tableData ?? {}).length).toBe(10);
|
|
746
|
+
});
|
|
747
|
+
it('2.21g a slow load for a page already left (e.g. reset mid-load) does not overwrite the current page', async () => {
|
|
748
|
+
mockGetDocs.mockResolvedValueOnce(makeSnap(makeDocs(10, 'a')));
|
|
749
|
+
const { result } = (0, react_1.renderHook)(() => (0, hooks_1.useDataViewerPaginator)({ view: 'table', data: [q()], queryLimit: 10 }));
|
|
750
|
+
await (0, react_1.waitFor)(() => expect(result.current.loading).not.toBe(true));
|
|
751
|
+
// Page 2 is slow; a reset (filter/search change) takes the table back to page 1 first.
|
|
752
|
+
let releasePage2 = () => undefined;
|
|
753
|
+
mockGetDocs.mockImplementationOnce(() => new Promise((r) => { releasePage2 = r; }));
|
|
754
|
+
(0, react_1.act)(() => result.current.pageUp());
|
|
755
|
+
await (0, react_1.waitFor)(() => expect(result.current.page).toBe(2));
|
|
756
|
+
mockGetDocs.mockResolvedValueOnce(makeSnap(makeDocs(10, 'fresh')));
|
|
757
|
+
(0, react_1.act)(() => result.current.reset());
|
|
758
|
+
await (0, react_1.waitFor)(() => expect(result.current.page).toBe(1));
|
|
759
|
+
await (0, react_1.waitFor)(() => expect(Object.keys(result.current.tableData ?? {})[0]).toBe('fresh0'));
|
|
760
|
+
await (0, react_1.act)(async () => releasePage2(makeSnap(makeDocs(10, 'late'))));
|
|
761
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
762
|
+
expect(Object.keys(result.current.tableData ?? {})[0]).toBe('fresh0');
|
|
763
|
+
});
|
|
764
|
+
it('2.21i next and back are ignored while a page is still loading', async () => {
|
|
765
|
+
mockGetDocs.mockResolvedValueOnce(makeSnap(makeDocs(10, 'a')));
|
|
766
|
+
const { result } = (0, react_1.renderHook)(() => (0, hooks_1.useDataViewerPaginator)({ view: 'table', data: [q()], queryLimit: 10 }));
|
|
767
|
+
await (0, react_1.waitFor)(() => expect(result.current.loading).not.toBe(true));
|
|
768
|
+
let releasePage2 = () => undefined;
|
|
769
|
+
mockGetDocs.mockImplementationOnce(() => new Promise((r) => { releasePage2 = r; }));
|
|
770
|
+
(0, react_1.act)(() => result.current.pageUp());
|
|
771
|
+
await (0, react_1.waitFor)(() => expect(result.current.page).toBe(2));
|
|
772
|
+
expect(result.current.loading).toBe(true);
|
|
773
|
+
(0, react_1.act)(() => result.current.pageUp());
|
|
774
|
+
(0, react_1.act)(() => result.current.pageDown());
|
|
775
|
+
expect(result.current.page).toBe(2);
|
|
776
|
+
expect(mockGetDocs).toHaveBeenCalledTimes(2);
|
|
777
|
+
await (0, react_1.act)(async () => releasePage2(makeSnap(makeDocs(10, 'b'))));
|
|
778
|
+
await (0, react_1.waitFor)(() => expect(result.current.loading).not.toBe(true));
|
|
779
|
+
// Once loaded, next starts after the last row of page 2.
|
|
780
|
+
mockStartAfter.mockClear();
|
|
781
|
+
mockGetDocs.mockResolvedValueOnce(makeSnap(makeDocs(10, 'c')));
|
|
782
|
+
(0, react_1.act)(() => result.current.pageUp());
|
|
783
|
+
await (0, react_1.waitFor)(() => expect(result.current.page).toBe(3));
|
|
784
|
+
expect(mockStartAfter).toHaveBeenCalledWith('b9');
|
|
785
|
+
});
|
|
786
|
+
it('2.21h reset clears the page cache', async () => {
|
|
787
|
+
mockGetDocs
|
|
788
|
+
.mockResolvedValueOnce(makeSnap(makeDocs(10, 'a')))
|
|
789
|
+
.mockResolvedValueOnce(makeSnap(makeDocs(10, 'b')));
|
|
790
|
+
const { result } = (0, react_1.renderHook)(() => (0, hooks_1.useDataViewerPaginator)({ view: 'table', data: [q()], queryLimit: 10 }));
|
|
791
|
+
await (0, react_1.waitFor)(() => expect(result.current.loading).not.toBe(true));
|
|
792
|
+
(0, react_1.act)(() => result.current.pageUp());
|
|
793
|
+
await (0, react_1.waitFor)(() => expect(Object.keys(result.current.tableData ?? {})[0]).toBe('b0'));
|
|
794
|
+
// After a reset, page 1 must come from Firestore, not the old cache.
|
|
795
|
+
let release = () => undefined;
|
|
796
|
+
mockGetDocs.mockImplementationOnce(() => new Promise((r) => { release = r; }));
|
|
797
|
+
(0, react_1.act)(() => result.current.reset());
|
|
798
|
+
await (0, react_1.waitFor)(() => expect(result.current.page).toBe(1));
|
|
799
|
+
expect(Object.keys(result.current.tableData ?? {}).length).toBe(0);
|
|
800
|
+
await (0, react_1.act)(async () => release(makeSnap(makeDocs(10, 'fresh'))));
|
|
801
|
+
await (0, react_1.waitFor)(() => expect(Object.keys(result.current.tableData ?? {})[0]).toBe('fresh0'));
|
|
802
|
+
});
|
|
636
803
|
it('2.22 three queries: stitch across Q0→Q1→Q2 over two page loads', async () => {
|
|
637
804
|
const q0 = { path: ['c0'] };
|
|
638
805
|
const q1 = { path: ['c1'] };
|