From 42324b257868fbc5bbfbb9506f1f2f2244e0d04a Mon Sep 17 00:00:00 2001 From: krutoo Date: Thu, 20 Aug 2026 00:02:10 +0500 Subject: [PATCH] feat(testing): Observer mocks addCallback method --- .../testing/intersection-observer-mock.mdx | 62 +++++++++++++++++++ docs/docs/testing/resize-observer-mock.mdx | 61 ++++++++++++++++++ src/testing/intersection-observer-mock.ts | 20 ++++-- src/testing/resize-observer-mock.ts | 20 ++++-- 4 files changed, 155 insertions(+), 8 deletions(-) diff --git a/docs/docs/testing/intersection-observer-mock.mdx b/docs/docs/testing/intersection-observer-mock.mdx index a713991..ea65fc7 100644 --- a/docs/docs/testing/intersection-observer-mock.mdx +++ b/docs/docs/testing/intersection-observer-mock.mdx @@ -30,3 +30,65 @@ observer.simulateIntersection([ }, ]); ``` + +### Typical unit-test + +Here is an example of unit test with `IntersectionObserverMock`. For implementation `useIntersection` is used: + +```tsx +import { IntersectionObserverContext, useIntersection } from '@krutoo/utils/react'; +import { IntersectionObserverMock } from '@krutoo/utils/testing'; +import { act, render } from '@testing-library/react'; +import { describe, expect, mock, test } from '#test-utils'; + +function MyComponent({ onChange }) { + const ref = useRef(null); + + useIntersection(ref, () => { + onChange?.(); + }); + + return ( +
+ Hello from my component +
+ ); +} + +describe('MyComponent', () => { + test('should call onChange on intersection change', () => { + const observer = new ResizeObserverMock(() => {}); + + const context = { + getObserver(callback: IntersectionObserverCallback) { + return observer.addCallback(callback); + }, + }; + + const spy = mock.fn(); + + const { getByTestId } = render( + + + , + ); + + // no calls after render + expect(spy.mock.callCount()).toBe(0); + + // one call after intersection simulated + act(() => { + observer.simulateIntersection([ + { + target: getByTestId('my-component'), + isIntersecting: true, + }, + ]); + }); + expect(spy.mock.callCount()).toBe(1); + }); +}); +``` + +As you can see, mock includes chainable `addCallback` method. +You can use it to provide one observer for multiple components during test. diff --git a/docs/docs/testing/resize-observer-mock.mdx b/docs/docs/testing/resize-observer-mock.mdx index 0e5e9ca..7d1d900 100644 --- a/docs/docs/testing/resize-observer-mock.mdx +++ b/docs/docs/testing/resize-observer-mock.mdx @@ -29,3 +29,64 @@ observer.simulateResize([ }, ]); ``` + +### Typical unit-test + +Here is an example of unit test with `ResizeObserverMock`. For implementation `useResize` is used: + +```tsx +import { ResizeObserverContext, useResize } from '@krutoo/utils/react'; +import { ResizeObserverMock } from '@krutoo/utils/testing'; +import { act, render } from '@testing-library/react'; +import { describe, expect, mock, test } from '#test-utils'; + +function MyComponent({ onResize }) { + const ref = useRef(null); + + useResize(ref, () => { + onResize?.(); + }); + + return ( +
+ Hello from my component +
+ ); +} + +describe('MyComponent', () => { + test('should call onResize on resize', () => { + const observer = new ResizeObserverMock(() => {}); + + const context = { + getObserver(callback: ResizeObserverCallback) { + return observer.addCallback(callback); + }, + }; + + const spy = mock.fn(); + + const { getByTestId } = render( + + + , + ); + + // no calls after render + expect(spy.mock.callCount()).toBe(0); + + // one call after resize simulated + act(() => { + observer.simulateResize([ + { + target: getByTestId('my-component'), + }, + ]); + }); + expect(spy.mock.callCount()).toBe(1); + }); +}); +``` + +As you can see, mock includes chainable `addCallback` method. +You can use it to provide one observer for multiple components during test. diff --git a/src/testing/intersection-observer-mock.ts b/src/testing/intersection-observer-mock.ts index 25a075e..3061a6c 100644 --- a/src/testing/intersection-observer-mock.ts +++ b/src/testing/intersection-observer-mock.ts @@ -17,8 +17,8 @@ export class IntersectionObserverMock implements IntersectionObserver { /** @inheritdoc */ readonly thresholds: ReadonlyArray; - /** Callback. */ - protected readonly callback: IntersectionObserverCallback; + /** Callbacks. */ + protected readonly callbacks: Set; /** Observed elements. */ protected readonly elements: Set; @@ -28,7 +28,7 @@ export class IntersectionObserverMock implements IntersectionObserver { this.rootMargin = init?.rootMargin ?? '0px 0px 0px 0px'; this.thresholds = Array.isArray(init?.threshold) ? init.threshold : [init?.threshold ?? 0]; - this.callback = callback; + this.callbacks = new Set([callback]); this.elements = new Set(); } @@ -82,6 +82,18 @@ export class IntersectionObserverMock implements IntersectionObserver { }); } - this.callback(readyEntries, this); + this.callbacks.forEach(fn => fn(readyEntries, this)); + } + + /** + * Adds callback. + * Useful to define single observer during tests and add callbacks to it. + * @param callback Callback. + * @returns This instance. + */ + addCallback(callback: IntersectionObserverCallback): this { + this.callbacks.add(callback); + + return this; } } diff --git a/src/testing/resize-observer-mock.ts b/src/testing/resize-observer-mock.ts index e9c079b..1bba8fe 100644 --- a/src/testing/resize-observer-mock.ts +++ b/src/testing/resize-observer-mock.ts @@ -8,14 +8,14 @@ export type PartialResizeObserverEntry = Pick & * Useful for unit testing. */ export class ResizeObserverMock implements ResizeObserver { - /** Callback. */ - protected readonly callback: ResizeObserverCallback; + /** Callbacks. */ + protected readonly callbacks: Set; /** Observed elements. */ protected readonly elements: Set; constructor(callback: ResizeObserverCallback) { - this.callback = callback; + this.callbacks = new Set([callback]); this.elements = new Set(); } @@ -60,6 +60,18 @@ export class ResizeObserverMock implements ResizeObserver { }); } - this.callback(readyEntries, this); + this.callbacks.forEach(fn => fn(readyEntries, this)); + } + + /** + * Adds callback. + * Useful to define single observer during tests and add callbacks to it. + * @param callback Callback. + * @returns This instance. + */ + addCallback(callback: ResizeObserverCallback): this { + this.callbacks.add(callback); + + return this; } }