Skip to content

Commit 9a9bf25

Browse files
ygongdevYicheng Gongelwayman02
authored
Introduce addon-test-support by mocking IntersectionObserver (#306)
* mock Intersection Observer * Update addon-test-support/did-intersect-mock.js Co-authored-by: Jordan Hawker <hawker.jordan@gmail.com> * added documentation * Update tests/dummy/app/templates/docs/did-intersect.md Co-authored-by: Yicheng Gong <yigong@yigong-mn4.linkedin.biz> Co-authored-by: Jordan Hawker <hawker.jordan@gmail.com>
1 parent 76219c4 commit 9a9bf25

File tree

3 files changed

+227
-0
lines changed

3 files changed

+227
-0
lines changed
+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { A as emberArray } from '@ember/array';
2+
import { settled, find } from '@ember/test-helpers';
3+
4+
/**
5+
* This replaces the browser's IntersectionObserver with a mocked one that is synchronous
6+
* as opposed to asynchronous, and controllable by us.
7+
*
8+
* forceElement return `settled()` for convenience, so that
9+
* any downstream side-effects can be awaited. This is also done for consistency with
10+
* the rest of our test helpers.
11+
*
12+
* Usage:
13+
*
14+
* const didIntersectMock = mockDidIntersect(sinon);
15+
* ...render logic...
16+
* await didIntersectMock.enter();
17+
*/
18+
class MockIntersectionObserver {
19+
static instances = [];
20+
21+
constructor(callback) {
22+
this.callback = callback;
23+
this._watchedElements = emberArray();
24+
MockIntersectionObserver.instances.push(this);
25+
}
26+
27+
observe(element) {
28+
this._watchedElements.addObject(element);
29+
}
30+
31+
unobserve(element) {
32+
this._watchedElements.removeObject(element);
33+
}
34+
35+
disconnect() {
36+
this._watchedElements = [];
37+
}
38+
39+
/**
40+
* Force a single element to enter the viewport
41+
* @param {String} el - a DOM selector string
42+
*/
43+
static enter(el) {
44+
return MockIntersectionObserver.forceElement(find(el), {
45+
isIntersecting: true,
46+
intersectionRatio: 1,
47+
});
48+
}
49+
50+
/**
51+
* Force a single element to exit the viewport
52+
* @param {DomElement} el - a DOM Selector string
53+
*/
54+
static exit(el) {
55+
return MockIntersectionObserver.forceElement(find(el), {
56+
isIntersecting: false,
57+
intersectionRatio: 0,
58+
});
59+
}
60+
61+
/**
62+
* Force an IntersectionObserverEntry targeted at a specific DOM node.
63+
* Useful when only triggering viewport state on certain elements.
64+
*
65+
* @param {DomElement} el
66+
* @param {object} [state] Additional state to be passed as the IntersectionObserverEntry
67+
*/
68+
static forceElement(el, state) {
69+
MockIntersectionObserver.instances.forEach((instance) => {
70+
if (instance._watchedElements.includes(el)) {
71+
instance.callback([
72+
{
73+
target: el,
74+
...state,
75+
},
76+
]);
77+
}
78+
});
79+
return settled();
80+
}
81+
}
82+
83+
/**
84+
* Replaces the global IntersectionObserver with the MockIntersectionObserver class
85+
*
86+
* @param {Sinon} sinon Pass sinon as the argument to ensure the mock is undone at the end of the test
87+
*/
88+
export default function mockDidIntersect(sinon) {
89+
sinon.replace(MockIntersectionObserver, 'instances', []);
90+
sinon.replace(window, 'IntersectionObserver', MockIntersectionObserver);
91+
return MockIntersectionObserver;
92+
}

tests/dummy/app/templates/docs/did-intersect.md

+36
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,42 @@ You can also set a maximum limit on the number of times the callbacks should tri
4343

4444
The options supported are documented in the MDN site under [Intersection observer options](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver#Intersection_observer_options).
4545

46+
## Testing
47+
Since the underlying IntersectionObserver behavior is non-deterministic, we provide a `did-intersect-mock` test helper to help you test `did-intersect` deterministically.
48+
49+
`did-intersect-mock` creates a mock provides 2 APIs
50+
51+
1. `enter(elementString)` triggers the `onEnter` callback, given an DOM element string
52+
2. `exit(elementString)` triggers the `onExit` callback, given an DOM element string
53+
54+
```javascript
55+
import mockDidIntersect from 'ember-scroll-modifiers/test-support/did-intersect-mock';
56+
57+
...
58+
const didIntersectMock = mockDidIntersect(sinon);
59+
60+
await render(hbs`
61+
<div
62+
data-test-did-intersect
63+
{{did-intersect onEnter=this.onEnteringIntersection onExit=this.onExitingIntersection}}
64+
>
65+
</div>
66+
`)
67+
...
68+
await didIntersectMock.enter('[data-test-did-intersect]');
69+
...
70+
await didIntersectMock.exit('[data-test-did-intersect]');
71+
```
72+
73+
Even though, this effectively allows you to trigger the `did-intersect` on demand without requiring real app interactions, you should still do it as best practice.
74+
75+
```javascript
76+
...
77+
await triggerEvent('scroll');
78+
await didIntersectMock.enter('[data-test-did-intersect]');
79+
...
80+
```
81+
4682
## Browser Support
4783

4884
This feature is [supported](https://caniuse.com/#search=intersectionobserver) in the latest versions of every browser except IE 11.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { module, test } from 'qunit';
2+
import { setupRenderingTest } from 'ember-qunit';
3+
import { render } from '@ember/test-helpers';
4+
import hbs from 'htmlbars-inline-precompile';
5+
import sinon from 'sinon';
6+
import mockDidIntersect from 'ember-scroll-modifiers/test-support/did-intersect-mock';
7+
8+
module(
9+
'Integration | Modifier | addon-test-support/did-intersect-mock',
10+
function (hooks) {
11+
setupRenderingTest(hooks);
12+
13+
hooks.beforeEach(function () {
14+
this.didIntersectMock = mockDidIntersect(sinon);
15+
this.enterStub = sinon.stub();
16+
this.exitStub = sinon.stub();
17+
this.maxEnter = 1;
18+
this.maxExit = 1;
19+
});
20+
21+
test('Did intersect mock triggers onEnter correctly', async function (assert) {
22+
assert.expect(2);
23+
24+
await render(
25+
hbs`<div data-test-did-intersect {{did-intersect onEnter=this.enterStub onExit=this.exitStub}}></div>`
26+
);
27+
await this.didIntersectMock.enter('[data-test-did-intersect]');
28+
29+
assert.ok(this.enterStub.calledOnce);
30+
assert.notOk(this.exitStub.calledOnce);
31+
});
32+
33+
test('Did intersect mock triggers onExit correctly', async function (assert) {
34+
assert.expect(2);
35+
36+
await render(
37+
hbs`<div data-test-did-intersect {{did-intersect onEnter=this.enterStub onExit=this.exitStub}}></div>`
38+
);
39+
await this.didIntersectMock.exit('[data-test-did-intersect]');
40+
41+
assert.notOk(this.enterStub.calledOnce);
42+
assert.ok(this.exitStub.calledOnce);
43+
});
44+
45+
test('Did intersect mock triggers onExit never exceeds maxEnter if maxEnter is provided', async function (assert) {
46+
assert.expect(1);
47+
48+
await render(
49+
hbs`<div data-test-did-intersect {{did-intersect onEnter=this.enterStub onExit=this.exitStub maxEnter=this.maxEnter}}></div>`
50+
);
51+
52+
for (let i = 0; i < this.maxEnter + 1; i++) {
53+
await this.didIntersectMock.enter('[data-test-did-intersect]');
54+
}
55+
56+
assert.equal(this.enterStub.callCount, this.maxEnter);
57+
});
58+
59+
test('Did intersect mock triggers onExit never exceeds maxExit if maxExit is provided', async function (assert) {
60+
assert.expect(1);
61+
62+
await render(
63+
hbs`<div data-test-did-intersect {{did-intersect onEnter=this.enterStub onExit=this.exitStub maxExit=this.maxExit}}></div>`
64+
);
65+
66+
for (let i = 0; i < this.maxExit + 1; i++) {
67+
await this.didIntersectMock.exit('[data-test-did-intersect]');
68+
}
69+
70+
assert.equal(this.exitStub.callCount, this.maxExit);
71+
});
72+
73+
test('Did intersect mock fire without limit if maxEnter and maxExit is not provided', async function (assert) {
74+
assert.expect(2);
75+
76+
await render(
77+
hbs`<div data-test-did-intersect {{did-intersect onEnter=this.enterStub onExit=this.exitStub}}></div>`
78+
);
79+
80+
const numOfFiredCallback = this.maxEnter + this.maxExit;
81+
82+
for (let i = 0; i < numOfFiredCallback; i++) {
83+
this.didIntersectMock.enter('[data-test-did-intersect]');
84+
this.didIntersectMock.exit('[data-test-did-intersect]');
85+
}
86+
87+
assert.equal(
88+
this.enterStub.callCount,
89+
numOfFiredCallback,
90+
'Enter callback has fired more than maxEnter times'
91+
);
92+
assert.equal(
93+
this.exitStub.callCount,
94+
numOfFiredCallback,
95+
'Exit callback has fired more than maxExit times'
96+
);
97+
});
98+
}
99+
);

0 commit comments

Comments
 (0)