diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts
index 662fc587af..d0f3993f09 100644
--- a/webapp/packages/core-blocks/src/index.ts
+++ b/webapp/packages/core-blocks/src/index.ts
@@ -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';
diff --git a/webapp/packages/core-blocks/src/useClickEvents.ts b/webapp/packages/core-blocks/src/useClickEvents.ts
deleted file mode 100644
index 154768d8fc..0000000000
--- a/webapp/packages/core-blocks/src/useClickEvents.ts
+++ /dev/null
@@ -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
{
- onClick?: () => void;
- onDoubleClick?: (event: React.MouseEvent) => void;
-}
-
-interface IHandlers {
- onClick: (event: React.MouseEvent) => void;
- onDoubleClick: (event: React.MouseEvent) => void;
-}
-
-export function useClickEvents(options: IOptions): IHandlers {
- return useObjectRef(
- () => ({
- delayed: false,
- onClick(event: React.MouseEvent) {
- if (this.delayed) {
- return;
- }
-
- this.delayed = true;
-
- setTimeout(() => {
- if (this.delayed) {
- this.options.onClick?.();
- this.delayed = false;
- }
- }, 300);
- },
- onDoubleClick(event: React.MouseEvent) {
- this.delayed = false;
- options.onDoubleClick?.(event);
- },
- }),
- { options },
- );
-}
diff --git a/webapp/packages/core-blocks/src/useClipBoard.test.ts b/webapp/packages/core-blocks/src/useClipBoard.test.ts
new file mode 100644
index 0000000000..9c8462c610
--- /dev/null
+++ b/webapp/packages/core-blocks/src/useClipBoard.test.ts
@@ -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');
+ });
+});
diff --git a/webapp/packages/core-blocks/src/useClipboard.ts b/webapp/packages/core-blocks/src/useClipboard.ts
index 225912e789..ee33f84f6c 100644
--- a/webapp/packages/core-blocks/src/useClipboard.ts
+++ b/webapp/packages/core-blocks/src/useClipboard.ts
@@ -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');
diff --git a/webapp/packages/core-blocks/src/useStateDelay.test.ts b/webapp/packages/core-blocks/src/useStateDelay.test.ts
new file mode 100644
index 0000000000..ea80893132
--- /dev/null
+++ b/webapp/packages/core-blocks/src/useStateDelay.test.ts
@@ -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);
+ });
+});
diff --git a/webapp/packages/tests-runner/src/renderInApp.tsx b/webapp/packages/tests-runner/src/renderInApp.tsx
index 7f2815115c..3acddc2cc1 100644
--- a/webapp/packages/tests-runner/src/renderInApp.tsx
+++ b/webapp/packages/tests-runner/src/renderInApp.tsx
@@ -13,15 +13,12 @@ import { AppContext, IServiceInjector } from '@cloudbeaver/core-di';
import type { IApplication } from './createApp';
function ApplicationWrapper(serviceInjector: IServiceInjector): React.FC {
- return function render({ children }) {
- return (
-
- {children}
-
- );
- };
+ return ({ children }) => (
+
+ {children}
+
+ );
}
-
export function renderInApp<
Q extends Queries = typeof queries,
Container extends Element | DocumentFragment = HTMLElement,