mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
[CB] write unit tests (#3928)
* dbeaver/pro#7563 adds unit tests for core-utils * removes restore all mocks * adds eslint rules: now arrow function + no unnamed functions (to improve debug experience) * fixes test according new eslint rules * removes arrow function expression eslint * adds Icon Or Image unit tests * removes problematic test * useSMock & sMock + reuses these mocks * adds StaticImage tests * adds UploadArea tests * adds tests for useAdministrationSettings * un-skips useCombinedHandler * tries to fix useObjectRef via correct mocking (failed github action) * adds useErrorDetails tests * rewrites useObjectRef tests so it works stable * unskips app-init & getTextFileReadingProcess * adds useFuzzySearch tests * adds useSearch tests * pr fixes after copilot review * self review fixes * pr fixes * introduces user events * uses get by role id to check accessibility of the components * fixes redefined focus in body * removes mocks * removes mocks 2 * removes mocks 3 * removes repeated test cases
This commit is contained in:
@@ -6,9 +6,9 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { WebSocket } from 'ws'
|
||||
import { WebSocket } from 'ws';
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
|
||||
// different machine has its own timezones and some tests can fail because of it
|
||||
process.env.TZ = 'UTC';
|
||||
globalThis.WebSocket = WebSocket as any
|
||||
globalThis.WebSocket = WebSocket as any;
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"@dbeaver/react-tests": "workspace:*",
|
||||
"@testing-library/jest-dom": "^6",
|
||||
"@testing-library/react": "^16",
|
||||
"@testing-library/user-event": "^14",
|
||||
"@types/react": "^19",
|
||||
"mobx": "^6",
|
||||
"react": "^19",
|
||||
|
||||
@@ -10,14 +10,6 @@ import { describe, expect, it, vi } from 'vitest';
|
||||
import { Cell } from './Cell.js';
|
||||
import { renderInApp } from '@cloudbeaver/tests-runner';
|
||||
|
||||
vi.mock('./s', () => ({
|
||||
s: (...args: any[]) => args.join(' '),
|
||||
}));
|
||||
|
||||
vi.mock('./useS', () => ({
|
||||
useS: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./Containers/Container', () => ({
|
||||
Container: (props: any) => <div>{props.children}</div>,
|
||||
}));
|
||||
|
||||
@@ -10,14 +10,6 @@ import { expect, describe, it, vi } from 'vitest';
|
||||
import { ErrorMessage } from './ErrorMessage.js';
|
||||
import { renderInApp } from '@cloudbeaver/tests-runner';
|
||||
|
||||
vi.mock('./s', () => ({
|
||||
s: (...args: any[]) => args.join(' '),
|
||||
}));
|
||||
|
||||
vi.mock('./useS', () => ({
|
||||
useS: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./localization/useTranslate', () => ({
|
||||
useTranslate: () => (key: string) => key,
|
||||
}));
|
||||
@@ -35,4 +27,9 @@ describe('ErrorMessage', () => {
|
||||
const { getByText } = renderInApp(<ErrorMessage text="error" />);
|
||||
await vi.waitFor(() => expect(getByText('error')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should have role="status"', async () => {
|
||||
const { getByRole } = renderInApp(<ErrorMessage text="error" />);
|
||||
await vi.waitFor(() => expect(getByRole('status')).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ interface Props {
|
||||
hasDetails?: boolean;
|
||||
text: string;
|
||||
className?: string;
|
||||
onShowDetails?: () => void;
|
||||
onShowDetails?: () => Promise<void> | void;
|
||||
}
|
||||
|
||||
export const ErrorMessage = observer<Props>(function ErrorMessage({ text, className, hasDetails, onShowDetails }) {
|
||||
|
||||
@@ -5,36 +5,27 @@
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import { afterAll, beforeAll, describe, expect, test } from 'vitest';
|
||||
|
||||
import { Icon } from './Icon.js';
|
||||
import { renderInApp } from '@cloudbeaver/tests-runner';
|
||||
|
||||
vi.mock('@cloudbeaver/core-utils', () => ({
|
||||
GlobalConstants: {
|
||||
absoluteUrl: (name: string) => name,
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Icon', () => {
|
||||
test('icons.svg#name', () => {
|
||||
(globalThis as any)._ROOT_URI_ = undefined;
|
||||
|
||||
const { getByTestId } = renderInApp(<Icon data-testid="Icon" name="test" />);
|
||||
expect(getByTestId('Icon').querySelector('use')).toHaveAttribute('href', '/icons/icons.svg#test');
|
||||
});
|
||||
|
||||
test('/image.jpg', () => {
|
||||
(globalThis as any)._ROOT_URI_ = undefined;
|
||||
|
||||
const { getByTestId } = renderInApp(<Icon data-testid="Icon" name="/image.jpg" />);
|
||||
expect(getByTestId('Icon').querySelector('use')).toHaveAttribute('href', '/image.jpg');
|
||||
});
|
||||
|
||||
test('{_ROOT_URI_}/icons.svg#name', () => {
|
||||
beforeAll(() => {
|
||||
(globalThis as any)._ROOT_URI_ = '/path/';
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
(globalThis as any)._ROOT_URI_ = undefined;
|
||||
});
|
||||
|
||||
test('should render with "icons.svg#name" path', () => {
|
||||
const { getByTestId } = renderInApp(<Icon data-testid="Icon" name="test" />);
|
||||
expect(getByTestId('Icon').querySelector('use')).toHaveAttribute('href', '/icons/icons.svg#test');
|
||||
expect(getByTestId('Icon').querySelector('use')).toHaveAttribute('href', '/path/icons/icons.svg#test');
|
||||
});
|
||||
|
||||
test('should render with custom absolute path', () => {
|
||||
const { getByTestId } = renderInApp(<Icon data-testid="Icon" name="/image.jpg" />);
|
||||
expect(getByTestId('Icon').querySelector('use')).toHaveAttribute('href', '/path/image.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 { fireEvent } from '@testing-library/react';
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { IconOrImage, type IconOrImageProps } from './IconOrImage.js';
|
||||
import { renderInApp } from '@cloudbeaver/tests-runner';
|
||||
|
||||
vi.mock('./Icon.js', () => ({
|
||||
Icon: vi.fn(({ ...props }) => (
|
||||
<svg {...props} role="svg">
|
||||
<use href={`/icons/icons.svg#${props.name}`} />
|
||||
</svg>
|
||||
)),
|
||||
}));
|
||||
|
||||
vi.mock('./StaticImage.js', () => ({
|
||||
StaticImage: vi.fn(({ ...props }) => <img {...props} />),
|
||||
}));
|
||||
|
||||
describe('IconOrImage', () => {
|
||||
test('should render StaticImage for platform: prefixed icon', () => {
|
||||
const { getByRole } = renderInApp(<IconOrImage icon="platform:/image.png" />);
|
||||
expect(getByRole('img')).toHaveAttribute('icon', 'platform:/image.png');
|
||||
});
|
||||
|
||||
test('should render StaticImage for / prefixed icon', () => {
|
||||
const { getByRole } = renderInApp(<IconOrImage icon="/image.jpg" />);
|
||||
expect(getByRole('img')).toHaveAttribute('icon', '/image.jpg');
|
||||
});
|
||||
|
||||
test('should render StaticImage for valid URL', () => {
|
||||
const { getByRole } = renderInApp(<IconOrImage icon="https://example.com/image.png" />);
|
||||
expect(getByRole('img')).toHaveAttribute('icon', 'https://example.com/image.png');
|
||||
});
|
||||
|
||||
test('should render Icon for regular icon name', () => {
|
||||
const { getByRole } = renderInApp(<IconOrImage icon="test" />);
|
||||
const svg = getByRole('svg');
|
||||
expect(svg).toHaveAttribute('name', 'test');
|
||||
});
|
||||
|
||||
test('should render Icon when svg prop is true even for platform: icon', () => {
|
||||
const { getByRole } = renderInApp(<IconOrImage icon="platform:/image.png" svg />);
|
||||
const svg = getByRole('svg');
|
||||
expect(svg).toHaveAttribute('name', 'platform:/image.png');
|
||||
});
|
||||
|
||||
test('should render Icon when svg prop is true even for URL', () => {
|
||||
const { getByRole } = renderInApp(<IconOrImage icon="https://example.com/image.png" svg />);
|
||||
const svg = getByRole('svg');
|
||||
expect(svg).toHaveAttribute('name', 'https://example.com/image.png');
|
||||
});
|
||||
|
||||
test('should pass all props to StaticImage', () => {
|
||||
const handleClick = vi.fn();
|
||||
const props: IconOrImageProps = {
|
||||
icon: '/image.jpg',
|
||||
className: 'custom-class',
|
||||
title: 'Test Title',
|
||||
width: 24,
|
||||
onClick: handleClick,
|
||||
};
|
||||
const { getByRole } = renderInApp(<IconOrImage {...props} />);
|
||||
const element = getByRole('img');
|
||||
|
||||
expect(element).toHaveClass(props.className!);
|
||||
expect(element).toHaveAttribute('title', props.title!);
|
||||
expect(element).toHaveAttribute('width', props.width!.toString());
|
||||
expect(element).toHaveAttribute('icon', props.icon);
|
||||
|
||||
fireEvent.click(element);
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should pass all props to Icon', () => {
|
||||
const handleClick = vi.fn();
|
||||
const props: IconOrImageProps = {
|
||||
icon: 'test',
|
||||
className: 'custom-class',
|
||||
viewBox: '0 0 24 24',
|
||||
width: 24,
|
||||
onClick: handleClick,
|
||||
};
|
||||
const { getByRole } = renderInApp(<IconOrImage {...props} />);
|
||||
const element = getByRole('svg');
|
||||
|
||||
expect(element).toHaveAttribute('name', props.icon);
|
||||
expect(element).toHaveClass(props.className!);
|
||||
expect(element).toHaveAttribute('viewBox', props.viewBox!);
|
||||
expect(element).toHaveAttribute('width', props.width!.toString());
|
||||
expect(element).toHaveAttribute('height', props.width!.toString());
|
||||
|
||||
fireEvent.click(element);
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should use default viewBox for Icon when not provided', () => {
|
||||
const { getByRole } = renderInApp(<IconOrImage icon="svg" />);
|
||||
const element = getByRole('svg');
|
||||
expect(element).toHaveAttribute('viewBox', '0 0 32 32');
|
||||
});
|
||||
});
|
||||
@@ -15,21 +15,14 @@ vi.mock('./IconOrImage', () => ({
|
||||
IconOrImage: (props: any) => <svg {...props}>{props.children}</svg>,
|
||||
}));
|
||||
|
||||
vi.mock('./s', () => ({
|
||||
s: (...args: any[]) => args.join(' '),
|
||||
}));
|
||||
|
||||
vi.mock('./useS', () => ({
|
||||
useS: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('Link', () => {
|
||||
it('should render link and children correctly', async () => {
|
||||
const { getByText } = renderInApp(<Link href="#">Test Link</Link>);
|
||||
const { getByText, getByRole } = renderInApp(<Link href="#">Test Link</Link>);
|
||||
const linkElement = await vi.waitFor(() => getByText('Test Link'));
|
||||
|
||||
expect(linkElement.tagName).toBe('A');
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
expect(getByRole('link')).toBe(linkElement);
|
||||
});
|
||||
|
||||
it('should display the indicator icon when indicator is true', () => {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 { fireEvent } from '@testing-library/react';
|
||||
import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest';
|
||||
import { renderInApp } from '@cloudbeaver/tests-runner';
|
||||
import { StaticImage } from './StaticImage.js';
|
||||
import * as sModule from './s.js';
|
||||
|
||||
describe('StaticImage', () => {
|
||||
beforeAll(() => {
|
||||
(globalThis as any)._ROOT_URI_ = '/absolute';
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
(globalThis as any)._ROOT_URI_ = undefined;
|
||||
});
|
||||
|
||||
test('should return null when icon is not provided', () => {
|
||||
const { container } = renderInApp(<StaticImage />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
test('should render img element when icon is provided', () => {
|
||||
const { getByRole } = renderInApp(<StaticImage icon="/image.png" />);
|
||||
const img = getByRole('img');
|
||||
expect(img).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should use valid URL as-is', () => {
|
||||
const validUrl = 'https://example.com/image.png';
|
||||
const { getByRole } = renderInApp(<StaticImage icon={validUrl} />);
|
||||
const img = getByRole('img');
|
||||
expect(img).toHaveAttribute('src', validUrl);
|
||||
});
|
||||
|
||||
test('should convert relative path to absolute URL', () => {
|
||||
const relativePath = '/icons/test.png';
|
||||
const { getByRole } = renderInApp(<StaticImage icon={relativePath} />);
|
||||
const img = getByRole('img');
|
||||
expect(img).toHaveAttribute('src', `/absolute${relativePath}`);
|
||||
});
|
||||
|
||||
test('should pass title prop as alt and title attributes', () => {
|
||||
const title = 'Test Image';
|
||||
const { getByRole } = renderInApp(<StaticImage icon="/image.png" title={title} />);
|
||||
const img = getByRole('img');
|
||||
expect(img).toHaveAttribute('alt', title);
|
||||
expect(img).toHaveAttribute('title', title);
|
||||
});
|
||||
|
||||
test('should pass width prop', () => {
|
||||
const width = 48;
|
||||
const { container } = renderInApp(<StaticImage icon="/image.png" width={width} />);
|
||||
const img = container.querySelector('img');
|
||||
expect(img).toHaveAttribute('width', width.toString());
|
||||
});
|
||||
|
||||
test('should pass className prop', () => {
|
||||
const className = 'custom-class';
|
||||
const { getByRole } = renderInApp(<StaticImage icon="/image.png" className={className} />);
|
||||
const img = getByRole('img');
|
||||
expect(img).toHaveClass(className);
|
||||
});
|
||||
|
||||
test('should handle onClick event', () => {
|
||||
const handleClick = vi.fn();
|
||||
const { getByRole } = renderInApp(<StaticImage icon="/image.png" onClick={handleClick} />);
|
||||
const img = getByRole('img');
|
||||
|
||||
fireEvent.click(img!);
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should pass additional img props', () => {
|
||||
const { getByRole } = renderInApp(<StaticImage icon="/image.png" id="test-id" loading="lazy" crossOrigin="anonymous" />);
|
||||
const img = getByRole('img');
|
||||
expect(img).toHaveAttribute('id', 'test-id');
|
||||
expect(img).toHaveAttribute('loading', 'lazy');
|
||||
expect(img).toHaveAttribute('crossOrigin', 'anonymous');
|
||||
});
|
||||
|
||||
test('should apply block class when block prop is true', () => {
|
||||
const sMock = vi.spyOn(sModule, 's');
|
||||
renderInApp(<StaticImage icon="/image.png" block />);
|
||||
expect(sMock).toHaveBeenCalledWith({}, { block: true }, undefined);
|
||||
});
|
||||
|
||||
test('should not apply block class when block prop is false', () => {
|
||||
const sMock = vi.spyOn(sModule, 's');
|
||||
renderInApp(<StaticImage icon="/image.png" block={false} />);
|
||||
expect(sMock).toHaveBeenCalledWith({}, { block: false }, undefined);
|
||||
});
|
||||
});
|
||||
@@ -18,10 +18,6 @@ vi.mock('./localization/useTranslate', () => ({
|
||||
useTranslate: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
vi.mock('./s', () => ({
|
||||
s: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./IconOrImage', () => ({
|
||||
IconOrImage: (props: any) => <svg {...props} />,
|
||||
}));
|
||||
@@ -30,10 +26,6 @@ vi.mock('./Link', () => ({
|
||||
Link: (props: any) => <a {...props} />,
|
||||
}));
|
||||
|
||||
vi.mock('./useS', () => ({
|
||||
useS: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./useErrorDetails', () => ({
|
||||
useErrorDetails: (exception: Error | null) => ({
|
||||
message: exception?.message || '',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -23,7 +23,7 @@ interface Props {
|
||||
exception?: Error | null;
|
||||
className?: string;
|
||||
multipleRows?: boolean;
|
||||
onShowDetails?: () => void;
|
||||
onShowDetails?: () => Promise<void> | void;
|
||||
}
|
||||
|
||||
export const StatusMessage = observer<Props>(function StatusMessage({
|
||||
|
||||
@@ -10,14 +10,6 @@ import { describe, expect, it, vi } from 'vitest';
|
||||
import { Text } from './Text.js';
|
||||
import { renderInApp } from '@cloudbeaver/tests-runner';
|
||||
|
||||
vi.mock('./s', () => ({
|
||||
s: (...args: any[]) => args.join(' '),
|
||||
}));
|
||||
|
||||
vi.mock('./useS', () => ({
|
||||
useS: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('Text Component', () => {
|
||||
it('renders children correctly', async () => {
|
||||
const { getByText } = renderInApp(<Text>Hello World</Text>);
|
||||
|
||||
@@ -10,14 +10,6 @@ import { describe, expect, it, vi } from 'vitest';
|
||||
import { TextPlaceholder } from './TextPlaceholder.js';
|
||||
import { renderInApp } from '@cloudbeaver/tests-runner';
|
||||
|
||||
vi.mock('./s', () => ({
|
||||
s: (...args: any[]) => args.join(' '),
|
||||
}));
|
||||
|
||||
vi.mock('./useS', () => ({
|
||||
useS: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('TextPlaceholder Component', () => {
|
||||
it('renders children correctly', async () => {
|
||||
const { getByText } = renderInApp(<TextPlaceholder>Hello World</TextPlaceholder>);
|
||||
|
||||
@@ -14,14 +14,6 @@ vi.mock('./Icon', () => ({
|
||||
Icon: (props: any) => <svg {...props}>Icon</svg>,
|
||||
}));
|
||||
|
||||
vi.mock('./s', () => ({
|
||||
s: (...args: any[]) => args.join(' '),
|
||||
}));
|
||||
|
||||
vi.mock('./useS', () => ({
|
||||
useS: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('TimerIcon', () => {
|
||||
it('renders correctly with state "play" and interval 30', async () => {
|
||||
const { getByText, getByTestId } = renderInApp(<TimerIcon state="play" data-testid="timer-icon" interval={30} />);
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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, vi } from 'vitest';
|
||||
import { createRef } from 'react';
|
||||
import { renderInApp } from '@cloudbeaver/tests-runner';
|
||||
import { UploadArea } from './UploadArea.js';
|
||||
|
||||
function createTestFileList(name = 'test.txt', content = 'content', type = 'text/plain') {
|
||||
const file = new File([content], name, { type });
|
||||
const fileList = {
|
||||
0: file,
|
||||
length: 1,
|
||||
item: (index: number) => (index === 0 ? file : null),
|
||||
*[Symbol.iterator]() {
|
||||
yield file;
|
||||
},
|
||||
} as unknown as FileList;
|
||||
return { file, fileList };
|
||||
}
|
||||
|
||||
describe('UploadArea', () => {
|
||||
it('should render children correctly', async () => {
|
||||
const { getByText } = renderInApp(<UploadArea>Upload File</UploadArea>);
|
||||
const label = await vi.waitFor(() => getByText('Upload File'));
|
||||
|
||||
expect(label).toBeInTheDocument();
|
||||
expect(label.tagName).toBe('LABEL');
|
||||
});
|
||||
|
||||
it('should render input file', () => {
|
||||
const { container } = renderInApp(<UploadArea>Upload File</UploadArea>);
|
||||
const input = container.querySelector('input[type="file"]');
|
||||
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveAttribute('type', 'file');
|
||||
});
|
||||
|
||||
it('should apply custom className correctly', async () => {
|
||||
const { getByText } = renderInApp(<UploadArea className="custom-class">Upload File</UploadArea>);
|
||||
const label = await vi.waitFor(() => getByText('Upload File'));
|
||||
|
||||
expect(label).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('should handle onChange event', async () => {
|
||||
const handleChange = vi.fn();
|
||||
const { container, user } = renderInApp(<UploadArea onChange={handleChange}>Upload File</UploadArea>);
|
||||
const input = container.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
|
||||
const { file } = createTestFileList();
|
||||
await user.upload(input, file);
|
||||
|
||||
expect(handleChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reset input value when reset is true', async () => {
|
||||
const handleChange = vi.fn();
|
||||
const { container, user } = renderInApp(
|
||||
<UploadArea reset onChange={handleChange}>
|
||||
Upload File
|
||||
</UploadArea>,
|
||||
);
|
||||
const input = container.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
|
||||
const { file } = createTestFileList();
|
||||
await user.upload(input, file);
|
||||
|
||||
expect(handleChange).toHaveBeenCalled();
|
||||
expect(input.value).toBe('');
|
||||
});
|
||||
|
||||
it('should apply disabled state correctly', () => {
|
||||
const { container } = renderInApp(<UploadArea disabled>Upload File</UploadArea>);
|
||||
const input = container.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
|
||||
expect(input).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should forward ref correctly', () => {
|
||||
const ref = createRef<HTMLInputElement>();
|
||||
renderInApp(<UploadArea ref={ref}>Upload File</UploadArea>);
|
||||
|
||||
expect(ref.current).toBeInstanceOf(HTMLInputElement);
|
||||
expect(ref.current?.type).toBe('file');
|
||||
});
|
||||
|
||||
it('should apply custom id when provided', () => {
|
||||
const customId = 'custom-upload-id';
|
||||
const { container } = renderInApp(<UploadArea id={customId}>Upload File</UploadArea>);
|
||||
const input = container.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
const label = container.querySelector('label');
|
||||
|
||||
expect(input).toHaveAttribute('id', customId);
|
||||
expect(label).toHaveAttribute('for', customId);
|
||||
});
|
||||
|
||||
it('should accept multiple files when multiple attribute is set', () => {
|
||||
const { container } = renderInApp(<UploadArea multiple>Upload Files</UploadArea>);
|
||||
const input = container.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
|
||||
expect(input).toHaveAttribute('multiple');
|
||||
});
|
||||
|
||||
it('should accept specific file types when accept attribute is set', () => {
|
||||
const { container } = renderInApp(<UploadArea accept=".jpg,.png">Upload Image</UploadArea>);
|
||||
const input = container.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
|
||||
expect(input).toHaveAttribute('accept', '.jpg,.png');
|
||||
});
|
||||
|
||||
it('should apply title attribute to label', () => {
|
||||
const { container } = renderInApp(<UploadArea title="Click to upload">Upload File</UploadArea>);
|
||||
const label = container.querySelector('label');
|
||||
|
||||
expect(label).toHaveAttribute('title', 'Click to upload');
|
||||
});
|
||||
|
||||
it('should set files property when value is provided', () => {
|
||||
const { fileList } = createTestFileList();
|
||||
const { container } = renderInApp(<UploadArea value={fileList}>Upload File</UploadArea>);
|
||||
const input = container.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
expect(input.files).toBe(fileList);
|
||||
});
|
||||
|
||||
it('should handle async onChange event', async () => {
|
||||
const handleChange = vi.fn(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const { container, user } = renderInApp(<UploadArea onChange={handleChange}>Upload File</UploadArea>);
|
||||
const input = container.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
|
||||
const { file } = createTestFileList();
|
||||
await user.upload(input, file);
|
||||
|
||||
expect(handleChange).toHaveBeenCalled();
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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, test, vitest } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { EAdminPermission, type ServerConfigResource } from '@cloudbeaver/core-root';
|
||||
|
||||
import { useAdministrationSettings } from './useAdministrationSettings.js';
|
||||
|
||||
vitest.mock('./ResourcesHooks/useResource', () => ({
|
||||
useResource: vitest.fn(),
|
||||
}));
|
||||
|
||||
vitest.mock('./usePermission', () => ({
|
||||
usePermission: vitest.fn(),
|
||||
}));
|
||||
|
||||
vitest.mock('@cloudbeaver/core-root', async importOriginal => {
|
||||
const actual = (await importOriginal()) as object;
|
||||
|
||||
return {
|
||||
...actual,
|
||||
ServerConfigResource: vitest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useAdministrationSettings', () => {
|
||||
let mockUsePermission: ReturnType<typeof vitest.fn>;
|
||||
let mockUseResource: ReturnType<typeof vitest.fn>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const { usePermission } = await import('./usePermission.js');
|
||||
const { useResource } = await import('./ResourcesHooks/useResource.js');
|
||||
|
||||
mockUsePermission = usePermission as ReturnType<typeof vitest.fn>;
|
||||
mockUseResource = useResource as ReturnType<typeof vitest.fn>;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vitest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should return credentialsSavingEnabled as true for admin users with admin save enabled', () => {
|
||||
mockUsePermission.mockReturnValue(true);
|
||||
mockUseResource.mockReturnValue({
|
||||
resource: {
|
||||
configurationMode: false,
|
||||
adminCredentialsSaveEnabled: true,
|
||||
publicCredentialsSaveEnabled: false,
|
||||
} as ServerConfigResource,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAdministrationSettings());
|
||||
|
||||
expect(result.current.credentialsSavingEnabled).toBe(true);
|
||||
});
|
||||
|
||||
test('should return credentialsSavingEnabled as false for non-admin users with public save disabled', () => {
|
||||
mockUsePermission.mockReturnValue(false);
|
||||
mockUseResource.mockReturnValue({
|
||||
resource: {
|
||||
configurationMode: false,
|
||||
adminCredentialsSaveEnabled: true,
|
||||
publicCredentialsSaveEnabled: false,
|
||||
} as ServerConfigResource,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAdministrationSettings());
|
||||
|
||||
expect(result.current.credentialsSavingEnabled).toBe(false);
|
||||
});
|
||||
|
||||
test('should return credentialsSavingEnabled as true for non-admin users with public save enabled', () => {
|
||||
mockUsePermission.mockReturnValue(false);
|
||||
mockUseResource.mockReturnValue({
|
||||
resource: {
|
||||
configurationMode: false,
|
||||
adminCredentialsSaveEnabled: true,
|
||||
publicCredentialsSaveEnabled: true,
|
||||
} as ServerConfigResource,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAdministrationSettings());
|
||||
|
||||
expect(result.current.credentialsSavingEnabled).toBe(true);
|
||||
});
|
||||
|
||||
test('should return credentialsSavingEnabled as true in configuration mode', () => {
|
||||
mockUsePermission.mockReturnValue(false);
|
||||
mockUseResource.mockReturnValue({
|
||||
resource: {
|
||||
configurationMode: true,
|
||||
adminCredentialsSaveEnabled: false,
|
||||
publicCredentialsSaveEnabled: false,
|
||||
} as ServerConfigResource,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAdministrationSettings());
|
||||
|
||||
expect(result.current.credentialsSavingEnabled).toBe(true);
|
||||
});
|
||||
|
||||
test('should return credentialsSavingEnabled as false when admin save is disabled', () => {
|
||||
mockUsePermission.mockReturnValue(false);
|
||||
mockUseResource.mockReturnValue({
|
||||
resource: {
|
||||
configurationMode: false,
|
||||
adminCredentialsSaveEnabled: false,
|
||||
publicCredentialsSaveEnabled: false,
|
||||
} as ServerConfigResource,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAdministrationSettings());
|
||||
|
||||
expect(result.current.credentialsSavingEnabled).toBe(false);
|
||||
});
|
||||
|
||||
test('should call usePermission with EAdminPermission.admin', () => {
|
||||
mockUsePermission.mockReturnValue(false);
|
||||
mockUseResource.mockReturnValue({
|
||||
resource: {
|
||||
configurationMode: false,
|
||||
adminCredentialsSaveEnabled: false,
|
||||
publicCredentialsSaveEnabled: false,
|
||||
} as ServerConfigResource,
|
||||
});
|
||||
|
||||
renderHook(() => useAdministrationSettings());
|
||||
|
||||
expect(mockUsePermission).toHaveBeenCalledWith(EAdminPermission.admin);
|
||||
});
|
||||
|
||||
test('should call useResource with ServerConfigResource', async () => {
|
||||
const { ServerConfigResource } = await import('@cloudbeaver/core-root');
|
||||
|
||||
mockUsePermission.mockReturnValue(false);
|
||||
mockUseResource.mockReturnValue({
|
||||
resource: {
|
||||
configurationMode: false,
|
||||
adminCredentialsSaveEnabled: false,
|
||||
publicCredentialsSaveEnabled: false,
|
||||
} as ServerConfigResource,
|
||||
});
|
||||
|
||||
renderHook(() => useAdministrationSettings());
|
||||
|
||||
expect(mockUseResource).toHaveBeenCalledWith(useAdministrationSettings, ServerConfigResource, undefined);
|
||||
});
|
||||
});
|
||||
@@ -10,11 +10,7 @@ import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { useCombinedHandler } from './useCombinedHandler.js';
|
||||
|
||||
vi.mock('./useObjectRef', () => ({
|
||||
useObjectRef: vi.fn(value => value),
|
||||
}));
|
||||
|
||||
describe.skip('useCombinedHandler', () => {
|
||||
describe('useCombinedHandler', () => {
|
||||
test('should call all provided handlers', () => {
|
||||
const handler1 = vi.fn();
|
||||
const handler2 = vi.fn();
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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, vitest } from 'vitest';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import * as coreDi from '@cloudbeaver/core-di';
|
||||
import { DetailsError } from '@cloudbeaver/core-sdk';
|
||||
import { LoadingError } from '@cloudbeaver/core-utils';
|
||||
import * as coreUtils from '@cloudbeaver/core-utils';
|
||||
import { useErrorDetails } from './useErrorDetails.js';
|
||||
// eslint-disable-next-line @cloudbeaver/no-sync-component-import
|
||||
import { ErrorDetailsDialog } from './ErrorDetailsDialog/ErrorDetailsDialog.js';
|
||||
|
||||
vitest.mock('@cloudbeaver/core-di', () => ({
|
||||
useService: vitest.fn(),
|
||||
}));
|
||||
|
||||
vitest.mock('./ErrorDetailsDialog/ErrorDetailsDialog.js', () => ({
|
||||
ErrorDetailsDialog: vitest.fn(),
|
||||
}));
|
||||
|
||||
vitest.mock('./localization/useTranslate', () => ({
|
||||
useTranslate: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
vitest.mock('@cloudbeaver/core-sdk', () => ({
|
||||
DetailsError: vitest.fn(Error),
|
||||
}));
|
||||
|
||||
vitest.mock('@cloudbeaver/core-dialogs', () => ({
|
||||
CommonDialogService: vitest.fn(),
|
||||
}));
|
||||
|
||||
class CommonDialogService {
|
||||
open = vitest.fn(() => Promise.resolve());
|
||||
}
|
||||
|
||||
const getMocks = () => {
|
||||
const commonDialogServiceMock = new CommonDialogService();
|
||||
const errorOfMock = vitest.fn();
|
||||
|
||||
vitest.spyOn(coreDi, 'useService').mockImplementation(() => commonDialogServiceMock);
|
||||
vitest.spyOn(coreUtils, 'errorOf').mockImplementation(errorOfMock);
|
||||
|
||||
return {
|
||||
commonDialogServiceMock,
|
||||
errorOfMock,
|
||||
};
|
||||
};
|
||||
|
||||
describe('useErrorDetails', () => {
|
||||
let mockErrorDetailsDialog: ReturnType<typeof vitest.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
vitest.clearAllMocks();
|
||||
mockErrorDetailsDialog = ErrorDetailsDialog as unknown as ReturnType<typeof vitest.fn>;
|
||||
});
|
||||
|
||||
it('should return null values when error is null', () => {
|
||||
const { errorOfMock } = getMocks();
|
||||
|
||||
errorOfMock.mockReturnValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useErrorDetails(null));
|
||||
|
||||
expect(result.current.error).toBeNull();
|
||||
expect(result.current.name).toBeUndefined();
|
||||
expect(result.current.message).toBeUndefined();
|
||||
expect(result.current.details).toBeUndefined();
|
||||
expect(result.current.hasDetails).toBe(false);
|
||||
expect(result.current.isOpen).toBe(false);
|
||||
expect(result.current.refresh).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle string error', () => {
|
||||
const { errorOfMock } = getMocks();
|
||||
|
||||
const errorMessage = 'Something went wrong';
|
||||
errorOfMock.mockReturnValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useErrorDetails(errorMessage));
|
||||
|
||||
expect(result.current.error).toBe(errorMessage);
|
||||
expect(result.current.name).toBe('core_blocks_exception_message_error_message');
|
||||
expect(result.current.message).toBe(errorMessage);
|
||||
expect(result.current.details).toBeUndefined();
|
||||
expect(result.current.hasDetails).toBe(false);
|
||||
expect(result.current.isOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle Error object', () => {
|
||||
const { errorOfMock } = getMocks();
|
||||
|
||||
const error = new Error('Test error');
|
||||
errorOfMock.mockReturnValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useErrorDetails(error));
|
||||
|
||||
expect(result.current.error).toBe(error);
|
||||
expect(result.current.name).toBe('Error');
|
||||
expect(result.current.message).toBe('Test error');
|
||||
expect(result.current.details).toBeUndefined();
|
||||
expect(result.current.hasDetails).toBe(false);
|
||||
expect(result.current.isOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('should extract DetailsError and check hasDetails', () => {
|
||||
const { errorOfMock } = getMocks();
|
||||
|
||||
const detailsError = new DetailsError('Error with details');
|
||||
detailsError.hasDetails = vitest.fn().mockReturnValue(true);
|
||||
|
||||
const error = new Error('Test error');
|
||||
|
||||
errorOfMock.mockImplementation((err, errorType) => {
|
||||
if (errorType === DetailsError) {
|
||||
return detailsError;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useErrorDetails(error));
|
||||
|
||||
expect(result.current.error).toBe(error);
|
||||
expect(result.current.details).toBe(detailsError);
|
||||
expect(result.current.hasDetails).toBe(true);
|
||||
expect(detailsError.hasDetails).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should extract LoadingError and provide refresh function', () => {
|
||||
const { errorOfMock } = getMocks();
|
||||
|
||||
const refreshMock = vitest.fn();
|
||||
const loadingError = new LoadingError(refreshMock);
|
||||
|
||||
const error = new Error('Test error');
|
||||
|
||||
errorOfMock.mockImplementation((err, errorType) => {
|
||||
if (errorType === LoadingError) {
|
||||
return loadingError;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useErrorDetails(error));
|
||||
|
||||
expect(result.current.error).toBe(error);
|
||||
expect(result.current.refresh).toBe(loadingError.refresh);
|
||||
});
|
||||
|
||||
it('should open error details dialog when open is called', async () => {
|
||||
const { errorOfMock, commonDialogServiceMock } = getMocks();
|
||||
|
||||
const error = new Error('Test error');
|
||||
errorOfMock.mockReturnValue(undefined);
|
||||
|
||||
commonDialogServiceMock.open.mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useErrorDetails(error));
|
||||
|
||||
expect(result.current.isOpen).toBe(false);
|
||||
|
||||
await result.current.open();
|
||||
|
||||
expect(commonDialogServiceMock.open).toHaveBeenCalledWith(mockErrorDetailsDialog, error);
|
||||
expect(result.current.isOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('should set isOpen to true while dialog is open', async () => {
|
||||
const { errorOfMock, commonDialogServiceMock } = getMocks();
|
||||
|
||||
const error = new Error('Test error');
|
||||
errorOfMock.mockReturnValue(undefined);
|
||||
|
||||
let resolveDialog: () => void;
|
||||
const dialogPromise = new Promise<void>(resolve => {
|
||||
resolveDialog = resolve;
|
||||
});
|
||||
|
||||
commonDialogServiceMock.open.mockReturnValue(dialogPromise);
|
||||
|
||||
const { result } = renderHook(() => useErrorDetails(error));
|
||||
|
||||
expect(result.current.isOpen).toBe(false);
|
||||
|
||||
const openPromise = result.current.open();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isOpen).toBe(true);
|
||||
});
|
||||
|
||||
expect(commonDialogServiceMock.open).toHaveBeenCalledWith(mockErrorDetailsDialog, error);
|
||||
|
||||
resolveDialog!();
|
||||
await openPromise;
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isOpen).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not open dialog when error is null', async () => {
|
||||
const { errorOfMock, commonDialogServiceMock } = getMocks();
|
||||
|
||||
errorOfMock.mockReturnValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useErrorDetails(null));
|
||||
|
||||
await result.current.open();
|
||||
|
||||
expect(commonDialogServiceMock.open).not.toHaveBeenCalled();
|
||||
expect(result.current.isOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle both DetailsError and LoadingError', () => {
|
||||
const { errorOfMock } = getMocks();
|
||||
|
||||
const detailsError = new DetailsError('Error with details');
|
||||
detailsError.hasDetails = vitest.fn().mockReturnValue(true);
|
||||
|
||||
const refreshMock = vitest.fn();
|
||||
const loadingError = new LoadingError(refreshMock);
|
||||
|
||||
const error = new Error('Test error');
|
||||
|
||||
errorOfMock.mockImplementation((err, errorType) => {
|
||||
if (errorType === DetailsError) {
|
||||
return detailsError;
|
||||
}
|
||||
if (errorType === LoadingError) {
|
||||
return loadingError;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useErrorDetails(error));
|
||||
|
||||
expect(result.current.details).toBe(detailsError);
|
||||
expect(result.current.hasDetails).toBe(true);
|
||||
expect(result.current.refresh).toBe(loadingError.refresh);
|
||||
});
|
||||
|
||||
it('should reset isOpen to false even if dialog throws error', async () => {
|
||||
const { errorOfMock, commonDialogServiceMock } = getMocks();
|
||||
|
||||
const error = new Error('Test error');
|
||||
errorOfMock.mockReturnValue(undefined);
|
||||
|
||||
const dialogError = new Error('Dialog error');
|
||||
commonDialogServiceMock.open.mockRejectedValue(dialogError);
|
||||
|
||||
const { result } = renderHook(() => useErrorDetails(error));
|
||||
|
||||
await expect(result.current.open()).rejects.toThrow('Dialog error');
|
||||
|
||||
expect(result.current.isOpen).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,7 +22,7 @@ interface IErrorDetailsHook {
|
||||
details?: DetailsError;
|
||||
hasDetails: boolean;
|
||||
isOpen: boolean;
|
||||
open: () => void;
|
||||
open: () => Promise<void>;
|
||||
refresh?: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,6 @@ import { renderHook } from '@testing-library/react';
|
||||
import type { IExecutorHandler, IExecutorHandlersCollection } from '@cloudbeaver/core-executor';
|
||||
|
||||
import { useExecutor } from './useExecutor.js';
|
||||
import { useObjectRef } from './useObjectRef.js';
|
||||
|
||||
vitest.mock('@cloudbeaver/core-executor', () => ({}));
|
||||
vitest.mock('./useObjectRef', () => ({
|
||||
useObjectRef: vitest.fn(obj => obj),
|
||||
}));
|
||||
|
||||
describe('useExecutor', () => {
|
||||
let mockExecutor: IExecutorHandlersCollection<any>;
|
||||
@@ -119,15 +113,4 @@ describe('useExecutor', () => {
|
||||
expect(mockExecutor.addHandler).not.toHaveBeenCalled();
|
||||
expect(mockExecutor.addPostHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should use useObjectRef', () => {
|
||||
const options = {
|
||||
executor: mockExecutor,
|
||||
handlers: [vitest.fn()],
|
||||
};
|
||||
|
||||
renderHook(() => useExecutor(options));
|
||||
|
||||
expect(useObjectRef).toHaveBeenCalledWith(options);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { useFuzzySearch } from './useFuzzySearch.js';
|
||||
|
||||
interface TestItem {
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
const mockSearchResults = vi.fn();
|
||||
const mockIsIndexing = vi.fn();
|
||||
const mockRemoveAll = vi.fn();
|
||||
const mockAddAll = vi.fn();
|
||||
const mockSearch = vi.fn();
|
||||
const mockClearSearch = vi.fn();
|
||||
|
||||
vi.mock('react-minisearch', () => ({
|
||||
useMiniSearch: vi.fn(() => ({
|
||||
searchResults: mockSearchResults(),
|
||||
isIndexing: mockIsIndexing(),
|
||||
removeAll: mockRemoveAll,
|
||||
addAll: mockAddAll,
|
||||
search: mockSearch,
|
||||
clearSearch: mockClearSearch,
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('useFuzzySearch', () => {
|
||||
const testData: TestItem[] = [
|
||||
{ name: 'Apple', description: 'A red fruit', category: 'fruit' },
|
||||
{ name: 'Apricot', description: 'A sweet fruit', category: 'fruit' },
|
||||
{ name: 'Carrot', description: 'An orange vegetable', category: 'vegetable' },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
mockSearchResults.mockClear();
|
||||
mockIsIndexing.mockClear();
|
||||
mockRemoveAll.mockClear();
|
||||
mockAddAll.mockClear();
|
||||
mockSearch.mockClear();
|
||||
mockClearSearch.mockClear();
|
||||
|
||||
mockSearchResults.mockReturnValue(null);
|
||||
mockIsIndexing.mockReturnValue(false);
|
||||
});
|
||||
|
||||
test('should initialize with empty search results', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFuzzySearch({
|
||||
sourceProposals: testData,
|
||||
fields: ['name', 'description'],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.searchResult).toBeNull();
|
||||
expect(result.current.isIndexing).toBe(false);
|
||||
});
|
||||
|
||||
test('should call search method with correct query', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFuzzySearch({
|
||||
sourceProposals: testData,
|
||||
fields: ['name'],
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.search('Apple');
|
||||
|
||||
expect(mockSearch).toHaveBeenCalledWith('Apple');
|
||||
expect(mockSearch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should support searching across multiple fields', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFuzzySearch({
|
||||
sourceProposals: testData,
|
||||
fields: ['name', 'description'],
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.search('orange');
|
||||
|
||||
expect(mockSearch).toHaveBeenCalledWith('orange');
|
||||
});
|
||||
|
||||
test('should call search with prefix query', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFuzzySearch({
|
||||
sourceProposals: testData,
|
||||
fields: ['name'],
|
||||
prefix: true,
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.search('Ap');
|
||||
|
||||
expect(mockSearch).toHaveBeenCalledWith('Ap');
|
||||
});
|
||||
|
||||
test('should call clearSearch method', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFuzzySearch({
|
||||
sourceProposals: testData,
|
||||
fields: ['name'],
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.search('Apple');
|
||||
result.current.clearSearch();
|
||||
|
||||
expect(mockClearSearch).toHaveBeenCalled();
|
||||
expect(mockClearSearch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should call removeAll and addAll when source proposals change', () => {
|
||||
const { rerender } = renderHook(
|
||||
({ proposals }) =>
|
||||
useFuzzySearch({
|
||||
sourceProposals: proposals,
|
||||
fields: ['name'],
|
||||
}),
|
||||
{
|
||||
initialProps: {
|
||||
proposals: testData,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
mockRemoveAll.mockClear();
|
||||
mockAddAll.mockClear();
|
||||
|
||||
const newData: TestItem[] = [...testData, { name: 'Grape', description: 'A purple fruit', category: 'fruit' }];
|
||||
|
||||
rerender({ proposals: newData });
|
||||
|
||||
expect(mockRemoveAll).toHaveBeenCalled();
|
||||
expect(mockAddAll).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should handle empty source proposals', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFuzzySearch({
|
||||
sourceProposals: [],
|
||||
fields: ['name'],
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.search('test');
|
||||
|
||||
expect(mockSearch).toHaveBeenCalledWith('test');
|
||||
});
|
||||
|
||||
test('should handle search with empty query', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFuzzySearch({
|
||||
sourceProposals: testData,
|
||||
fields: ['name'],
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.search('');
|
||||
|
||||
expect(mockSearch).toHaveBeenCalledWith('');
|
||||
});
|
||||
|
||||
test('should use useObservableRef to make state observable', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFuzzySearch({
|
||||
sourceProposals: testData,
|
||||
fields: ['name', 'description'],
|
||||
}),
|
||||
);
|
||||
|
||||
// Verify the returned state has all required properties
|
||||
expect(result.current).toHaveProperty('searchResult');
|
||||
expect(result.current).toHaveProperty('isIndexing');
|
||||
expect(result.current).toHaveProperty('removeAll');
|
||||
expect(result.current).toHaveProperty('addAll');
|
||||
expect(result.current).toHaveProperty('search');
|
||||
expect(result.current).toHaveProperty('clearSearch');
|
||||
});
|
||||
});
|
||||
@@ -6,118 +6,155 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { describe, expect, test, vitest } from 'vitest';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import * as coreUtils from '@cloudbeaver/core-utils';
|
||||
|
||||
import { useObjectRef } from './useObjectRef.js';
|
||||
|
||||
vitest.mock('@cloudbeaver/core-utils', () => ({
|
||||
bindFunctions: vitest.fn(),
|
||||
}));
|
||||
interface ITestObject {
|
||||
value: number;
|
||||
text?: string;
|
||||
getValue(): number;
|
||||
setValue(value: number): void;
|
||||
}
|
||||
|
||||
function createTestObject(): ITestObject {
|
||||
return {
|
||||
value: 1,
|
||||
text: 'initial',
|
||||
getValue() {
|
||||
return this.value;
|
||||
},
|
||||
setValue(value: number) {
|
||||
this.value = value;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('useObjectRef', () => {
|
||||
test('should initialize', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useObjectRef({
|
||||
count: 0,
|
||||
increment: function (this: { count: number }) {
|
||||
this.count++;
|
||||
},
|
||||
}),
|
||||
test('returns stable object reference between renders', () => {
|
||||
const { result, rerender } = renderHook(() => useObjectRef(createTestObject));
|
||||
|
||||
const firstRef = result.current;
|
||||
rerender();
|
||||
const secondRef = result.current;
|
||||
|
||||
expect(firstRef).toBe(secondRef);
|
||||
});
|
||||
|
||||
test('initializes from object literal and keeps same reference', () => {
|
||||
const initObject: ITestObject = createTestObject();
|
||||
|
||||
const { result, rerender } = renderHook(() => useObjectRef(initObject));
|
||||
|
||||
const firstRef = result.current;
|
||||
rerender();
|
||||
|
||||
expect(result.current).toBe(firstRef);
|
||||
// internal state should keep same reference between renders
|
||||
expect(result.current.value).toBe(1);
|
||||
});
|
||||
|
||||
test('applies update object on each render', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ value }: { value: number }) =>
|
||||
useObjectRef(
|
||||
() => ({
|
||||
value: 0,
|
||||
}),
|
||||
{
|
||||
value,
|
||||
},
|
||||
),
|
||||
{
|
||||
initialProps: { value: 1 },
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current.count).toBe(0);
|
||||
expect(typeof result.current.increment).toBe('function');
|
||||
expect(result.current.value).toBe(1);
|
||||
|
||||
rerender({ value: 5 });
|
||||
|
||||
expect(result.current.value).toBe(5);
|
||||
});
|
||||
|
||||
test('should initialize with empty object', () => {
|
||||
const { result } = renderHook(() => useObjectRef({}));
|
||||
test('binds specified methods to object when initialized', () => {
|
||||
const { result } = renderHook(() => useObjectRef(createTestObject, false, ['getValue', 'setValue']));
|
||||
|
||||
expect(result.current).toEqual({});
|
||||
const ref = result.current;
|
||||
const getValue = ref.getValue;
|
||||
const setValue = ref.setValue;
|
||||
|
||||
// if binding works, `this` inside methods should point to the ref object
|
||||
expect(getValue()).toBe(1);
|
||||
|
||||
setValue(10);
|
||||
expect(ref.value).toBe(10);
|
||||
expect(getValue()).toBe(10);
|
||||
});
|
||||
|
||||
test('should bind ref functions', () => {
|
||||
const bindFunctions = vitest.spyOn(coreUtils, 'bindFunctions');
|
||||
test('binds methods when using bind array as second arg', () => {
|
||||
const { result } = renderHook(() => useObjectRef(createTestObject, ['getValue', 'setValue']));
|
||||
|
||||
renderHook(() =>
|
||||
useObjectRef(
|
||||
() => ({
|
||||
count: 0,
|
||||
increment: function (this: { count: number }) {
|
||||
this.count++;
|
||||
},
|
||||
}),
|
||||
false,
|
||||
['increment'],
|
||||
),
|
||||
);
|
||||
const ref = result.current;
|
||||
const getValue = ref.getValue;
|
||||
const setValue = ref.setValue;
|
||||
|
||||
expect(bindFunctions).toHaveBeenCalledTimes(1);
|
||||
bindFunctions.mockClear();
|
||||
expect(getValue()).toBe(1);
|
||||
setValue(3);
|
||||
expect(getValue()).toBe(3);
|
||||
});
|
||||
|
||||
test('should merge update to bind', () => {
|
||||
const bindFunctions = vitest.spyOn(coreUtils, 'bindFunctions');
|
||||
|
||||
renderHook(() =>
|
||||
useObjectRef(
|
||||
() => ({
|
||||
count: 0,
|
||||
increment: function (this: { count: number }) {
|
||||
this.count++;
|
||||
},
|
||||
}),
|
||||
{
|
||||
count: 0,
|
||||
},
|
||||
['increment'],
|
||||
),
|
||||
);
|
||||
|
||||
expect(bindFunctions).toHaveBeenCalledTimes(1);
|
||||
bindFunctions.mockClear();
|
||||
});
|
||||
|
||||
test('should update ref via initial state method', () => {
|
||||
type TRef = { update: { count: number; increment?: (this: { count: number }) => void } };
|
||||
const init = () => ({ count: 0 });
|
||||
|
||||
const { result } = renderHook(({ update }: TRef) => useObjectRef(init, update, ['increment']), {
|
||||
initialProps: {
|
||||
update: {
|
||||
count: 0,
|
||||
increment: function () {
|
||||
this.count++;
|
||||
},
|
||||
},
|
||||
test('rebinds only methods present in update object', () => {
|
||||
const create = () => ({
|
||||
value: 1,
|
||||
other: 1,
|
||||
getValue() {
|
||||
return this.value;
|
||||
},
|
||||
getOther() {
|
||||
return this.other;
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.current.count).toBe(0);
|
||||
const { result, rerender } = renderHook(
|
||||
({ value }: { value: number }) =>
|
||||
useObjectRef(
|
||||
create,
|
||||
{
|
||||
value,
|
||||
},
|
||||
['getValue', 'getOther'],
|
||||
),
|
||||
{ initialProps: { value: 1 } },
|
||||
);
|
||||
|
||||
result.current?.increment?.();
|
||||
const ref = result.current;
|
||||
const getValue = ref.getValue;
|
||||
const getOther = ref.getOther;
|
||||
|
||||
expect(result.current.count).toBe(1);
|
||||
expect(getValue()).toBe(1);
|
||||
expect(getOther()).toBe(1);
|
||||
|
||||
rerender({ value: 10 });
|
||||
|
||||
// getValue should be rebound to updated "value", getOther should still work with old binding
|
||||
expect(ref.value).toBe(10);
|
||||
expect(getValue()).toBe(10);
|
||||
expect(getOther()).toBe(1);
|
||||
});
|
||||
|
||||
test('should update ref', () => {
|
||||
type TRef = { update: { count: number } };
|
||||
const init = () => ({ count: 0 });
|
||||
test('supports partial init and update', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ text }: { text: string }) => useObjectRef<ITestObject, Partial<ITestObject>>(() => ({ value: 1 }) as ITestObject, { text }),
|
||||
{ initialProps: { text: 'first' } },
|
||||
);
|
||||
|
||||
const { result, rerender } = renderHook(({ update }: TRef) => useObjectRef(init, update), {
|
||||
initialProps: {
|
||||
update: {
|
||||
count: 0,
|
||||
},
|
||||
} as TRef,
|
||||
});
|
||||
expect(result.current.value).toBe(1);
|
||||
expect(result.current.text).toBe('first');
|
||||
|
||||
expect(result.current.count).toBe(0);
|
||||
rerender({ text: 'second' });
|
||||
|
||||
rerender({ update: { count: 3 } });
|
||||
|
||||
expect(result.current.count).toBe(3);
|
||||
expect(result.current.text).toBe('second');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,16 +8,10 @@
|
||||
|
||||
import { describe, expect, test, vitest } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { action, computed, isObservable, observable, runInAction } from 'mobx';
|
||||
|
||||
import { computed, isObservable, observable, runInAction } from 'mobx';
|
||||
import * as coreUtils from '@cloudbeaver/core-utils';
|
||||
|
||||
import { useObservableRef } from './useObservableRef.js';
|
||||
|
||||
vitest.mock('@cloudbeaver/core-utils', () => ({
|
||||
bindFunctions: vitest.fn(),
|
||||
}));
|
||||
|
||||
describe('useObservableRef', () => {
|
||||
test('should initialize with a function', () => {
|
||||
const init = () => ({ count: 0 });
|
||||
@@ -42,7 +36,7 @@ describe('useObservableRef', () => {
|
||||
test('should bind functions', () => {
|
||||
const bindFunctions = vitest.spyOn(coreUtils, 'bindFunctions');
|
||||
|
||||
const observed = { count: observable, increment: action };
|
||||
const observed = { count: observable };
|
||||
|
||||
renderHook(() =>
|
||||
useObservableRef(
|
||||
@@ -102,7 +96,7 @@ describe('useObservableRef', () => {
|
||||
this.count++;
|
||||
},
|
||||
});
|
||||
const observed = { count: observable, increment: action };
|
||||
const observed = { count: observable };
|
||||
const update = ['increment'];
|
||||
|
||||
renderHook(() => useObservableRef(init, observed, update));
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { useSearch } from './useSearch.js';
|
||||
|
||||
interface TestItem {
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
const useFuzzySearchSpy = vi.fn();
|
||||
|
||||
const mockFuzzySearch = vi.fn();
|
||||
let mockSearchResult: any = null;
|
||||
|
||||
vi.mock('./useFuzzySearch', () => ({
|
||||
useFuzzySearch: vi.fn((config: any) => {
|
||||
useFuzzySearchSpy(config);
|
||||
return {
|
||||
search: mockFuzzySearch,
|
||||
get searchResult() {
|
||||
return mockSearchResult;
|
||||
},
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('useSearch', () => {
|
||||
const testData: TestItem[] = [
|
||||
{ name: 'Apple', description: 'A red fruit', category: 'fruit' },
|
||||
{ name: 'Apricot', description: 'A sweet fruit', category: 'fruit' },
|
||||
{ name: 'Carrot', description: 'An orange vegetable', category: 'vegetable' },
|
||||
{ name: 'Banana', description: 'A yellow fruit', category: 'fruit' },
|
||||
{ name: 'Avocado', description: 'A green fruit', category: 'fruit' },
|
||||
{ name: 'Pineapple', description: 'A tropical fruit', category: 'fruit' },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
useFuzzySearchSpy.mockClear();
|
||||
mockFuzzySearch.mockClear();
|
||||
mockSearchResult = null;
|
||||
});
|
||||
|
||||
describe('Initialization', () => {
|
||||
test('should initialize with correct default values and observed state', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSearch({
|
||||
sourceHints: testData,
|
||||
searchFields: ['name', 'description'],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.searchResult).toEqual([]);
|
||||
});
|
||||
|
||||
test('should initialize fuzzy search when matchStrategy is fuzzy', () => {
|
||||
renderHook(() =>
|
||||
useSearch({
|
||||
sourceHints: testData,
|
||||
searchFields: ['name'],
|
||||
matchStrategy: 'fuzzy',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(useFuzzySearchSpy).toHaveBeenCalledWith({
|
||||
sourceProposals: testData,
|
||||
fields: ['name'],
|
||||
});
|
||||
});
|
||||
|
||||
test('should use contains strategy by default', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSearch({
|
||||
sourceHints: testData,
|
||||
searchFields: ['name'],
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.setSearch('app');
|
||||
expect(result.current.searchResult).toHaveLength(2);
|
||||
expect(result.current.searchResult.map(item => item.name)).toContain('Apple');
|
||||
expect(result.current.searchResult.map(item => item.name)).toContain('Pineapple');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Search Strategy: startsWith', () => {
|
||||
test('should filter items that start with search term', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSearch({
|
||||
sourceHints: testData,
|
||||
searchFields: ['name'],
|
||||
matchStrategy: 'startsWith',
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.setSearch('Ap');
|
||||
expect(result.current.searchResult).toHaveLength(2);
|
||||
expect(result.current.searchResult.map(item => item.name)).toContain('Apple');
|
||||
expect(result.current.searchResult.map(item => item.name)).toContain('Apricot');
|
||||
});
|
||||
|
||||
test('should be case insensitive', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSearch({
|
||||
sourceHints: testData,
|
||||
searchFields: ['name'],
|
||||
matchStrategy: 'startsWith',
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.setSearch('ap');
|
||||
expect(result.current.searchResult).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('should not match items that do not start with search term', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSearch({
|
||||
sourceHints: testData,
|
||||
searchFields: ['name'],
|
||||
matchStrategy: 'startsWith',
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.setSearch('na');
|
||||
expect(result.current.searchResult).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Search Strategy: contains', () => {
|
||||
test('should filter items that contain search term', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSearch({
|
||||
sourceHints: testData,
|
||||
searchFields: ['name'],
|
||||
matchStrategy: 'contains',
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.setSearch('an');
|
||||
expect(result.current.searchResult).toHaveLength(1);
|
||||
expect(result.current.searchResult[0]!.name).toBe('Banana');
|
||||
});
|
||||
|
||||
test('should search in multiple fields', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSearch({
|
||||
sourceHints: testData,
|
||||
searchFields: ['name', 'description'],
|
||||
matchStrategy: 'contains',
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.setSearch('orange');
|
||||
expect(result.current.searchResult).toHaveLength(1);
|
||||
expect(result.current.searchResult[0]!.name).toBe('Carrot');
|
||||
});
|
||||
|
||||
test('should be case insensitive', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSearch({
|
||||
sourceHints: testData,
|
||||
searchFields: ['description'],
|
||||
matchStrategy: 'contains',
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.setSearch('FRUIT');
|
||||
expect(result.current.searchResult.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Search Strategy: fuzzy', () => {
|
||||
test('should call fuzzy search on setSearch', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSearch({
|
||||
sourceHints: testData,
|
||||
searchFields: ['name'],
|
||||
matchStrategy: 'fuzzy',
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.setSearch('aple');
|
||||
expect(mockFuzzySearch).toHaveBeenCalledWith('aple');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Predicate Function', () => {
|
||||
test('should filter results using custom predicate', () => {
|
||||
const predicate = (item: TestItem) => item.category === 'fruit';
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSearch({
|
||||
sourceHints: testData,
|
||||
searchFields: ['name'],
|
||||
matchStrategy: 'contains',
|
||||
predicate,
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.setSearch('a');
|
||||
const fruitItems = result.current.searchResult.filter(item => item.category === 'fruit');
|
||||
expect(fruitItems).toEqual(result.current.searchResult);
|
||||
});
|
||||
|
||||
test('should exclude items when predicate returns false', () => {
|
||||
const predicate = () => false;
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSearch({
|
||||
sourceHints: testData,
|
||||
searchFields: ['name'],
|
||||
matchStrategy: 'contains',
|
||||
predicate,
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.setSearch('a');
|
||||
expect(result.current.searchResult).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
test('should handle empty search string', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSearch({
|
||||
sourceHints: testData,
|
||||
searchFields: ['name'],
|
||||
matchStrategy: 'contains',
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.setSearch('');
|
||||
expect(result.current.searchResult).toEqual([]);
|
||||
});
|
||||
|
||||
test('should handle empty source hints', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSearch({
|
||||
sourceHints: [],
|
||||
searchFields: ['name'],
|
||||
matchStrategy: 'contains',
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.setSearch('test');
|
||||
expect(result.current.searchResult).toEqual([]);
|
||||
});
|
||||
|
||||
test('should exclude exact matches', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSearch({
|
||||
sourceHints: testData,
|
||||
searchFields: ['name'],
|
||||
matchStrategy: 'contains',
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.setSearch('Apple');
|
||||
expect(result.current.searchResult.map(item => item.name)).not.toContain('Apple');
|
||||
});
|
||||
|
||||
test('should handle non-string field values', () => {
|
||||
const dataWithMixedTypes = [
|
||||
{ name: 'Test', description: 'A test item', category: 'test' },
|
||||
{ name: 123 as any, description: 'Number name', category: 'test' },
|
||||
{ name: null as any, description: 'Null name', category: 'test' },
|
||||
];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSearch({
|
||||
sourceHints: dataWithMixedTypes,
|
||||
searchFields: ['name'],
|
||||
matchStrategy: 'contains',
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.setSearch('tes');
|
||||
expect(result.current.searchResult).toHaveLength(1);
|
||||
expect(result.current.searchResult[0]!.name).toBe('Test');
|
||||
});
|
||||
|
||||
test('should handle search term with different casing for exact match', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSearch({
|
||||
sourceHints: testData,
|
||||
searchFields: ['name'],
|
||||
matchStrategy: 'contains',
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.setSearch('apple');
|
||||
expect(result.current.searchResult.map(item => item.name.toLowerCase())).not.toContain('apple');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,7 @@ import { TestBootstrap } from './TestBootstrap.js';
|
||||
import { TestService } from './TestService.js';
|
||||
import testModule from './module.js';
|
||||
|
||||
test.skip('App Initialization', async () => {
|
||||
test('App Initialization', async () => {
|
||||
const app = new App([testModule]);
|
||||
|
||||
await (app as any).registerServices();
|
||||
|
||||
@@ -21,12 +21,12 @@ describe('copyToClipboard', () => {
|
||||
});
|
||||
|
||||
it('should focus on active element after copy', () => {
|
||||
document.body.focus = vitest.fn();
|
||||
const focusSpy = vitest.spyOn(document.body, 'focus');
|
||||
|
||||
copyToClipboard('test');
|
||||
|
||||
expect(document.activeElement).toBe(document.body);
|
||||
expect(document.body.focus).toHaveBeenCalled();
|
||||
expect(focusSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should have no children after copy', () => {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { isImageBroken } from './isBrokenImage.js';
|
||||
|
||||
export type { Options as IScreenshotOptions };
|
||||
|
||||
const BROKEN_IMAGE_ERROR_MESSAGE = 'Something went wrong. please try select another file format';
|
||||
const BROKEN_IMAGE_ERROR_MESSAGE = 'Something went wrong. Please try to select another file format';
|
||||
|
||||
export async function downloadSvg<T extends Node>(element: T, options: Options, fileName: string): Promise<void> {
|
||||
const svg = await modernScreenshot.domToForeignObjectSvg<T>(element, options);
|
||||
|
||||
@@ -5,20 +5,20 @@
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vitest, type MockInstance } from 'vitest';
|
||||
import { consoleSpy, addKnownError } from '@cloudbeaver/tests-runner';
|
||||
import { afterEach, beforeEach, describe, expect, it, vitest, type MockInstance } from 'vitest';
|
||||
import { addKnownError, initKnownConsoleMessages } from '@cloudbeaver/tests-runner';
|
||||
|
||||
import { getTextFileReadingProcess } from './getTextFileReadingProcess.js';
|
||||
|
||||
beforeAll(() => {
|
||||
addKnownError(/Error: Read error/);
|
||||
});
|
||||
|
||||
describe('getTextFileReadingProcess', () => {
|
||||
let file: File;
|
||||
let mockFileReader: Partial<FileReader>;
|
||||
let fileReaderMock: MockInstance<(this: FileReader) => FileReader>;
|
||||
|
||||
initKnownConsoleMessages(() => {
|
||||
addKnownError(/Error: Read error/);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
file = new File(['file content'], 'test.txt', { type: 'text/plain' });
|
||||
|
||||
@@ -68,7 +68,8 @@ describe('getTextFileReadingProcess', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it.skip('should reject with an error if reading fails', async () => {
|
||||
it('should reject with an error if reading fails', async () => {
|
||||
const { consoleSpy } = await import('@cloudbeaver/tests-runner');
|
||||
const { promise } = getTextFileReadingProcess(file);
|
||||
const error = new Error('Read error');
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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, vi } from 'vitest';
|
||||
|
||||
import { isImageBroken } from './isBrokenImage.js';
|
||||
|
||||
describe('isImageBroken', () => {
|
||||
const OriginalImage = globalThis.Image;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (OriginalImage) {
|
||||
globalThis.Image = OriginalImage;
|
||||
} else {
|
||||
delete (globalThis as Record<string, unknown>)['Image'];
|
||||
}
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should return false if image loads successfully', async () => {
|
||||
const srcSpy = vi.fn();
|
||||
|
||||
class MockImage {
|
||||
public onload: (() => void) | null = null;
|
||||
public onerror: (() => void) | null = null;
|
||||
|
||||
set src(value: string) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const thisRef = this;
|
||||
srcSpy(value);
|
||||
setTimeout(() => {
|
||||
thisRef.onload?.();
|
||||
}, 0);
|
||||
vi.runAllTimers();
|
||||
}
|
||||
}
|
||||
|
||||
vi.stubGlobal('Image', MockImage);
|
||||
|
||||
await expect(isImageBroken('https://example.com/ok.png')).resolves.toBe(false);
|
||||
expect(srcSpy).toHaveBeenCalledWith('https://example.com/ok.png');
|
||||
});
|
||||
|
||||
it('should return true if image loading fails', async () => {
|
||||
const srcSpy = vi.fn();
|
||||
|
||||
class MockImage {
|
||||
public onload: (() => void) | null = null;
|
||||
public onerror: (() => void) | null = null;
|
||||
|
||||
set src(value: string) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const thisRef = this;
|
||||
srcSpy(value);
|
||||
setTimeout(() => {
|
||||
thisRef.onerror?.();
|
||||
}, 0);
|
||||
vi.runAllTimers();
|
||||
}
|
||||
}
|
||||
|
||||
vi.stubGlobal('Image', MockImage);
|
||||
|
||||
await expect(isImageBroken('https://example.com/broken.png')).resolves.toBe(true);
|
||||
expect(srcSpy).toHaveBeenCalledWith('https://example.com/broken.png');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 } from 'vitest';
|
||||
|
||||
import { isIp } from './isIp.js';
|
||||
|
||||
describe('isIp', () => {
|
||||
it('should return true for correct IPv4', () => {
|
||||
expect(isIp('127.0.0.1')).toBe(true);
|
||||
expect(isIp('192.168.1.100')).toBe(true);
|
||||
expect(isIp('0.0.0.0')).toBe(true);
|
||||
expect(isIp('255.255.255.255')).toBe(true);
|
||||
expect(isIp('10.0.0.1')).toBe(true);
|
||||
expect(isIp('172.16.0.1')).toBe(true);
|
||||
expect(isIp('8.8.8.8')).toBe(true);
|
||||
expect(isIp('1.1.1.1')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for incorrect IPv4', () => {
|
||||
expect(isIp('256.0.0.1')).toBe(false);
|
||||
expect(isIp('192.168.1')).toBe(false);
|
||||
expect(isIp('192.168.1.256')).toBe(false);
|
||||
expect(isIp('192.168.-1.1')).toBe(false);
|
||||
expect(isIp('192.168.1.1.1')).toBe(false);
|
||||
expect(isIp('192.168.a.1')).toBe(false);
|
||||
expect(isIp('999.999.999.999')).toBe(false);
|
||||
expect(isIp('192.168.1.')).toBe(false);
|
||||
expect(isIp('.192.168.1.1')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for correct IPv6 with and without brackets', () => {
|
||||
expect(isIp('[2001:0db8::1]')).toBe(true);
|
||||
expect(isIp('2001:0db8:85a3:0000:0000:8a2e:0370:7334')).toBe(true);
|
||||
expect(isIp('::1')).toBe(true);
|
||||
expect(isIp('[::]')).toBe(true);
|
||||
expect(isIp('[::1]')).toBe(true);
|
||||
expect(isIp('fe80::1')).toBe(true);
|
||||
expect(isIp('[fe80::1]')).toBe(true);
|
||||
expect(isIp('2001:db8::8a2e:370:7334')).toBe(true);
|
||||
expect(isIp('[2001:db8::8a2e:370:7334]')).toBe(true);
|
||||
expect(isIp('::ffff:192.0.2.1')).toBe(true);
|
||||
expect(isIp('2001:db8::')).toBe(true);
|
||||
expect(isIp('::2001:db8')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for incorrect IPv6 or host', () => {
|
||||
expect(isIp('[2001:::1::]')).toBe(false);
|
||||
expect(isIp('example.com')).toBe(false);
|
||||
expect(isIp('')).toBe(false);
|
||||
expect(isIp('::::')).toBe(false);
|
||||
expect(isIp('2001:db8::g')).toBe(false);
|
||||
expect(isIp('[2001:db8::2')).toBe(false);
|
||||
expect(isIp('2001:db8::2]')).toBe(false);
|
||||
expect(isIp('2001:db8::2[')).toBe(false);
|
||||
expect(isIp('gggg::1')).toBe(false);
|
||||
expect(isIp('2001:db8:85a3::8a2e:370k:7334')).toBe(false);
|
||||
expect(isIp('[2001:db8:::]')).toBe(false);
|
||||
expect(isIp('2001:db8:::1')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle surrounding whitespace gracefully', () => {
|
||||
expect(isIp(' 10.0.0.5 ')).toBe(true);
|
||||
expect(isIp('\t[2001:db8::2]\n')).toBe(true);
|
||||
expect(isIp(' 192.168.1.1 ')).toBe(true);
|
||||
expect(isIp('\n::1\t')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-IP strings', () => {
|
||||
expect(isIp('localhost')).toBe(false);
|
||||
expect(isIp('hostname')).toBe(false);
|
||||
expect(isIp('example.com')).toBe(false);
|
||||
expect(isIp('www.example.com')).toBe(false);
|
||||
expect(isIp('not-an-ip')).toBe(false);
|
||||
expect(isIp('123')).toBe(false);
|
||||
expect(isIp('abc.def.ghi.jkl')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for edge cases and special inputs', () => {
|
||||
expect(isIp('')).toBe(false);
|
||||
expect(isIp(' ')).toBe(false);
|
||||
expect(isIp('null')).toBe(false);
|
||||
expect(isIp('undefined')).toBe(false);
|
||||
expect(isIp('...')).toBe(false);
|
||||
expect(isIp(':::')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -6,12 +6,53 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
const IPV4_REGEX = /^(25[0-5]|2[0-4]\d|1\d\d|\d\d?)\.(25[0-5]|2[0-4]\d|1\d\d|\d\d?)\.(25[0-5]|2[0-4]\d|1\d\d|\d\d?)\.(25[0-5]|2[0-4]\d|1\d\d|\d\d?)$/;
|
||||
|
||||
export function isIp(host: string): boolean {
|
||||
// Matches IPv4 like 127.0.0.1
|
||||
const ipv4 = /^(25[0-5]|2[0-4]\d|1\d\d|\d\d?)\.(25[0-5]|2[0-4]\d|1\d\d|\d\d?)\.(25[0-5]|2[0-4]\d|1\d\d|\d\d?)\.(25[0-5]|2[0-4]\d|1\d\d|\d\d?)$/;
|
||||
if (!host) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Matches IPv6 like [2001:0db8::1]
|
||||
const ipv6 = /^\[?([a-f0-9:]+)\]?$/i;
|
||||
const trimmedHost = host.trim();
|
||||
|
||||
return ipv4.test(host) || ipv6.test(host);
|
||||
return isIPv4(trimmedHost) || isIPv6(trimmedHost);
|
||||
}
|
||||
|
||||
function isIPv4(host: string): boolean {
|
||||
return IPV4_REGEX.test(host);
|
||||
}
|
||||
|
||||
function isIPv6(host: string): boolean {
|
||||
const candidate = normalizeIPv6(host);
|
||||
|
||||
if (!candidate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(`http://${candidate}`);
|
||||
|
||||
return url.hostname.startsWith('[') && url.hostname.endsWith(']');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeIPv6(host: string): string | null {
|
||||
const startsWithBracket = host.startsWith('[');
|
||||
const endsWithBracket = host.endsWith(']');
|
||||
|
||||
if (startsWithBracket !== endsWithBracket) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (startsWithBracket) {
|
||||
return host;
|
||||
}
|
||||
|
||||
if (host.includes('[') || host.includes(']')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `[${host}]`;
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ export default defineConfig(
|
||||
'no-inner-declarations': 'off',
|
||||
'no-constant-condition': 'off',
|
||||
'space-before-blocks': 'error',
|
||||
'func-names': ['error', 'always'],
|
||||
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||||
'no-unsafe-finally': 'error',
|
||||
'require-await': 'error',
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"@testing-library/dom": "^10",
|
||||
"@testing-library/jest-dom": "^6",
|
||||
"@testing-library/react": "^16",
|
||||
"@testing-library/user-event": "^14",
|
||||
"mobx": "^6",
|
||||
"react": "^19",
|
||||
"react-dom": "^19",
|
||||
|
||||
@@ -11,6 +11,8 @@ import { Suspense } from 'react';
|
||||
|
||||
import { type IServiceProvider, ServiceProvider } from '@cloudbeaver/core-di';
|
||||
|
||||
import { userEvent, type UserEvent } from '@testing-library/user-event';
|
||||
|
||||
import type { IApplication } from './createApp.js';
|
||||
|
||||
function resetDocument() {
|
||||
@@ -27,6 +29,12 @@ function ApplicationWrapper(serviceInjector: IServiceProvider): React.FC<React.P
|
||||
);
|
||||
}
|
||||
|
||||
type App<
|
||||
Q extends Queries = typeof queries,
|
||||
Container extends Element | DocumentFragment = HTMLElement,
|
||||
BaseElement extends Element | DocumentFragment = Container,
|
||||
> = RenderResult<Q, Container, BaseElement> & { user: ReturnType<UserEvent['setup']> };
|
||||
|
||||
// TODO move it to the common-react/@dbeaver/react-tests packages
|
||||
export function renderInApp<
|
||||
Q extends Queries = typeof queries,
|
||||
@@ -36,11 +44,19 @@ export function renderInApp<
|
||||
ui: React.ReactElement,
|
||||
options: Omit<RenderOptions<Q, Container, BaseElement>, 'queries' | 'wrapper'> = {},
|
||||
app?: IApplication,
|
||||
): RenderResult<Q, Container, BaseElement> {
|
||||
): App<Q, Container, BaseElement> {
|
||||
resetDocument();
|
||||
const user = userEvent.setup();
|
||||
|
||||
if (!app) {
|
||||
return render(ui, options);
|
||||
return {
|
||||
...render(ui, options),
|
||||
user,
|
||||
} as App<Q, Container, BaseElement>;
|
||||
}
|
||||
|
||||
return render(ui, { wrapper: ApplicationWrapper(app.serviceProvider), ...options });
|
||||
return {
|
||||
...render(ui, { wrapper: ApplicationWrapper(app.serviceProvider), ...options }),
|
||||
user,
|
||||
} as App<Q, Container, BaseElement>;
|
||||
}
|
||||
|
||||
@@ -1490,6 +1490,7 @@ __metadata:
|
||||
"@dbeaver/ui-kit": "workspace:*"
|
||||
"@testing-library/jest-dom": "npm:^6"
|
||||
"@testing-library/react": "npm:^16"
|
||||
"@testing-library/user-event": "npm:^14"
|
||||
"@types/react": "npm:^19"
|
||||
go-split: "npm:^3"
|
||||
minisearch: "npm:^7"
|
||||
@@ -4599,6 +4600,7 @@ __metadata:
|
||||
"@testing-library/dom": "npm:^10"
|
||||
"@testing-library/jest-dom": "npm:^6"
|
||||
"@testing-library/react": "npm:^16"
|
||||
"@testing-library/user-event": "npm:^14"
|
||||
"@types/node": "npm:^22"
|
||||
"@types/react": "npm:^19"
|
||||
mobx: "npm:^6"
|
||||
|
||||
Reference in New Issue
Block a user