mirror of
https://github.com/zoriya/v10.git
synced 2026-08-14 01:49:30 +00:00
feat(store): initial release (#279)
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { Disposer } from './disposer';
|
||||
|
||||
describe('disposer', () => {
|
||||
describe('constructor', () => {
|
||||
it('creates a disposer with size 0', () => {
|
||||
const disposer = new Disposer();
|
||||
expect(disposer.size).toBe(0);
|
||||
});
|
||||
|
||||
it('tracks size as cleanups are added', () => {
|
||||
const disposer = new Disposer();
|
||||
disposer.add(() => {});
|
||||
expect(disposer.size).toBe(1);
|
||||
disposer.add(() => {});
|
||||
expect(disposer.size).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('add', () => {
|
||||
it('adds cleanup functions', () => {
|
||||
const disposer = new Disposer();
|
||||
const cleanup1 = vi.fn();
|
||||
const cleanup2 = vi.fn();
|
||||
|
||||
disposer.add(cleanup1);
|
||||
disposer.add(cleanup2);
|
||||
|
||||
expect(disposer.size).toBe(2);
|
||||
});
|
||||
|
||||
it('does not call cleanups when adding', () => {
|
||||
const disposer = new Disposer();
|
||||
const cleanup = vi.fn();
|
||||
|
||||
disposer.add(cleanup);
|
||||
|
||||
expect(cleanup).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('dispose', () => {
|
||||
it('calls all cleanup functions', () => {
|
||||
const disposer = new Disposer();
|
||||
const cleanup1 = vi.fn();
|
||||
const cleanup2 = vi.fn();
|
||||
const cleanup3 = vi.fn();
|
||||
|
||||
disposer.add(cleanup1);
|
||||
disposer.add(cleanup2);
|
||||
disposer.add(cleanup3);
|
||||
|
||||
disposer.dispose();
|
||||
|
||||
expect(cleanup1).toHaveBeenCalledOnce();
|
||||
expect(cleanup2).toHaveBeenCalledOnce();
|
||||
expect(cleanup3).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('clears the disposer after dispose', () => {
|
||||
const disposer = new Disposer();
|
||||
disposer.add(() => {});
|
||||
disposer.add(() => {});
|
||||
|
||||
expect(disposer.size).toBe(2);
|
||||
disposer.dispose();
|
||||
expect(disposer.size).toBe(0);
|
||||
});
|
||||
|
||||
it('can be called multiple times safely', () => {
|
||||
const disposer = new Disposer();
|
||||
const cleanup = vi.fn();
|
||||
|
||||
disposer.add(cleanup);
|
||||
disposer.dispose();
|
||||
disposer.dispose();
|
||||
|
||||
expect(cleanup).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('allows adding new cleanups after dispose', () => {
|
||||
const disposer = new Disposer();
|
||||
const cleanup1 = vi.fn();
|
||||
const cleanup2 = vi.fn();
|
||||
|
||||
disposer.add(cleanup1);
|
||||
disposer.dispose();
|
||||
|
||||
disposer.add(cleanup2);
|
||||
disposer.dispose();
|
||||
|
||||
expect(cleanup1).toHaveBeenCalledOnce();
|
||||
expect(cleanup2).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe('disposeAsync', () => {
|
||||
it('calls all cleanup functions', async () => {
|
||||
const disposer = new Disposer();
|
||||
const cleanup1 = vi.fn();
|
||||
const cleanup2 = vi.fn();
|
||||
|
||||
disposer.add(cleanup1);
|
||||
disposer.add(cleanup2);
|
||||
|
||||
await disposer.disposeAsync();
|
||||
|
||||
expect(cleanup1).toHaveBeenCalledOnce();
|
||||
expect(cleanup2).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('awaits async cleanup functions', async () => {
|
||||
const disposer = new Disposer();
|
||||
const order: string[] = [];
|
||||
|
||||
disposer.add(async () => {
|
||||
await Promise.resolve();
|
||||
order.push('async1');
|
||||
});
|
||||
|
||||
disposer.add(() => {
|
||||
order.push('sync');
|
||||
});
|
||||
|
||||
disposer.add(async () => {
|
||||
await Promise.resolve();
|
||||
order.push('async2');
|
||||
});
|
||||
|
||||
await disposer.disposeAsync();
|
||||
|
||||
expect(order).toContain('async1');
|
||||
expect(order).toContain('sync');
|
||||
expect(order).toContain('async2');
|
||||
});
|
||||
|
||||
it('clears the disposer after disposeAsync', async () => {
|
||||
const disposer = new Disposer();
|
||||
disposer.add(async () => {});
|
||||
|
||||
expect(disposer.size).toBe(1);
|
||||
await disposer.disposeAsync();
|
||||
expect(disposer.size).toBe(0);
|
||||
});
|
||||
|
||||
it('handles mixed sync and async cleanups', async () => {
|
||||
const disposer = new Disposer();
|
||||
const results: number[] = [];
|
||||
|
||||
disposer.add(() => {
|
||||
results.push(1);
|
||||
});
|
||||
disposer.add(async () => {
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
results.push(2);
|
||||
});
|
||||
disposer.add(() => {
|
||||
results.push(3);
|
||||
});
|
||||
|
||||
await disposer.disposeAsync();
|
||||
|
||||
expect(results).toHaveLength(3);
|
||||
expect(results).toContain(1);
|
||||
expect(results).toContain(2);
|
||||
expect(results).toContain(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration', () => {
|
||||
it('works with real-world cleanup patterns', () => {
|
||||
const disposer = new Disposer();
|
||||
|
||||
// Simulating event listener cleanup
|
||||
const listeners = new Map<string, () => void>();
|
||||
const addEventListener = (type: string, handler: () => void) => {
|
||||
listeners.set(type, handler);
|
||||
return () => {
|
||||
listeners.delete(type);
|
||||
};
|
||||
};
|
||||
|
||||
disposer.add(addEventListener('click', () => {}));
|
||||
disposer.add(addEventListener('keydown', () => {}));
|
||||
|
||||
expect(listeners.size).toBe(2);
|
||||
disposer.dispose();
|
||||
expect(listeners.size).toBe(0);
|
||||
});
|
||||
|
||||
it('works with timer cleanup patterns', () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const disposer = new Disposer();
|
||||
let timerFired = false;
|
||||
|
||||
const id = setTimeout(() => {
|
||||
timerFired = true;
|
||||
}, 1000);
|
||||
|
||||
disposer.add(() => clearTimeout(id));
|
||||
|
||||
disposer.dispose();
|
||||
vi.advanceTimersByTime(2000);
|
||||
|
||||
expect(timerFired).toBe(false);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* A cleanup function that may be sync or async.
|
||||
*/
|
||||
export type CleanupFn = () => void | Promise<void>;
|
||||
|
||||
/**
|
||||
* A collector for cleanup functions.
|
||||
*
|
||||
* Allows registering multiple cleanup functions and disposing them all at once.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposer = new Disposer();
|
||||
*
|
||||
* disposer.add(listen(video, 'play', handlePlay));
|
||||
* disposer.add(listen(video, 'pause', handlePause));
|
||||
* disposer.add(animationFrame(render));
|
||||
*
|
||||
* // Later, clean up everything at once
|
||||
* disposer.dispose();
|
||||
* // or for async cleanups:
|
||||
* await disposer.disposeAsync();
|
||||
* ```
|
||||
*/
|
||||
export class Disposer {
|
||||
#cleanups = new Set<CleanupFn>();
|
||||
|
||||
/**
|
||||
* Number of registered cleanup functions.
|
||||
*/
|
||||
get size(): number {
|
||||
return this.#cleanups.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a cleanup function to the collection.
|
||||
*/
|
||||
add(cleanup: CleanupFn): void {
|
||||
this.#cleanups.add(cleanup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all cleanup functions synchronously.
|
||||
*
|
||||
* Note: If any cleanup functions return promises, they will not be awaited.
|
||||
* Use `disposeAsync()` if you have async cleanup functions.
|
||||
*/
|
||||
dispose(): void {
|
||||
for (const cleanup of this.#cleanups) {
|
||||
cleanup();
|
||||
}
|
||||
this.#cleanups.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all cleanup functions, awaiting any promises.
|
||||
*/
|
||||
async disposeAsync(): Promise<void> {
|
||||
await Promise.all([...this.#cleanups].map(cleanup => cleanup()));
|
||||
this.#cleanups.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { animationFrame } from './animation-frame';
|
||||
|
||||
describe('animationFrame', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('calls the callback on next animation frame', async () => {
|
||||
const callback = vi.fn();
|
||||
|
||||
animationFrame(callback);
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('passes timestamp to callback', async () => {
|
||||
const callback = vi.fn();
|
||||
|
||||
animationFrame(callback);
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(expect.any(Number));
|
||||
});
|
||||
|
||||
it('returns a cleanup function', () => {
|
||||
const callback = vi.fn();
|
||||
|
||||
const cancel = animationFrame(callback);
|
||||
|
||||
expect(cancel).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
it('cancel prevents callback from being called', async () => {
|
||||
const callback = vi.fn();
|
||||
|
||||
const cancel = animationFrame(callback);
|
||||
cancel();
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('cancel can be called multiple times safely', async () => {
|
||||
const callback = vi.fn();
|
||||
|
||||
const cancel = animationFrame(callback);
|
||||
cancel();
|
||||
cancel();
|
||||
cancel();
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('allows multiple independent animation frames', async () => {
|
||||
const callback1 = vi.fn();
|
||||
const callback2 = vi.fn();
|
||||
const callback3 = vi.fn();
|
||||
|
||||
animationFrame(callback1);
|
||||
animationFrame(callback2);
|
||||
const cancel3 = animationFrame(callback3);
|
||||
|
||||
cancel3();
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(callback1).toHaveBeenCalledOnce();
|
||||
expect(callback2).toHaveBeenCalledOnce();
|
||||
expect(callback3).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Request an animation frame and return a cleanup function to cancel it.
|
||||
*
|
||||
* @param callback - The callback to invoke on the next animation frame
|
||||
* @returns A cleanup function that cancels the animation frame request
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const cancel = animationFrame((time) => {
|
||||
* console.log('Frame at', time);
|
||||
* });
|
||||
*
|
||||
* // Later, cancel if needed
|
||||
* cancel();
|
||||
* ```
|
||||
*/
|
||||
export function animationFrame(callback: FrameRequestCallback): () => void {
|
||||
const id = requestAnimationFrame(callback);
|
||||
return () => cancelAnimationFrame(id);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { onEvent } from './event';
|
||||
|
||||
describe('onEvent', () => {
|
||||
it('returns a promise', () => {
|
||||
const target = new EventTarget();
|
||||
const promise = onEvent(target, 'click');
|
||||
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
|
||||
// Dispatch to resolve
|
||||
target.dispatchEvent(new Event('click'));
|
||||
});
|
||||
|
||||
it('resolves with the event when it fires', async () => {
|
||||
const target = new EventTarget();
|
||||
const event = new Event('click');
|
||||
|
||||
const promise = onEvent(target, 'click');
|
||||
target.dispatchEvent(event);
|
||||
|
||||
await expect(promise).resolves.toBe(event);
|
||||
});
|
||||
|
||||
it('only listens for the first occurrence (once)', async () => {
|
||||
const target = new EventTarget();
|
||||
|
||||
const promise = onEvent(target, 'click');
|
||||
|
||||
target.dispatchEvent(new Event('click'));
|
||||
target.dispatchEvent(new Event('click'));
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toBeInstanceOf(Event);
|
||||
});
|
||||
|
||||
it('rejects when signal is already aborted', async () => {
|
||||
const target = new EventTarget();
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
|
||||
const promise = onEvent(target, 'click', { signal: controller.signal });
|
||||
|
||||
await expect(promise).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('rejects with AbortError when signal is aborted', async () => {
|
||||
const target = new EventTarget();
|
||||
const controller = new AbortController();
|
||||
|
||||
const promise = onEvent(target, 'click', { signal: controller.signal });
|
||||
controller.abort();
|
||||
|
||||
await expect(promise).rejects.toMatchObject({
|
||||
name: 'AbortError',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects with custom abort reason', async () => {
|
||||
const target = new EventTarget();
|
||||
const controller = new AbortController();
|
||||
const reason = new Error('Custom reason');
|
||||
|
||||
const promise = onEvent(target, 'click', { signal: controller.signal });
|
||||
controller.abort(reason);
|
||||
|
||||
await expect(promise).rejects.toBe(reason);
|
||||
});
|
||||
|
||||
it('cleans up abort listener when event fires', async () => {
|
||||
const target = new EventTarget();
|
||||
const controller = new AbortController();
|
||||
|
||||
const promise = onEvent(target, 'click', { signal: controller.signal });
|
||||
target.dispatchEvent(new Event('click'));
|
||||
|
||||
await promise;
|
||||
|
||||
// Aborting after resolution should not cause issues
|
||||
controller.abort();
|
||||
});
|
||||
|
||||
it('passes options to addEventListener', async () => {
|
||||
const target = new EventTarget();
|
||||
const addSpy = vi.spyOn(target, 'addEventListener');
|
||||
|
||||
const promise = onEvent(target, 'click', { passive: true, capture: true });
|
||||
target.dispatchEvent(new Event('click'));
|
||||
|
||||
await promise;
|
||||
|
||||
expect(addSpy).toHaveBeenCalledWith(
|
||||
'click',
|
||||
expect.any(Function),
|
||||
expect.objectContaining({
|
||||
passive: true,
|
||||
capture: true,
|
||||
once: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('works with HTMLMediaElement events', async () => {
|
||||
const video = document.createElement('video');
|
||||
|
||||
const promise = onEvent(video, 'play');
|
||||
video.dispatchEvent(new Event('play'));
|
||||
|
||||
const event = await promise;
|
||||
expect(event.type).toBe('play');
|
||||
});
|
||||
|
||||
it('works with Window events', async () => {
|
||||
const promise = onEvent(window, 'resize');
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
|
||||
const event = await promise;
|
||||
expect(event.type).toBe('resize');
|
||||
});
|
||||
|
||||
it('works with Document events', async () => {
|
||||
const promise = onEvent(document, 'visibilitychange');
|
||||
document.dispatchEvent(new Event('visibilitychange'));
|
||||
|
||||
const event = await promise;
|
||||
expect(event.type).toBe('visibilitychange');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
export interface OnEventOptions extends AddEventListenerOptions {
|
||||
/**
|
||||
* An AbortSignal to cancel waiting for the event.
|
||||
*
|
||||
* If aborted, the returned promise will reject with an `AbortError`.
|
||||
*/
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for an event to occur on a target.
|
||||
*
|
||||
* @param target - The event target (HTMLMediaElement)
|
||||
* @param type - The event type to wait for
|
||||
* @param options - Optional event options including AbortSignal
|
||||
* @returns A promise that resolves with the event
|
||||
*/
|
||||
export function onEvent<K extends keyof HTMLMediaElementEventMap>(
|
||||
target: HTMLMediaElement,
|
||||
type: K,
|
||||
options?: OnEventOptions,
|
||||
): Promise<HTMLMediaElementEventMap[K]>;
|
||||
|
||||
/**
|
||||
* Wait for an event to occur on a target.
|
||||
*
|
||||
* @param target - The event target (HTMLElement)
|
||||
* @param type - The event type to wait for
|
||||
* @param options - Optional event options including AbortSignal
|
||||
* @returns A promise that resolves with the event
|
||||
*/
|
||||
export function onEvent<K extends keyof HTMLElementEventMap>(
|
||||
target: HTMLElement,
|
||||
type: K,
|
||||
options?: OnEventOptions,
|
||||
): Promise<HTMLElementEventMap[K]>;
|
||||
|
||||
/**
|
||||
* Wait for an event to occur on a target.
|
||||
*
|
||||
* @param target - The event target (Window)
|
||||
* @param type - The event type to wait for
|
||||
* @param options - Optional event options including AbortSignal
|
||||
* @returns A promise that resolves with the event
|
||||
*/
|
||||
export function onEvent<K extends keyof WindowEventMap>(
|
||||
target: Window,
|
||||
type: K,
|
||||
options?: OnEventOptions,
|
||||
): Promise<WindowEventMap[K]>;
|
||||
|
||||
/**
|
||||
* Wait for an event to occur on a target.
|
||||
*
|
||||
* @param target - The event target (Document)
|
||||
* @param type - The event type to wait for
|
||||
* @param options - Optional event options including AbortSignal
|
||||
* @returns A promise that resolves with the event
|
||||
*/
|
||||
export function onEvent<K extends keyof DocumentEventMap>(
|
||||
target: Document,
|
||||
type: K,
|
||||
options?: OnEventOptions,
|
||||
): Promise<DocumentEventMap[K]>;
|
||||
|
||||
/**
|
||||
* Wait for an event to occur on a target.
|
||||
*
|
||||
* @param target - The event target
|
||||
* @param type - The event type to wait for
|
||||
* @param options - Optional event options including AbortSignal
|
||||
* @returns A promise that resolves with the event
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Wait for video to be seeked
|
||||
* const event = await onEvent(video, 'seeked');
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // With AbortSignal for cancellation
|
||||
* const controller = new AbortController();
|
||||
*
|
||||
* try {
|
||||
* const event = await onEvent(video, 'seeked', { signal: controller.signal });
|
||||
* } catch (e) {
|
||||
* if (e.name === 'AbortError') {
|
||||
* console.log('Cancelled waiting for event');
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // Cancel from elsewhere
|
||||
* controller.abort();
|
||||
* ```
|
||||
*/
|
||||
export function onEvent(target: EventTarget, type: string, options?: OnEventOptions): Promise<Event>;
|
||||
|
||||
export function onEvent(target: EventTarget, type: string, options?: OnEventOptions): Promise<Event> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const handleAbort = () => {
|
||||
reject(options?.signal?.reason ?? new DOMException('Aborted', 'AbortError'));
|
||||
};
|
||||
|
||||
// If already aborted, reject immediately
|
||||
if (options?.signal?.aborted) {
|
||||
handleAbort();
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for abort
|
||||
options?.signal?.addEventListener('abort', handleAbort, { once: true });
|
||||
|
||||
// Listen for the event
|
||||
target.addEventListener(
|
||||
type,
|
||||
(event) => {
|
||||
options?.signal?.removeEventListener('abort', handleAbort);
|
||||
resolve(event);
|
||||
},
|
||||
{ ...options, once: true },
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { idleCallback } from './idle-callback';
|
||||
|
||||
describe('idleCallback', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('calls the callback when idle', async () => {
|
||||
const callback = vi.fn();
|
||||
|
||||
idleCallback(callback);
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('passes deadline object to callback', async () => {
|
||||
const callback = vi.fn();
|
||||
|
||||
idleCallback(callback);
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
didTimeout: expect.any(Boolean),
|
||||
timeRemaining: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns a cleanup function', () => {
|
||||
const callback = vi.fn();
|
||||
|
||||
const cancel = idleCallback(callback);
|
||||
|
||||
expect(cancel).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
it('cancel prevents callback from being called', async () => {
|
||||
const callback = vi.fn();
|
||||
|
||||
const cancel = idleCallback(callback);
|
||||
cancel();
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('cancel can be called multiple times safely', async () => {
|
||||
const callback = vi.fn();
|
||||
|
||||
const cancel = idleCallback(callback);
|
||||
cancel();
|
||||
cancel();
|
||||
cancel();
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('allows multiple independent idle callbacks', async () => {
|
||||
const callback1 = vi.fn();
|
||||
const callback2 = vi.fn();
|
||||
const callback3 = vi.fn();
|
||||
|
||||
idleCallback(callback1);
|
||||
idleCallback(callback2);
|
||||
const cancel3 = idleCallback(callback3);
|
||||
|
||||
cancel3();
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(callback1).toHaveBeenCalledOnce();
|
||||
expect(callback2).toHaveBeenCalledOnce();
|
||||
expect(callback3).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('accepts options parameter', async () => {
|
||||
const callback = vi.fn();
|
||||
|
||||
idleCallback(callback, { timeout: 1000 });
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { supportsIdleCallback } from './supports';
|
||||
|
||||
/**
|
||||
* Request an idle callback and return a cleanup function to cancel it.
|
||||
*
|
||||
* Falls back to `setTimeout` with 1ms delay in environments that don't
|
||||
* support `requestIdleCallback` (e.g., Safari).
|
||||
*
|
||||
* @param callback - The callback to invoke when the browser is idle
|
||||
* @param options - Optional idle callback options (timeout, etc.)
|
||||
* @returns A cleanup function that cancels the idle callback request
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const cancel = idleCallback((deadline) => {
|
||||
* console.log('Time remaining:', deadline.timeRemaining());
|
||||
* });
|
||||
*
|
||||
* // Later, cancel if needed
|
||||
* cancel();
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // With timeout option
|
||||
* const cancel = idleCallback(doWork, { timeout: 1000 });
|
||||
* ```
|
||||
*/
|
||||
export function idleCallback(callback: IdleRequestCallback, options?: IdleRequestOptions): () => void {
|
||||
if (supportsIdleCallback()) {
|
||||
const id = requestIdleCallback(callback, options);
|
||||
return () => cancelIdleCallback(id);
|
||||
}
|
||||
|
||||
// Fallback for Safari and other browsers without requestIdleCallback
|
||||
const id = setTimeout(() => {
|
||||
callback({
|
||||
didTimeout: false,
|
||||
timeRemaining: () => 50, // Approximate idle time
|
||||
});
|
||||
}, 1);
|
||||
|
||||
return () => clearTimeout(id);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export { animationFrame } from './animation-frame';
|
||||
export { onEvent, type OnEventOptions } from './event';
|
||||
export { idleCallback } from './idle-callback';
|
||||
export { listen } from './listen';
|
||||
export { supportsAnimationFrame, supportsIdleCallback } from './supports';
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { listen } from './listen';
|
||||
|
||||
describe('listen', () => {
|
||||
it('adds an event listener', () => {
|
||||
const target = new EventTarget();
|
||||
const handler = vi.fn();
|
||||
|
||||
listen(target, 'click', handler);
|
||||
target.dispatchEvent(new Event('click'));
|
||||
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('returns a cleanup function', () => {
|
||||
const target = new EventTarget();
|
||||
const handler = vi.fn();
|
||||
|
||||
const cleanup = listen(target, 'click', handler);
|
||||
|
||||
expect(cleanup).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
it('cleanup removes the listener', () => {
|
||||
const target = new EventTarget();
|
||||
const handler = vi.fn();
|
||||
|
||||
const cleanup = listen(target, 'click', handler);
|
||||
cleanup();
|
||||
target.dispatchEvent(new Event('click'));
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes options to addEventListener', () => {
|
||||
const target = new EventTarget();
|
||||
const handler = vi.fn();
|
||||
|
||||
listen(target, 'click', handler, { once: true });
|
||||
|
||||
target.dispatchEvent(new Event('click'));
|
||||
target.dispatchEvent(new Event('click'));
|
||||
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('passes capture option correctly', () => {
|
||||
const parent = document.createElement('div');
|
||||
const child = document.createElement('span');
|
||||
parent.appendChild(child);
|
||||
|
||||
const order: string[] = [];
|
||||
|
||||
listen(parent, 'click', () => order.push('parent-capture'), { capture: true });
|
||||
listen(child, 'click', () => order.push('child'));
|
||||
listen(parent, 'click', () => order.push('parent-bubble'));
|
||||
|
||||
child.dispatchEvent(new Event('click', { bubbles: true }));
|
||||
|
||||
expect(order).toEqual(['parent-capture', 'child', 'parent-bubble']);
|
||||
});
|
||||
|
||||
it('cleanup works with options', () => {
|
||||
const target = new EventTarget();
|
||||
const handler = vi.fn();
|
||||
|
||||
const cleanup = listen(target, 'click', handler, { passive: true });
|
||||
cleanup();
|
||||
target.dispatchEvent(new Event('click'));
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('works with AbortSignal in options', () => {
|
||||
const target = new EventTarget();
|
||||
const handler = vi.fn();
|
||||
const controller = new AbortController();
|
||||
|
||||
listen(target, 'click', handler, { signal: controller.signal });
|
||||
|
||||
target.dispatchEvent(new Event('click'));
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
|
||||
controller.abort();
|
||||
target.dispatchEvent(new Event('click'));
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('provides correct event type for typed targets', () => {
|
||||
const video = document.createElement('video');
|
||||
const handler = vi.fn((event: Event) => {
|
||||
expect(event.type).toBe('play');
|
||||
});
|
||||
|
||||
listen(video, 'play', handler);
|
||||
video.dispatchEvent(new Event('play'));
|
||||
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('works with Window', () => {
|
||||
const handler = vi.fn();
|
||||
const cleanup = listen(window, 'resize', handler);
|
||||
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('works with Document', () => {
|
||||
const handler = vi.fn();
|
||||
const cleanup = listen(document, 'visibilitychange', handler);
|
||||
|
||||
document.dispatchEvent(new Event('visibilitychange'));
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
|
||||
cleanup();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Add an event listener and return a cleanup function to remove it.
|
||||
*
|
||||
* @param target - The event target (HTMLMediaElement)
|
||||
* @param type - The event type
|
||||
* @param listener - The event listener
|
||||
* @param options - Optional event listener options
|
||||
* @returns A cleanup function that removes the event listener
|
||||
*/
|
||||
export function listen<K extends keyof HTMLMediaElementEventMap>(
|
||||
target: HTMLMediaElement,
|
||||
type: K,
|
||||
listener: (event: HTMLMediaElementEventMap[K]) => void,
|
||||
options?: AddEventListenerOptions,
|
||||
): () => void;
|
||||
|
||||
/**
|
||||
* Add an event listener and return a cleanup function to remove it.
|
||||
*
|
||||
* @param target - The event target (HTMLElement)
|
||||
* @param type - The event type
|
||||
* @param listener - The event listener
|
||||
* @param options - Optional event listener options
|
||||
* @returns A cleanup function that removes the event listener
|
||||
*/
|
||||
export function listen<K extends keyof HTMLElementEventMap>(
|
||||
target: HTMLElement,
|
||||
type: K,
|
||||
listener: (event: HTMLElementEventMap[K]) => void,
|
||||
options?: AddEventListenerOptions,
|
||||
): () => void;
|
||||
|
||||
/**
|
||||
* Add an event listener and return a cleanup function to remove it.
|
||||
*
|
||||
* @param target - The event target (Window)
|
||||
* @param type - The event type
|
||||
* @param listener - The event listener
|
||||
* @param options - Optional event listener options
|
||||
* @returns A cleanup function that removes the event listener
|
||||
*/
|
||||
export function listen<K extends keyof WindowEventMap>(
|
||||
target: Window,
|
||||
type: K,
|
||||
listener: (event: WindowEventMap[K]) => void,
|
||||
options?: AddEventListenerOptions,
|
||||
): () => void;
|
||||
|
||||
/**
|
||||
* Add an event listener and return a cleanup function to remove it.
|
||||
*
|
||||
* @param target - The event target (Document)
|
||||
* @param type - The event type
|
||||
* @param listener - The event listener
|
||||
* @param options - Optional event listener options
|
||||
* @returns A cleanup function that removes the event listener
|
||||
*/
|
||||
export function listen<K extends keyof DocumentEventMap>(
|
||||
target: Document,
|
||||
type: K,
|
||||
listener: (event: DocumentEventMap[K]) => void,
|
||||
options?: AddEventListenerOptions,
|
||||
): () => void;
|
||||
|
||||
/**
|
||||
* Add an event listener and return a cleanup function to remove it.
|
||||
*
|
||||
* @param target - The event target
|
||||
* @param type - The event type
|
||||
* @param listener - The event listener
|
||||
* @param options - Optional event listener options
|
||||
* @returns A cleanup function that removes the event listener
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const cleanup = listen(video, 'play', () => console.log('playing'));
|
||||
*
|
||||
* // Later, remove the listener
|
||||
* cleanup();
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // With options
|
||||
* const cleanup = listen(video, 'play', handler, { once: true, passive: true });
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // With AbortSignal (native browser support)
|
||||
* const controller = new AbortController();
|
||||
* listen(video, 'play', handler, { signal: controller.signal });
|
||||
*
|
||||
* // Later, abort to remove the listener
|
||||
* controller.abort();
|
||||
* ```
|
||||
*/
|
||||
export function listen(
|
||||
target: EventTarget,
|
||||
type: string,
|
||||
listener: EventListenerOrEventListenerObject,
|
||||
options?: AddEventListenerOptions,
|
||||
): () => void;
|
||||
|
||||
export function listen(
|
||||
target: EventTarget,
|
||||
type: string,
|
||||
listener: EventListenerOrEventListenerObject,
|
||||
options?: AddEventListenerOptions,
|
||||
): () => void {
|
||||
target.addEventListener(type, listener, options);
|
||||
return () => target.removeEventListener(type, listener, options);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { supportsAnimationFrame, supportsIdleCallback } from './supports';
|
||||
|
||||
describe('supports', () => {
|
||||
describe('supportsAnimationFrame', () => {
|
||||
it('returns a boolean', () => {
|
||||
const result = supportsAnimationFrame();
|
||||
expect(typeof result).toBe('boolean');
|
||||
});
|
||||
|
||||
it('returns true in browser environment', () => {
|
||||
expect(supportsAnimationFrame()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('supportsIdleCallback', () => {
|
||||
it('returns a boolean', () => {
|
||||
const result = supportsIdleCallback();
|
||||
// Note: requestIdleCallback may or may not be available in jsdom
|
||||
// depending on the version, so we just check it returns a boolean
|
||||
expect(typeof result).toBe('boolean');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Check if `requestIdleCallback` is supported.
|
||||
*
|
||||
* @returns `true` if `requestIdleCallback` is available
|
||||
*/
|
||||
export function supportsIdleCallback(): boolean {
|
||||
return typeof requestIdleCallback === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if `requestAnimationFrame` is supported.
|
||||
*
|
||||
* @returns `true` if `requestAnimationFrame` is available
|
||||
*/
|
||||
export function supportsAnimationFrame(): boolean {
|
||||
return typeof requestAnimationFrame === 'function';
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { isEventLike } from './event-like';
|
||||
|
||||
describe('event-like', () => {
|
||||
describe('isEventLike', () => {
|
||||
it('returns true for objects with type and timeStamp', () => {
|
||||
expect(isEventLike({ type: 'click', timeStamp: 123 })).toBe(true);
|
||||
expect(isEventLike({ type: 'play', timeStamp: 0 })).toBe(true);
|
||||
expect(isEventLike({ type: '', timeStamp: Date.now() })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for objects with additional properties', () => {
|
||||
expect(
|
||||
isEventLike({
|
||||
type: 'click',
|
||||
timeStamp: 123,
|
||||
isTrusted: true,
|
||||
target: null,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for DOM Events', () => {
|
||||
const event = new Event('click');
|
||||
expect(isEventLike(event)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for CustomEvent', () => {
|
||||
const event = new CustomEvent('custom', { detail: { foo: 'bar' } });
|
||||
expect(isEventLike(event)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for missing type', () => {
|
||||
expect(isEventLike({ timeStamp: 123 })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for missing timeStamp', () => {
|
||||
expect(isEventLike({ type: 'click' })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for non-string type', () => {
|
||||
expect(isEventLike({ type: 123, timeStamp: 123 })).toBe(false);
|
||||
expect(isEventLike({ type: null, timeStamp: 123 })).toBe(false);
|
||||
expect(isEventLike({ type: undefined, timeStamp: 123 })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for non-number timeStamp', () => {
|
||||
expect(isEventLike({ type: 'click', timeStamp: '123' })).toBe(false);
|
||||
expect(isEventLike({ type: 'click', timeStamp: null })).toBe(false);
|
||||
expect(isEventLike({ type: 'click', timeStamp: undefined })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for null', () => {
|
||||
expect(isEventLike(null)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for undefined', () => {
|
||||
expect(isEventLike(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for primitives', () => {
|
||||
expect(isEventLike('event')).toBe(false);
|
||||
expect(isEventLike(123)).toBe(false);
|
||||
expect(isEventLike(true)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for arrays', () => {
|
||||
expect(isEventLike([])).toBe(false);
|
||||
expect(isEventLike(['click', 123])).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for functions', () => {
|
||||
expect(isEventLike(() => {})).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { isNumber, isObject, isString } from './predicate';
|
||||
|
||||
export interface EventLike {
|
||||
type: string;
|
||||
timeStamp: number;
|
||||
isTrusted?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value looks like an Event (has type and timeStamp).
|
||||
*
|
||||
* Works with DOM Events, React SyntheticEvents, and RN events.
|
||||
*/
|
||||
export function isEventLike(value: unknown): value is EventLike {
|
||||
return (
|
||||
isObject(value)
|
||||
&& 'type' in value
|
||||
&& isString(value.type)
|
||||
&& 'timeStamp' in value
|
||||
&& isNumber(value.timeStamp)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './disposer';
|
||||
export * from './event-like';
|
||||
export * from './predicate';
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
isAbortError,
|
||||
isBoolean,
|
||||
isFunction,
|
||||
isNil,
|
||||
isNull,
|
||||
isNumber,
|
||||
isObject,
|
||||
isPromise,
|
||||
isString,
|
||||
isUndefined,
|
||||
} from './predicate';
|
||||
|
||||
describe('predicate', () => {
|
||||
describe('isString', () => {
|
||||
it('returns true for strings', () => {
|
||||
expect(isString('')).toBe(true);
|
||||
expect(isString('hello')).toBe(true);
|
||||
expect(isString('123')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for non-strings', () => {
|
||||
expect(isString(123)).toBe(false);
|
||||
expect(isString(null)).toBe(false);
|
||||
expect(isString(undefined)).toBe(false);
|
||||
expect(isString({})).toBe(false);
|
||||
expect(isString([])).toBe(false);
|
||||
expect(isString(true)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNumber', () => {
|
||||
it('returns true for numbers', () => {
|
||||
expect(isNumber(0)).toBe(true);
|
||||
expect(isNumber(123)).toBe(true);
|
||||
expect(isNumber(-1)).toBe(true);
|
||||
expect(isNumber(3.14)).toBe(true);
|
||||
expect(isNumber(Number.NaN)).toBe(true);
|
||||
expect(isNumber(Infinity)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for non-numbers', () => {
|
||||
expect(isNumber('123')).toBe(false);
|
||||
expect(isNumber(null)).toBe(false);
|
||||
expect(isNumber(undefined)).toBe(false);
|
||||
expect(isNumber({})).toBe(false);
|
||||
expect(isNumber([])).toBe(false);
|
||||
expect(isNumber(true)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isBoolean', () => {
|
||||
it('returns true for booleans', () => {
|
||||
expect(isBoolean(true)).toBe(true);
|
||||
expect(isBoolean(false)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for non-booleans', () => {
|
||||
expect(isBoolean(0)).toBe(false);
|
||||
expect(isBoolean(1)).toBe(false);
|
||||
expect(isBoolean('true')).toBe(false);
|
||||
expect(isBoolean(null)).toBe(false);
|
||||
expect(isBoolean(undefined)).toBe(false);
|
||||
expect(isBoolean({})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFunction', () => {
|
||||
it('returns true for functions', () => {
|
||||
expect(isFunction(() => {})).toBe(true);
|
||||
expect(isFunction(() => {})).toBe(true);
|
||||
expect(isFunction(async () => {})).toBe(true);
|
||||
expect(isFunction(class {})).toBe(true);
|
||||
expect(isFunction(Array.isArray)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for non-functions', () => {
|
||||
expect(isFunction({})).toBe(false);
|
||||
expect(isFunction([])).toBe(false);
|
||||
expect(isFunction('function')).toBe(false);
|
||||
expect(isFunction(null)).toBe(false);
|
||||
expect(isFunction(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNull', () => {
|
||||
it('returns true for null', () => {
|
||||
expect(isNull(null)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for non-null', () => {
|
||||
expect(isNull(undefined)).toBe(false);
|
||||
expect(isNull(0)).toBe(false);
|
||||
expect(isNull('')).toBe(false);
|
||||
expect(isNull(false)).toBe(false);
|
||||
expect(isNull({})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isUndefined', () => {
|
||||
it('returns true for undefined', () => {
|
||||
expect(isUndefined(undefined)).toBe(true);
|
||||
expect(isUndefined(void 0)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for non-undefined', () => {
|
||||
expect(isUndefined(null)).toBe(false);
|
||||
expect(isUndefined(0)).toBe(false);
|
||||
expect(isUndefined('')).toBe(false);
|
||||
expect(isUndefined(false)).toBe(false);
|
||||
expect(isUndefined({})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNil', () => {
|
||||
it('returns true for null and undefined', () => {
|
||||
expect(isNil(null)).toBe(true);
|
||||
expect(isNil(undefined)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for non-nil values', () => {
|
||||
expect(isNil(0)).toBe(false);
|
||||
expect(isNil('')).toBe(false);
|
||||
expect(isNil(false)).toBe(false);
|
||||
expect(isNil({})).toBe(false);
|
||||
expect(isNil([])).toBe(false);
|
||||
expect(isNil(Number.NaN)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPromise', () => {
|
||||
it('returns true for promises', () => {
|
||||
expect(isPromise(Promise.resolve())).toBe(true);
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
expect(isPromise(Promise.reject().catch(() => {}))).toBe(true);
|
||||
expect(isPromise(new Promise(() => {}))).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for non-promises', () => {
|
||||
expect(isPromise({})).toBe(false);
|
||||
expect(isPromise({ then: () => {} })).toBe(false); // thenable but not Promise
|
||||
expect(isPromise(null)).toBe(false);
|
||||
expect(isPromise(undefined)).toBe(false);
|
||||
expect(isPromise(() => {})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isObject', () => {
|
||||
it('returns true for objects', () => {
|
||||
expect(isObject({})).toBe(true);
|
||||
expect(isObject([])).toBe(true);
|
||||
expect(isObject(new Date())).toBe(true);
|
||||
expect(isObject(/regex/)).toBe(true);
|
||||
expect(isObject(new Map())).toBe(true);
|
||||
expect(isObject(new Set())).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for null', () => {
|
||||
expect(isObject(null)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for primitives', () => {
|
||||
expect(isObject(undefined)).toBe(false);
|
||||
expect(isObject('string')).toBe(false);
|
||||
expect(isObject(123)).toBe(false);
|
||||
expect(isObject(true)).toBe(false);
|
||||
// eslint-disable-next-line symbol-description
|
||||
expect(isObject(Symbol())).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for functions', () => {
|
||||
expect(isObject(() => {})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAbortError', () => {
|
||||
it('returns true for AbortError', () => {
|
||||
const error = new DOMException('Aborted', 'AbortError');
|
||||
expect(isAbortError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for custom AbortError', () => {
|
||||
const error = new Error('Aborted');
|
||||
error.name = 'AbortError';
|
||||
expect(isAbortError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for other errors', () => {
|
||||
expect(isAbortError(new Error('test'))).toBe(false);
|
||||
expect(isAbortError(new TypeError('test'))).toBe(false);
|
||||
expect(isAbortError(new RangeError('test'))).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for non-errors', () => {
|
||||
expect(isAbortError(null)).toBe(false);
|
||||
expect(isAbortError(undefined)).toBe(false);
|
||||
expect(isAbortError('AbortError')).toBe(false);
|
||||
expect(isAbortError({ name: 'AbortError' })).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
export function isString(value: unknown): value is string {
|
||||
return typeof value === 'string';
|
||||
}
|
||||
|
||||
export function isNumber(value: unknown): value is number {
|
||||
return typeof value === 'number';
|
||||
}
|
||||
|
||||
export function isBoolean(value: unknown): value is boolean {
|
||||
return typeof value === 'boolean';
|
||||
}
|
||||
|
||||
export function isFunction(value: unknown): value is (...args: any[]) => any {
|
||||
return typeof value === 'function';
|
||||
}
|
||||
|
||||
export function isNull(value: unknown): value is null {
|
||||
return value === null;
|
||||
}
|
||||
|
||||
export function isUndefined(value: unknown): value is undefined {
|
||||
return typeof value === 'undefined';
|
||||
}
|
||||
|
||||
export function isNil(value: unknown): value is null | undefined {
|
||||
return value == null;
|
||||
}
|
||||
|
||||
export function isPromise(value: unknown): value is Promise<any> {
|
||||
return value instanceof Promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is an object, excluding null.
|
||||
*/
|
||||
export function isObject(value: unknown): value is object {
|
||||
return value !== null && typeof value === 'object';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is an AbortError.
|
||||
*/
|
||||
export function isAbortError(value: unknown): value is Error {
|
||||
return (
|
||||
value instanceof Error
|
||||
&& value.name === 'AbortError'
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user