Files
privacy.sexy/tests/unit/presentation/components/Shared/Resize/UseAnimationFrameLimiter.spec.ts
undergroundwires 292362135d Centralize and optimize ResizeObserver usage
This commit addresses failures in end-to-end tests that occurred due to
`ResizeObserver` loop limit exceptions.

These errors were triggered by Vue dependency upgrades in the commit
aae5434451.
The errors had the following message:
> `ResizeObserver loop completed with undelivered notifications`

This error happens when there are too many observations and the observer
is not able to deliver all observations within a single animation frame.
See: WICG/resize-observer#38

his commit resolves the issue by controlling how many observations are
delivered per animation frame and limiting it to only one.

It improves performance by reducing layout trashing, improving frame
rates, and managing resources more effectively.

Changes:

- Introduce an animation frame control to manage observations more
  efficiently.
- Centralized `ResizeObserver` management within the `UseResizeObserver`
  hook to improve consistency and reuse across the application.
2024-05-20 10:36:49 +02:00

134 lines
5.4 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
useAnimationFrameLimiter, type AnimationFrameId, type AnimationFrameRequestCallback,
type CancelAnimationFrameFunction, type RegisterTeardownCallbackFunction,
type RequestAnimationFrameFunction,
} from '@/presentation/components/Shared/Hooks/Resize/UseAnimationFrameLimiter';
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
describe('useAnimationFrameLimiter', () => {
describe('resetNextFrame', () => {
it('schedules the callback in the next animation frame', () => {
// arrange
const expectedCallback = () => {};
let scheduledCallback: AnimationFrameRequestCallback | undefined;
const requestAnimationFrame: RequestAnimationFrameFunction = (callback) => {
scheduledCallback = callback;
return 0;
};
const context = new TestContext()
.withRequestAnimationFrameFunction(requestAnimationFrame);
// act
const { resetNextFrame } = context.useAnimationFrameLimiter();
resetNextFrame(expectedCallback);
// assert
expect(scheduledCallback).to.equal(expectedCallback);
});
it('cancels the existing animation frame before scheduling a new one', () => {
// arrange
const expectedCancelledAnimationFrameId: AnimationFrameId = 5;
let actualCancelledAnimationFrameId: AnimationFrameId | undefined;
const requestAnimationFrame: RequestAnimationFrameFunction = () => {
return expectedCancelledAnimationFrameId;
};
const cancelAnimationFrame: CancelAnimationFrameFunction = (animationFrameId) => {
actualCancelledAnimationFrameId = animationFrameId;
};
const context = new TestContext()
.withRequestAnimationFrameFunction(requestAnimationFrame)
.withCancelAnimationFrame(cancelAnimationFrame);
// act
const { resetNextFrame } = context.useAnimationFrameLimiter();
resetNextFrame(() => {}); // Nothing to cancel in first call
resetNextFrame(() => {});
// assert
expect(actualCancelledAnimationFrameId).to.equal(expectedCancelledAnimationFrameId);
});
});
describe('cancelNextFrame', () => {
it('cancels the scheduled animation frame if one exists', () => {
// arrange
const expectedCancelledAnimationFrameId: AnimationFrameId = 5;
let actualCancelledAnimationFrameId: AnimationFrameId | undefined;
const requestAnimationFrame: RequestAnimationFrameFunction = () => {
return expectedCancelledAnimationFrameId;
};
const cancelAnimationFrame: CancelAnimationFrameFunction = (animationFrameId) => {
actualCancelledAnimationFrameId = animationFrameId;
};
const context = new TestContext()
.withRequestAnimationFrameFunction(requestAnimationFrame)
.withCancelAnimationFrame(cancelAnimationFrame);
// act
const { resetNextFrame, cancelNextFrame } = context.useAnimationFrameLimiter();
resetNextFrame(() => {}); // Schedule the initial one
cancelNextFrame();
// assert
expect(actualCancelledAnimationFrameId).to.equal(expectedCancelledAnimationFrameId);
});
});
it('automatically cancels the animation frame on cleanup', () => {
// arrange
let actualCancelledAnimationFrameId: AnimationFrameId | undefined;
let actualCleanupCallback: (() => void) | undefined;
const onTeardownCallback: RegisterTeardownCallbackFunction = (cleanupCallback) => {
actualCleanupCallback = cleanupCallback;
};
const expectedCancelledAnimationFrameId: AnimationFrameId = 5;
const requestAnimationFrame: RequestAnimationFrameFunction = () => {
return expectedCancelledAnimationFrameId;
};
const cancelAnimationFrame: CancelAnimationFrameFunction = (animationFrameId) => {
actualCancelledAnimationFrameId = animationFrameId;
};
const testContext = new TestContext()
.withOnTeardownCallback(onTeardownCallback)
.withRequestAnimationFrameFunction(requestAnimationFrame)
.withCancelAnimationFrame(cancelAnimationFrame);
// act
const { resetNextFrame } = testContext.useAnimationFrameLimiter();
resetNextFrame(() => {}); // Schedule the initial one
// assert
expectExists(actualCleanupCallback);
actualCleanupCallback();
expect(actualCancelledAnimationFrameId).to.equal(expectedCancelledAnimationFrameId);
});
});
class TestContext {
private cancelAnimationFrame: CancelAnimationFrameFunction = () => {};
private requestAnimationFrameFunction: RequestAnimationFrameFunction = () => Math.random();
private onTeardownCallback: RegisterTeardownCallbackFunction = () => {};
public withRequestAnimationFrameFunction(
requestAnimationFrameFunction: RequestAnimationFrameFunction,
): this {
this.requestAnimationFrameFunction = requestAnimationFrameFunction;
return this;
}
public withCancelAnimationFrame(
cancelAnimationFrame: CancelAnimationFrameFunction,
): this {
this.cancelAnimationFrame = cancelAnimationFrame;
return this;
}
public withOnTeardownCallback(
registerCleanupCallback: RegisterTeardownCallbackFunction,
): this {
this.onTeardownCallback = registerCleanupCallback;
return this;
}
public useAnimationFrameLimiter(): ReturnType<typeof useAnimationFrameLimiter> {
return useAnimationFrameLimiter(
this.cancelAnimationFrame,
this.requestAnimationFrameFunction,
this.onTeardownCallback,
);
}
}