Skip to content

Commit 53d7748

Browse files
committed
test: update test suite for function component rewrite
Migrate all tests to @testing-library/react, add IntersectionObserver mock in setup/, and add new test files for buildRootMargin utility and no-scrollbar behaviour.
1 parent ee6fd3f commit 53d7748

10 files changed

Lines changed: 337 additions & 93 deletions

jest.config.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ module.exports = {
119119
// runner: "jest-runner",
120120

121121
// The paths to modules that run some code to configure or set up the testing environment before each test
122-
// setupFiles: [],
122+
setupFiles: ['<rootDir>/src/__tests__/setup/intersectionObserverMock.ts'],
123123

124124
// The path to a module that runs some code to configure or set up the testing framework before each test
125125
// setupTestFrameworkScriptFile: null,
@@ -143,9 +143,7 @@ module.exports = {
143143
// ],
144144

145145
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
146-
// testPathIgnorePatterns: [
147-
// "/node_modules/"
148-
// ],
146+
testPathIgnorePatterns: ['/node_modules/', '<rootDir>/src/__tests__/setup/'],
149147

150148
// The regexp pattern Jest uses to detect test files
151149
// testRegex: "",

src/__tests__/bottom.test.tsx

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
1-
import { render, cleanup } from '@testing-library/react';
1+
import { render, cleanup, act } from '@testing-library/react';
22
import InfiniteScroll from '../index';
3+
import { MockIntersectionObserver } from './setup/intersectionObserverMock';
34

45
describe('bottom detection triggers next', () => {
56
beforeEach(() => {
6-
jest.useFakeTimers();
7+
MockIntersectionObserver.instances = [];
78
});
89

9-
afterEach(() => {
10-
cleanup();
11-
jest.useRealTimers();
12-
});
10+
afterEach(cleanup);
1311

14-
it('calls next when scrolled to bottom (height container)', () => {
12+
it('calls next when sentinel intersects (height container)', () => {
1513
const next = jest.fn();
16-
const { container } = render(
14+
render(
1715
<InfiniteScroll
1816
dataLength={0}
1917
loader={'Loading...'}
@@ -26,27 +24,53 @@ describe('bottom detection triggers next', () => {
2624
</InfiniteScroll>
2725
);
2826

29-
const node = container.querySelector(
30-
'.infinite-scroll-component'
31-
) as HTMLElement;
32-
33-
Object.defineProperty(node, 'clientHeight', {
34-
configurable: true,
35-
get: () => 100,
36-
});
37-
Object.defineProperty(node, 'scrollHeight', {
38-
configurable: true,
39-
get: () => 200,
40-
});
41-
Object.defineProperty(node, 'scrollTop', {
42-
configurable: true,
43-
get: () => 100,
27+
act(() => {
28+
MockIntersectionObserver.instances[0].triggerIntersect();
4429
});
4530

46-
node.dispatchEvent(new Event('scroll'));
31+
expect(next).toHaveBeenCalled();
32+
});
4733

48-
jest.advanceTimersByTime(200);
34+
it('does not call next when hasMore is false', () => {
35+
const next = jest.fn();
36+
render(
37+
<InfiniteScroll
38+
dataLength={0}
39+
loader={'Loading...'}
40+
hasMore={false}
41+
next={next}
42+
height={100}
43+
>
44+
<div />
45+
</InfiniteScroll>
46+
);
4947

50-
expect(next).toHaveBeenCalled();
48+
// No observer is created when hasMore=false (no sentinel rendered)
49+
expect(MockIntersectionObserver.instances).toHaveLength(0);
50+
expect(next).not.toHaveBeenCalled();
51+
});
52+
53+
it('does not call next twice before dataLength changes', () => {
54+
const next = jest.fn();
55+
render(
56+
<InfiniteScroll
57+
dataLength={0}
58+
loader={'Loading...'}
59+
hasMore={true}
60+
next={next}
61+
height={100}
62+
>
63+
<div />
64+
</InfiniteScroll>
65+
);
66+
67+
const observer = MockIntersectionObserver.instances[0];
68+
69+
act(() => {
70+
observer.triggerIntersect();
71+
observer.triggerIntersect(); // second fire before dataLength changes
72+
});
73+
74+
expect(next).toHaveBeenCalledTimes(1);
5175
});
5276
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { buildRootMargin } from '../utils/buildRootMargin';
2+
3+
describe('buildRootMargin', () => {
4+
let warnSpy: jest.SpyInstance;
5+
6+
beforeEach(() => {
7+
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
8+
});
9+
10+
afterEach(() => {
11+
warnSpy.mockRestore();
12+
});
13+
14+
describe('normal (non-inverse) mode', () => {
15+
it('converts default number threshold (0.8) to bottom margin', () => {
16+
expect(buildRootMargin(0.8, false)).toBe('0px 0px 20% 0px');
17+
});
18+
19+
it('converts 0.5 number threshold', () => {
20+
expect(buildRootMargin(0.5, false)).toBe('0px 0px 50% 0px');
21+
});
22+
23+
it('converts 1.0 number threshold (trigger at 100%)', () => {
24+
expect(buildRootMargin(1.0, false)).toBe('0px 0px 0% 0px');
25+
});
26+
27+
it('converts percent string threshold', () => {
28+
expect(buildRootMargin('80%', false)).toBe('0px 0px 20% 0px');
29+
});
30+
31+
it('converts pixel string threshold', () => {
32+
expect(buildRootMargin('120px', false)).toBe('0px 0px 120px 0px');
33+
});
34+
35+
it('converts 0px threshold', () => {
36+
expect(buildRootMargin('0px', false)).toBe('0px 0px 0px 0px');
37+
});
38+
});
39+
40+
describe('inverse mode', () => {
41+
it('converts default number threshold (0.8) to top margin', () => {
42+
expect(buildRootMargin(0.8, true)).toBe('20% 0px 0px 0px');
43+
});
44+
45+
it('converts pixel string threshold to top margin', () => {
46+
expect(buildRootMargin('120px', true)).toBe('120px 0px 0px 0px');
47+
});
48+
49+
it('converts percent string threshold to top margin', () => {
50+
expect(buildRootMargin('50%', true)).toBe('50% 0px 0px 0px');
51+
});
52+
});
53+
});

src/__tests__/hasChildren.test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { render, cleanup } from '@testing-library/react';
22
import InfiniteScroll from '../index';
3+
import { MockIntersectionObserver } from './setup/intersectionObserverMock';
34

45
describe('hasChildren logic and loader visibility', () => {
6+
beforeEach(() => {
7+
MockIntersectionObserver.instances = [];
8+
});
9+
510
afterEach(cleanup);
611

712
it('shows loader when hasMore and no children', () => {
@@ -20,6 +25,8 @@ describe('hasChildren logic and loader visibility', () => {
2025
});
2126

2227
it('does not show loader when hasChildren=true and non-array child', () => {
28+
// With IO the loader is only shown synchronously when there are no children.
29+
// hasChildren=true suppresses the immediate loader render — IO must fire first.
2330
const { queryByText } = render(
2431
<InfiniteScroll
2532
dataLength={1}
@@ -32,6 +39,7 @@ describe('hasChildren logic and loader visibility', () => {
3239
<div>child</div>
3340
</InfiniteScroll>
3441
);
42+
// IO has not fired yet, so showLoader=false AND hasChildren=true suppresses the immediate render
3543
expect(queryByText('Loading...')).toBeNull();
3644
});
3745
});

src/__tests__/index.test.tsx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
import { render, cleanup } from '@testing-library/react';
1+
import { render, cleanup, act } from '@testing-library/react';
22
import InfiniteScroll from '../index';
3+
import { MockIntersectionObserver } from './setup/intersectionObserverMock';
34

45
describe('React Infinite Scroll Component', () => {
56
const originalConsoleError = console.error;
67

8+
beforeEach(() => {
9+
MockIntersectionObserver.instances = [];
10+
});
11+
712
afterEach(() => {
813
cleanup();
914
console.error = originalConsoleError;
@@ -82,6 +87,7 @@ describe('React Infinite Scroll Component', () => {
8287
expect(setTimeoutSpy).toHaveBeenCalled();
8388
expect(onScrollMock).toHaveBeenCalled();
8489
setTimeoutSpy.mockRestore();
90+
jest.useRealTimers();
8591
});
8692

8793
describe('When missing the dataLength prop', () => {
@@ -101,7 +107,7 @@ describe('React Infinite Scroll Component', () => {
101107

102108
describe('When user scrolls to the bottom', () => {
103109
it('does not show loader if hasMore is false', () => {
104-
const { container, queryByText } = render(
110+
const { queryByText } = render(
105111
<InfiniteScroll
106112
dataLength={4}
107113
loader={'Loading...'}
@@ -112,17 +118,12 @@ describe('React Infinite Scroll Component', () => {
112118
<div />
113119
</InfiniteScroll>
114120
);
115-
116-
const scrollEvent = new Event('scroll');
117-
const node = container.querySelector(
118-
'.infinite-scroll-component'
119-
) as HTMLElement;
120-
node.dispatchEvent(scrollEvent);
121+
// No IO observer created, loader never shown
121122
expect(queryByText('Loading...')).toBeFalsy();
122123
});
123124

124-
it('shows loader if hasMore is true', () => {
125-
const { container, getByText } = render(
125+
it('shows loader if hasMore is true after IO fires', () => {
126+
const { getByText } = render(
126127
<InfiniteScroll
127128
dataLength={4}
128129
loader={'Loading...'}
@@ -135,11 +136,10 @@ describe('React Infinite Scroll Component', () => {
135136
</InfiniteScroll>
136137
);
137138

138-
const scrollEvent = new Event('scroll');
139-
const node = container.querySelector(
140-
'.infinite-scroll-component'
141-
) as HTMLElement;
142-
node.dispatchEvent(scrollEvent);
139+
act(() => {
140+
MockIntersectionObserver.instances[0].triggerIntersect();
141+
});
142+
143143
expect(getByText('Loading...')).toBeTruthy();
144144
});
145145
});

src/__tests__/inverse.test.tsx

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
import { render, cleanup } from '@testing-library/react';
1+
import { render, cleanup, act } from '@testing-library/react';
22
import InfiniteScroll from '../index';
3+
import { MockIntersectionObserver } from './setup/intersectionObserverMock';
34

45
describe('inverse mode triggers next near top', () => {
5-
beforeEach(() => jest.useFakeTimers());
6-
afterEach(() => {
7-
cleanup();
8-
jest.useRealTimers();
6+
beforeEach(() => {
7+
MockIntersectionObserver.instances = [];
98
});
109

11-
it('calls next when at top (inverse)', () => {
10+
afterEach(cleanup);
11+
12+
it('calls next when sentinel intersects in inverse mode', () => {
1213
const next = jest.fn();
13-
const { container } = render(
14+
render(
1415
<InfiniteScroll
1516
dataLength={10}
1617
loader={'Loading...'}
@@ -24,27 +25,30 @@ describe('inverse mode triggers next near top', () => {
2425
</InfiniteScroll>
2526
);
2627

27-
const node = container.querySelector(
28-
'.infinite-scroll-component'
29-
) as HTMLElement;
30-
31-
Object.defineProperty(node, 'clientHeight', {
32-
configurable: true,
33-
get: () => 100,
34-
});
35-
Object.defineProperty(node, 'scrollHeight', {
36-
configurable: true,
37-
get: () => 1000,
38-
});
39-
Object.defineProperty(node, 'scrollTop', {
40-
configurable: true,
41-
get: () => 0,
28+
act(() => {
29+
MockIntersectionObserver.instances[0].triggerIntersect();
4230
});
4331

44-
node.dispatchEvent(new Event('scroll'));
32+
expect(next).toHaveBeenCalled();
33+
});
4534

46-
jest.advanceTimersByTime(200);
35+
it('applies top rootMargin in inverse mode', () => {
36+
const next = jest.fn();
37+
render(
38+
<InfiniteScroll
39+
dataLength={5}
40+
loader={'Loading...'}
41+
hasMore={true}
42+
next={next}
43+
inverse
44+
scrollThreshold={0.8}
45+
>
46+
<div />
47+
</InfiniteScroll>
48+
);
4749

48-
expect(next).toHaveBeenCalled();
50+
const { options } = MockIntersectionObserver.instances[0];
51+
// inverse mode: margin applies to top, so rootMargin starts with a non-zero value
52+
expect(options.rootMargin).toBe('20% 0px 0px 0px');
4953
});
5054
});

0 commit comments

Comments
 (0)