Cb 5757 unit tests for core utils and core blocks (#2961)

* CB-5757 adds unit tests for getDomainFromUrl helper

* CB-5757 adds unit tests for getTextFileReadingProcess helper

* CB-5757 adds unit tests for TextTools helper

* CB-5757 adds unit tests for downloadFromURL helper

* CB-5757 adds unit tests for Deferred and PromiseExecutor

* CB-5757 adds unit tests for core-blocks hooks

* CB-5757 fixes the tests
This commit is contained in:
sergeyteleshev
2024-10-18 08:34:19 +03:00
committed by GitHub
parent 115cfb3bde
commit ffe0a18828
10 changed files with 621 additions and 37 deletions
-1
View File
@@ -219,7 +219,6 @@ export * from './useClipboard.js';
export * from './useCombinedHandler.js';
export * from './useCombinedRef.js';
export * from './useExecutor.js';
export * from './useFn.js';
export * from './useFocus.js';
export * from './useFormValidator.js';
export * from './ResourcesHooks/useOffsetPagination.js';
@@ -0,0 +1,159 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
import { act, renderHook } from '@testing-library/react';
import { useActivationDelay } from './useActivationDelay.js';
describe('useActivationDelay', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.clearAllTimers();
jest.useRealTimers();
});
it('should initially return false', () => {
const { result } = renderHook(() => useActivationDelay(false, 1000));
expect(result.current).toBe(false);
});
it('should not change state before delay when activated', () => {
const { result } = renderHook(() => useActivationDelay(true, 1000));
expect(result.current).toBe(false);
act(() => {
jest.advanceTimersByTime(500);
});
expect(result.current).toBe(false);
});
it('should change state after delay when activated', () => {
const { result } = renderHook(() => useActivationDelay(true, 1000));
act(() => {
jest.advanceTimersByTime(1000);
});
expect(result.current).toBe(true);
});
it('should fire callback after delay when activated', () => {
const callback = jest.fn();
renderHook(() => useActivationDelay(true, 1000, callback));
expect(callback).not.toHaveBeenCalled();
act(() => {
jest.advanceTimersByTime(1000);
});
expect(callback).toHaveBeenCalledTimes(1);
});
it('should handle callback changes', () => {
const initialCallback = jest.fn();
const newCallback = jest.fn();
const { rerender } = renderHook(({ callback }) => useActivationDelay(true, 1000, callback), { initialProps: { callback: initialCallback } });
rerender({ callback: newCallback });
act(() => {
jest.advanceTimersByTime(1000);
});
expect(initialCallback).not.toHaveBeenCalled();
expect(newCallback).toHaveBeenCalledTimes(1);
});
it('should clear timeout on unmount', () => {
const callback = jest.fn();
const { unmount } = renderHook(() => useActivationDelay(true, 1000, callback));
unmount();
act(() => {
jest.advanceTimersByTime(1000);
});
expect(callback).not.toHaveBeenCalled();
});
it('should not fire callback if delay changed', () => {
const callback = jest.fn();
const { rerender } = renderHook(({ delay }) => useActivationDelay(true, delay, callback), { initialProps: { delay: 1000 } });
act(() => {
jest.advanceTimersByTime(500);
});
rerender({ delay: 2000 });
act(() => {
jest.advanceTimersByTime(1500);
});
expect(callback).toHaveBeenCalledTimes(0);
});
it('should not fire callback when state changed to false', () => {
const callback = jest.fn();
const { rerender } = renderHook(({ state }) => useActivationDelay(state, 1000, callback), { initialProps: { state: true } });
act(() => {
jest.advanceTimersByTime(500);
});
rerender({ state: false });
act(() => {
jest.advanceTimersByTime(500);
});
expect(callback).toHaveBeenCalledTimes(0);
});
it('should handle rapid state changes', () => {
const callback = jest.fn();
const { rerender } = renderHook(({ state }) => useActivationDelay(state, 1000, callback), { initialProps: { state: true } });
act(() => {
jest.advanceTimersByTime(500);
});
rerender({ state: false });
act(() => {
jest.advanceTimersByTime(100);
});
rerender({ state: true });
act(() => {
jest.advanceTimersByTime(1000);
});
expect(callback).toHaveBeenCalledTimes(1);
});
it('should work with zero delay', () => {
const callback = jest.fn();
renderHook(() => useActivationDelay(true, 0, callback));
act(() => {
jest.advanceTimersByTime(0);
});
expect(callback).toHaveBeenCalledTimes(1);
});
});
@@ -0,0 +1,69 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { describe, expect, jest, test } from '@jest/globals';
import { renderHook } from '@testing-library/react';
import { useCombinedHandler } from './useCombinedHandler.js';
jest.mock('./useObjectRef', () => ({
useObjectRef: jest.fn(value => value),
}));
describe('useCombinedHandler', () => {
test('should call all provided handlers', () => {
const handler1 = jest.fn();
const handler2 = jest.fn();
const { result } = renderHook(() => useCombinedHandler(handler1, handler2));
result.current('arg1', 'arg2');
expect(handler1).toHaveBeenCalledWith('arg1', 'arg2');
expect(handler2).toHaveBeenCalledWith('arg1', 'arg2');
});
test('should handle null and undefined handlers', () => {
const { result } = renderHook(() => useCombinedHandler(null, undefined));
expect(() => result.current('testArg')).not.toThrow();
});
test('should not fail when no handlers are provided', () => {
const { result } = renderHook(() => useCombinedHandler());
expect(() => result.current()).not.toThrow();
});
test('should allow the combined handler to be called multiple times', () => {
const handler1 = jest.fn();
const handler2 = jest.fn();
const { result } = renderHook(() => useCombinedHandler(handler1, handler2));
result.current('firstCall');
result.current('secondCall');
expect(handler1).toHaveBeenNthCalledWith(1, 'firstCall');
expect(handler1).toHaveBeenNthCalledWith(2, 'secondCall');
expect(handler2).toHaveBeenNthCalledWith(1, 'firstCall');
expect(handler2).toHaveBeenNthCalledWith(2, 'secondCall');
});
test('should work with asynchronous handlers', async () => {
const handler1 = jest.fn(async arg => await Promise.resolve(arg));
const handler2 = jest.fn(async arg => await Promise.resolve(arg));
const { result } = renderHook(() => useCombinedHandler(handler1, handler2));
result.current('asyncArg');
expect(handler1).toHaveBeenCalledWith('asyncArg');
expect(handler2).toHaveBeenCalledWith('asyncArg');
});
});
@@ -0,0 +1,69 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { describe, expect, it, jest } from '@jest/globals';
import { act, renderHook } from '@testing-library/react';
import React from 'react';
import { useCombinedRef } from './useCombinedRef.js';
describe('useCombinedRef', () => {
it('should handle ref as function', () => {
const callbackRef = jest.fn();
const { result } = renderHook(() => useCombinedRef(callbackRef));
const testInstance = { test: true };
act(() => {
result.current(testInstance);
});
expect(callbackRef).toHaveBeenCalledWith(testInstance);
});
it('should handle ref as RefObject', () => {
const mutableRef = React.createRef();
const { result } = renderHook(() => useCombinedRef(mutableRef));
const testInstance = { test: true };
act(() => {
result.current(testInstance);
});
expect(mutableRef.current).toBe(testInstance);
});
it('should handle ref as null', () => {
const { result } = renderHook(() => useCombinedRef(null));
const testInstance = { test: true };
expect(() => {
act(() => {
result.current(testInstance);
});
}).not.toThrow();
});
it('should handle multiple refs', () => {
const callbackRef = jest.fn();
const mutableRef = React.createRef();
const anotherMutableRef = React.createRef();
const { result } = renderHook(() => useCombinedRef(callbackRef, mutableRef, anotherMutableRef));
const testInstance = { test: true };
act(() => {
result.current(testInstance);
});
expect(callbackRef).toHaveBeenCalledWith(testInstance);
expect(mutableRef.current).toBe(testInstance);
expect(anotherMutableRef.current).toBe(testInstance);
});
});
-17
View File
@@ -1,17 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { useCallback, useRef } from 'react';
const noop: any[] = [];
export function useFn<TArgs extends any[], TValue>(fn: (...args: TArgs) => TValue): (...args: TArgs) => TValue {
const ref = useRef(fn);
ref.current = fn;
return useCallback((...args: TArgs) => ref.current(...args), noop);
}
@@ -7,8 +7,6 @@
*/
import { action, computed, makeObservable, observable } from 'mobx';
import { errorOf } from '../errorOf.js';
import { PromiseCancelledError } from './PromiseCancelledError.js';
import { PromiseExecutor } from './PromiseExecutor.js';
export enum EDeferredState {
@@ -111,20 +109,3 @@ export class Deferred<T> {
}
}
}
export class DeferredFromPromise<T> extends Deferred<T> {
constructor(promise: Promise<T>) {
super();
promise.then(
value => this.toResolved(value),
err => {
const promiseCancelledError = errorOf(err, PromiseCancelledError);
if (promiseCancelledError) {
this.toCancelled(promiseCancelledError.cause);
} else {
this.toRejected(err);
}
},
);
}
}
@@ -0,0 +1,46 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { beforeEach, describe, expect, it } from '@jest/globals';
import { PromiseExecutor } from './PromiseExecutor.js';
describe('PromiseExecutor', () => {
let promiseExecutor: PromiseExecutor<string>;
beforeEach(() => {
promiseExecutor = new PromiseExecutor();
});
it('should initialize with a promise', () => {
expect(promiseExecutor.promise).toBeInstanceOf(Promise);
});
it('should resolve the promise with the correct value', async () => {
const testValue = 'resolved value';
promiseExecutor.resolve(testValue);
await expect(promiseExecutor.promise).resolves.toBe(testValue);
});
it('should reject the promise with the correct reason', async () => {
const testError = new Error('test error');
promiseExecutor.reject(testError);
await expect(promiseExecutor.promise).rejects.toBe(testError);
});
it('should resolve with a PromiseLike object', async () => {
const testPromiseLike = Promise.resolve('promise-like value');
promiseExecutor.resolve(testPromiseLike);
await expect(promiseExecutor.promise).resolves.toBe('promise-like value');
});
});
@@ -0,0 +1,117 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
import { TextTools } from './TextTools.js';
const TEXT_WIDTH = 100;
const LETTER_SPACING = 0.4;
function getLength(value: string): number {
return TEXT_WIDTH + (value.length - 1) * LETTER_SPACING;
}
describe('TextTools', () => {
let mockContext: jest.Mocked<CanvasRenderingContext2D>;
beforeEach(() => {
mockContext = {
measureText: jest.fn().mockReturnValue({ width: TEXT_WIDTH }),
font: '',
} as unknown as jest.Mocked<CanvasRenderingContext2D>;
const mockCanvas = {
getContext: jest.fn().mockReturnValue(mockContext),
};
jest.spyOn(document, 'createElement').mockImplementation(() => mockCanvas as unknown as HTMLCanvasElement);
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('getWidth', () => {
it('should calculate width without container', () => {
const options = {
font: 'bold 16px Arial',
text: ['Hello', 'World'],
};
const result = TextTools.getWidth(options);
expect(mockContext.font).toBe('bold 16px Arial');
expect(result).toEqual(options.text.map(getLength));
expect(mockContext.measureText).toHaveBeenCalledTimes(options.text.length);
});
it('should use container styles when provided', () => {
const mockContainer = document.createElement('div');
const mockStyles = {
getPropertyValue: jest.fn().mockReturnValueOnce('bold').mockReturnValueOnce('16px').mockReturnValueOnce('Arial, sans'),
};
jest.spyOn(window, 'getComputedStyle').mockReturnValue(mockStyles as unknown as CSSStyleDeclaration);
const options = {
font: 'default',
container: mockContainer,
text: ['Test'],
};
const result = TextTools.getWidth(options);
expect(mockContext.font).toBe('bold 16px Arial');
expect(result).toEqual(options.text.map(getLength));
});
it('should use provided font when container styles are incomplete', () => {
const mockContainer = document.createElement('div');
const mockStyles = {
getPropertyValue: jest.fn().mockReturnValue(''),
};
jest.spyOn(window, 'getComputedStyle').mockReturnValue(mockStyles as unknown as CSSStyleDeclaration);
const options = {
font: 'italic 14px Times',
container: mockContainer,
text: ['Test'],
};
const result = TextTools.getWidth(options);
expect(mockContext.font).toBe('italic 14px Times');
expect(result).toEqual(options.text.map(getLength));
});
it('should handle empty text array', () => {
const options = {
font: 'bold 16px Arial',
text: [],
};
const result = TextTools.getWidth(options);
expect(result).toEqual([]);
expect(mockContext.measureText).not.toHaveBeenCalled();
});
it('should handle single-character strings', () => {
const options = {
font: 'bold 16px Arial',
text: ['A', 'B'],
};
const result = TextTools.getWidth(options);
expect(result).toEqual(options.text.map(getLength));
expect(mockContext.measureText).toHaveBeenCalledTimes(options.text.length);
});
});
});
@@ -0,0 +1,78 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
import { downloadFromURL } from './downloadFromURL.js';
type MockXHR = {
open: jest.Mock;
send: jest.Mock;
setRequestHeader: jest.Mock;
responseType: string;
onload: jest.Mock;
onerror: jest.Mock;
response: Blob | null;
};
describe('downloadFromURL', () => {
let mockXHR: MockXHR;
beforeEach(() => {
mockXHR = {
open: jest.fn(),
send: jest.fn(),
setRequestHeader: jest.fn(),
responseType: '',
onload: jest.fn(),
onerror: jest.fn(),
response: null,
};
(global as any).XMLHttpRequest = jest.fn(() => mockXHR);
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should open and send request to the specified URL', async () => {
const url = 'http://example.com/test';
downloadFromURL(url);
expect(mockXHR.open).toHaveBeenCalledWith('GET', url, true);
expect(mockXHR.send).toHaveBeenCalled();
});
it('should resolve with a Blob when the request is successful', async () => {
const mockBlob = new Blob(['test'], { type: 'text/plain' });
mockXHR.response = mockBlob;
setTimeout(() => {
mockXHR.onload?.();
}, 0);
const url = 'http://example.com/test';
const result = await downloadFromURL(url);
expect(mockXHR.responseType).toBe('blob');
expect(result).toBe(mockBlob);
});
it('should reject with an error when the request fails', async () => {
const mockError = new Error('Network error');
setTimeout(() => {
mockXHR.onerror?.(mockError);
}, 0);
const url = 'http://example.com/test';
await expect(downloadFromURL(url)).rejects.toThrow('Network error');
expect(mockXHR.responseType).toBe('blob');
});
});
@@ -0,0 +1,83 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
import { getTextFileReadingProcess } from './getTextFileReadingProcess.js';
describe('getTextFileReadingProcess', () => {
let file: File;
let mockFileReader: Partial<FileReader>;
beforeEach(() => {
file = new File(['file content'], 'test.txt', { type: 'text/plain' });
mockFileReader = {
readAsText: jest.fn(),
onload: null,
onerror: null,
onabort: null,
};
jest.spyOn(window, 'FileReader').mockImplementation(() => mockFileReader as FileReader);
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should return correct instances', () => {
const { promise, reader } = getTextFileReadingProcess(file);
expect(promise).toBeInstanceOf(Promise);
expect(reader).toBe(mockFileReader);
});
it('should resolve with file content on successful read', async () => {
const { promise } = getTextFileReadingProcess(file);
(mockFileReader.onload as any)({
target: { result: 'file content' },
});
await expect(promise).resolves.toBe('file content');
expect(mockFileReader.readAsText).toHaveBeenCalledWith(file);
});
it('should reject with an error if file has no data', async () => {
const { promise } = getTextFileReadingProcess(file);
const results = [null, undefined, ''];
for (const result of results) {
(mockFileReader.onload as any)({
target: { result: result },
});
await expect(promise).rejects.toThrow(`No data for the file: "${file.name}"`);
}
});
it('should reject with an error if reading fails', async () => {
const { promise } = getTextFileReadingProcess(file);
const error = new Error('Read error');
(mockFileReader.onerror as any)({
target: { error: error },
});
await expect(promise).rejects.toThrow(`Error occurred reading file: "${file.name}"`);
});
it('should reject if the read is aborted', async () => {
const { promise } = getTextFileReadingProcess(file);
(mockFileReader.onabort as any)();
await expect(promise).rejects.toThrow(`Reading "${file.name}" is aborted`);
});
});