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.
@@ -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 calls limitToLast + endBefore with startKey of current page', async () => {
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(mockLimitToLast).toHaveBeenCalledWith(10);
494
- // startKey of page 2 is 'b0'
495
- expect(mockEndBefore).toHaveBeenCalledWith('b0');
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 after boundary uses query B with endBefore(startKey)', async () => {
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: should use limitToLast + endBefore on queryB (startKey = 'c0')
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(6, 'b')))
601
- .mockResolvedValueOnce(makeSnap(makeDocs(4, 'a')));
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(result.current.loading).not.toBe(true));
605
- expect(mockLimitToLast).toHaveBeenCalled();
606
- expect(mockEndBefore).toHaveBeenCalledWith('c0');
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'] };