我的应用程序中有一个(写得很差)javascript组件处理无限滚动分页,我正在尝试重写它以使用IntersectionObserver
,如here所述,但是我我在测试方面遇到了问题。
有没有办法在QUnit测试中驱动观察者的行为,即用我的测试中描述的一些条目触发观察者回调?
我提出的一个可能的解决方案是在组件的原型中公开回调函数,并在我的测试中直接调用它,如下所示:
InfiniteScroll.prototype.observerCallback = function(entries) {
//handle the infinite scroll
}
InfiniteScroll.prototype.initObserver = function() {
var io = new IntersectionObserver(this.observerCallback);
io.observe(someElements);
}
//In my test
var component = new InfiniteScroll();
component.observerCallback(someEntries);
//Do some assertions about the state after the callback has been executed
我真的不喜欢这种方法,因为它暴露了组件内部使用IntersectionObserver
的事实,这是我认为客户端代码不应该看到的实现细节,所以有更好的方法吗?测试这个的方法?
对不使用jQuery的解决方案的热爱:)
答案 0 :(得分:6)
由于我们正在使用TypeScript和React(tsx)的配置,因此没有一个答案对我有用。这是最终有效的方法:
beforeEach(() => {
// IntersectionObserver isn't available in test environment
const mockIntersectionObserver = jest.fn();
mockIntersectionObserver.mockReturnValue({
observe: () => null,
unobserve: () => null,
disconnect: () => null
});
window.IntersectionObserver = mockIntersectionObserver;
});
答案 1 :(得分:5)
在您的 jest.setup.js 文件中,使用以下实现模拟IntersectionObserver:
global.IntersectionObserver = class IntersectionObserver {
constructor() {}
observe() {
return null;
}
unobserve() {
return null;
}
};
除了使用Jest Setup File,您还可以直接在测试中或在beforeAll,beforeEach块中进行此模拟。
答案 2 :(得分:1)
2019年的同样问题是我解决的方法:
import ....
describe('IntersectionObserverMokTest', () => {
...
const observeMock = {
observe: () => null,
disconnect: () => null // maybe not needed
};
beforeEach(async(() => {
(<any> window).IntersectionObserver = () => observeMock;
....
}));
if(' should run the Test without throwing an error for the IntersectionObserver', () => {
...
})
});
因此,我使用observe
(和disconnect
)方法创建了一个模拟对象,并覆盖了窗口对象上的IntersectionObserver
。根据您的用法,您可能必须覆盖其他功能(请参阅:https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#Browser_compatibility)
该代码受https://gist.github.com/ianmcnally/4b68c56900a20840b6ca840e2403771c的启发,但未使用jest
答案 3 :(得分:1)
我在基于vue-cli的安装程序中遇到了这个问题。我最终混合使用了上面看到的答案:
const mockIntersectionObserver = class {
constructor() {}
observe() {}
unobserve() {}
disconnect() {}
};
beforeEach(() => {
window.IntersectionObserver = mockIntersectionObserver;
});
答案 4 :(得分:1)
我像这样为 Jest+Typescript 测试了它
type CB = (arg1: IntersectionObserverEntry[]) => void;
class MockedObserver {
cb: CB;
options: IntersectionObserverInit;
elements: HTMLElement[];
constructor(cb: CB, options: IntersectionObserverInit) {
this.cb = cb;
this.options = options;
this.elements = [];
}
unobserve(elem: HTMLElement): void {
this.elements = this.elements.filter((en) => en !== elem);
}
observe(elem: HTMLElement): void {
this.elements = [...new Set(this.elements.concat(elem))];
}
disconnect(): void {
this.elements = [];
}
fire(arr: IntersectionObserverEntry[]): void {
this.cb(arr);
}
}
function traceMethodCalls(obj: object | Function, calls: any = {}) {
const handler: ProxyHandler<object | Function> = {
get(target, propKey, receiver) {
const targetValue = Reflect.get(target, propKey, receiver);
if (typeof targetValue === 'function') {
return function (...args: any[]) {
calls[propKey] = (calls[propKey] || []).concat(args);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return targetValue.apply(this, args);
};
} else {
return targetValue;
}
},
};
return new Proxy(obj, handler);
}
测试中
describe('useIntersectionObserver', () => {
let observer: any;
let mockedObserverCalls: { [k: string]: any } = {};
beforeEach(() => {
Object.defineProperty(window, 'IntersectionObserver', {
writable: true,
value: jest
.fn()
.mockImplementation(function TrackMock(
cb: CB,
options: IntersectionObserverInit
) {
observer = traceMethodCalls(
new MockedObserver(cb, options),
mockedObserverCalls
);
return observer;
}),
});
});
afterEach(() => {
observer = null;
mockedObserverCalls = {};
});
test('should do something', () => {
const mockedObserver = observer as unknown as MockedObserver;
const entry1 = {
target: new HTMLElement(),
intersectionRatio: 0.7,
};
// fire CB
mockedObserver.fire([entry1 as unknown as IntersectionObserverEntry]);
// possibly need to make test async/wait for see changes
// await waitForNextUpdate();
// await waitForDomChanges();
// await new Promise((resolve) => setTimeout(resolve, 0));
// Check calls to observer
expect(mockedObserverCalls.disconnect).toEqual([]);
expect(mockedObserverCalls.observe).toEqual([]);
});
});
答案 5 :(得分:0)
这是基于先前答案的另一种选择,您可以在beforeEach
方法内或在.test.js
文件的开头运行它。
您还可以将参数传递到setupIntersectionObserverMock
来模拟observe
和/或unobserve
方法,以使用jest.fn()
模拟功能对其进行监视。
/**
* Utility function that mocks the `IntersectionObserver` API. Necessary for components that rely
* on it, otherwise the tests will crash. Recommended to execute inside `beforeEach`.
* @param {object} intersectionObserverMock - Parameter that is sent to the `Object.defineProperty`
* overwrite method. `jest.fn()` mock functions can be passed here if the goal is to not only
* mock the intersection observer, but its methods.
*/
export function setupIntersectionObserverMock({
observe = () => null,
unobserve = () => null,
} = {}) {
class IntersectionObserver {
observe = observe;
unobserve = unobserve;
}
Object.defineProperty(
window,
'IntersectionObserver',
{ writable: true, configurable: true, value: IntersectionObserver }
);
Object.defineProperty(
global,
'IntersectionObserver',
{ writable: true, configurable: true, value: IntersectionObserver }
);
}
答案 6 :(得分:0)
与@Kevin Brotcke有类似的堆栈问题,除了使用其解决方案还会导致进一步的TypeScript错误:
缺少返回类型注释的函数表达式,隐式地具有“ any”返回类型。
以下是对我有用的调整后的解决方案:
beforeEach(() => {
// IntersectionObserver isn't available in test environment
const mockIntersectionObserver = jest.fn()
mockIntersectionObserver.mockReturnValue({
observe: jest.fn().mockReturnValue(null),
unobserve: jest.fn().mockReturnValue(null),
disconnect: jest.fn().mockReturnValue(null)
})
window.IntersectionObserver = mockIntersectionObserver
})