Cb 5293 unit tests for hooks in core blocks (#2724)

* CB-5293 adds unit tests for useState delay hook

* CB-5293 removes unused helper hook

* СB-5277 adds tests for useClipboard hook

---------

Co-authored-by: Alexey <wrouds@gmail.com>
This commit is contained in:
sergeyteleshev
2024-06-26 16:38:07 +03:00
committed by GitHub
co-authored by Alexey
parent 56a2e8c13d
commit 6e4f16e824
6 changed files with 239 additions and 56 deletions
-1
View File
@@ -198,7 +198,6 @@ export * from './TimerIcon';
export * from './InfoItem';
export * from './Iframe';
export * from './Code';
export * from './useClickEvents';
export * from './useControlledScroll';
export * from './useClipboard';
export * from './useCombinedHandler';
@@ -1,45 +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 { useObjectRef } from './useObjectRef';
interface IOptions<T> {
onClick?: () => void;
onDoubleClick?: (event: React.MouseEvent<T>) => void;
}
interface IHandlers<T> {
onClick: (event: React.MouseEvent<T>) => void;
onDoubleClick: (event: React.MouseEvent<T>) => void;
}
export function useClickEvents<T>(options: IOptions<T>): IHandlers<T> {
return useObjectRef(
() => ({
delayed: false,
onClick(event: React.MouseEvent<T>) {
if (this.delayed) {
return;
}
this.delayed = true;
setTimeout(() => {
if (this.delayed) {
this.options.onClick?.();
this.delayed = false;
}
}, 300);
},
onDoubleClick(event: React.MouseEvent<T>) {
this.delayed = false;
options.onDoubleClick?.(event);
},
}),
{ options },
);
}
@@ -0,0 +1,97 @@
/*
* 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 { renderHook } from '@testing-library/react';
import * as coreDi from '@cloudbeaver/core-di';
import * as coreUtils from '@cloudbeaver/core-utils';
import { useClipboard } from './useClipboard';
jest.mock('@cloudbeaver/core-utils', () => ({
copyToClipboard: jest.fn(),
}));
jest.mock('@cloudbeaver/core-di', () => ({
useService: jest.fn(),
}));
jest.mock('@cloudbeaver/core-events', () => ({
NotificationService: {},
}));
class NotificationService {
logSuccess = jest.fn();
logException = jest.fn();
}
const getMocks = () => {
const copyToClipboardMock = jest.fn();
const notificationServiceMock = new NotificationService();
jest.spyOn(coreUtils, 'copyToClipboard').mockImplementation(copyToClipboardMock);
jest.spyOn(coreDi, 'useService').mockImplementation(() => notificationServiceMock);
return {
copyToClipboardMock,
notificationServiceMock,
};
};
describe('useClipboard', () => {
const VALUE_TO_COPY = 'test';
beforeEach(() => {
jest.clearAllMocks();
});
it('should copy without notification', async () => {
const { notificationServiceMock, copyToClipboardMock } = getMocks();
const { result } = renderHook(() => useClipboard());
result.current(VALUE_TO_COPY);
expect(copyToClipboardMock).toHaveBeenCalledWith(VALUE_TO_COPY);
result.current(VALUE_TO_COPY, false);
expect(copyToClipboardMock).toHaveBeenCalledWith(VALUE_TO_COPY);
expect(copyToClipboardMock).toHaveBeenCalledTimes(2);
expect(notificationServiceMock.logSuccess).not.toHaveBeenCalled();
expect(notificationServiceMock.logException).not.toHaveBeenCalled();
});
it('should copy with notification', async () => {
const { notificationServiceMock, copyToClipboardMock } = getMocks();
const { result } = renderHook(() => useClipboard());
result.current(VALUE_TO_COPY, true);
expect(copyToClipboardMock).toHaveBeenCalledWith(VALUE_TO_COPY);
expect(notificationServiceMock.logSuccess).toHaveBeenCalledWith({ title: 'ui_copy_to_clipboard_copied' });
expect(notificationServiceMock.logException).not.toHaveBeenCalled();
});
it('should handle exception while trying to copy', async () => {
const { notificationServiceMock, copyToClipboardMock } = getMocks();
const { result } = renderHook(() => useClipboard());
const exception = new Error('test');
copyToClipboardMock.mockImplementation(() => {
throw exception;
});
result.current(VALUE_TO_COPY, true);
expect(copyToClipboardMock).toHaveBeenCalledWith(VALUE_TO_COPY);
expect(notificationServiceMock.logSuccess).not.toHaveBeenCalled();
expect(notificationServiceMock.logException).toHaveBeenCalledWith(exception, 'ui_copy_to_clipboard_failed_to_copy');
});
});
@@ -8,7 +8,7 @@
import { useCallback } from 'react';
import { useService } from '@cloudbeaver/core-di';
import { ENotificationType, NotificationService } from '@cloudbeaver/core-events';
import { NotificationService } from '@cloudbeaver/core-events';
import { copyToClipboard } from '@cloudbeaver/core-utils';
export function useClipboard() {
@@ -19,7 +19,7 @@ export function useClipboard() {
try {
copyToClipboard(value);
if (notify) {
notificationService.notify({ title: 'ui_copy_to_clipboard_copied' }, ENotificationType.Success);
notificationService.logSuccess({ title: 'ui_copy_to_clipboard_copied' });
}
} catch (exception: any) {
notificationService.logException(exception, 'ui_copy_to_clipboard_failed_to_copy');
@@ -0,0 +1,135 @@
/*
* 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 { renderHook, waitFor } from '@testing-library/react';
import { useStateDelay } from './useStateDelay';
interface IHookProps {
value: boolean;
delay: number;
callback?: VoidFunction;
}
const useStateDelayWrapper = ({ value, delay, callback }: IHookProps) => useStateDelay(value, delay, callback);
describe('useStateDelay', () => {
beforeAll(() => {
jest.useFakeTimers();
});
test("should return initial state during whole hook's lifecycle", async () => {
const { result } = renderHook(() => useStateDelay(true, 100));
expect(result.current).toBe(true);
await waitFor(async () => {
await new Promise(resolve => setTimeout(resolve, 50));
});
expect(result.current).toBe(true);
await waitFor(async () => {
await new Promise(resolve => setTimeout(resolve, 60));
});
expect(result.current).toBe(true);
});
test('should return updated state after delay if it was updated', async () => {
const { result, rerender } = renderHook(({ value, delay }: IHookProps) => useStateDelayWrapper({ value, delay }), {
initialProps: {
value: false,
delay: 100,
},
});
expect(result.current).toBe(false);
rerender({
value: true,
delay: 100,
});
await waitFor(async () => {
await new Promise(resolve => setTimeout(resolve, 50));
});
expect(result.current).toBe(false);
await waitFor(async () => {
await new Promise(resolve => setTimeout(resolve, 60));
});
expect(result.current).toBe(true);
});
test('should execute callback on state change', async () => {
const callback = jest.fn();
const { rerender } = renderHook(({ value, delay }: IHookProps) => useStateDelayWrapper({ value, delay, callback }), {
initialProps: {
value: false,
delay: 100,
callback,
},
});
expect(callback).toHaveBeenCalledTimes(0);
await waitFor(async () => {
await new Promise(resolve => setTimeout(resolve, 50));
});
expect(callback).toHaveBeenCalledTimes(0);
rerender({
value: true,
delay: 100,
callback,
});
await waitFor(async () => {
await new Promise(resolve => setTimeout(resolve, 60));
});
expect(callback).toHaveBeenCalledTimes(1);
});
test('should not call callback', async () => {
const callback = jest.fn();
const { result, rerender } = renderHook(({ value, delay }: IHookProps) => useStateDelayWrapper({ value, delay, callback }), {
initialProps: {
value: false,
delay: 100,
callback,
},
});
expect(result.current).toBe(false);
expect(callback).toHaveBeenCalledTimes(0);
await waitFor(async () => {
await new Promise(resolve => setTimeout(resolve, 50));
});
expect(callback).toHaveBeenCalledTimes(0);
rerender({
value: false,
delay: 100,
callback,
});
await waitFor(async () => {
await new Promise(resolve => setTimeout(resolve, 60));
});
expect(callback).toHaveBeenCalledTimes(0);
});
test("should prolong delay if was updated as hook's argument", async () => {
const { result, rerender } = renderHook(({ value, delay }: IHookProps) => useStateDelayWrapper({ value, delay }), {
initialProps: {
value: false,
delay: 100,
},
});
expect(result.current).toBe(false);
await waitFor(async () => {
await new Promise(resolve => setTimeout(resolve, 50));
});
rerender({
value: true,
delay: 200,
});
await waitFor(async () => {
await new Promise(resolve => setTimeout(resolve, 60));
});
expect(result.current).toBe(false);
await waitFor(async () => {
await new Promise(resolve => setTimeout(resolve, 100));
});
expect(result.current).toBe(true);
});
});
@@ -13,15 +13,12 @@ import { AppContext, IServiceInjector } from '@cloudbeaver/core-di';
import type { IApplication } from './createApp';
function ApplicationWrapper(serviceInjector: IServiceInjector): React.FC<React.PropsWithChildren> {
return function render({ children }) {
return (
<Suspense fallback={null}>
<AppContext app={serviceInjector}>{children}</AppContext>
</Suspense>
);
};
return ({ children }) => (
<Suspense fallback={null}>
<AppContext app={serviceInjector}>{children}</AppContext>
</Suspense>
);
}
export function renderInApp<
Q extends Queries = typeof queries,
Container extends Element | DocumentFragment = HTMLElement,