diff --git a/webapp/packages/core-utils/src/GlobalConstants.test.ts b/webapp/packages/core-utils/src/GlobalConstants.test.ts new file mode 100644 index 0000000000..c2ae017301 --- /dev/null +++ b/webapp/packages/core-utils/src/GlobalConstants.test.ts @@ -0,0 +1,129 @@ +/* + * 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 { GlobalConstants } from './GlobalConstants'; + +jest.mock('./isValidUrl', () => ({ + isValidUrl: jest.fn().mockReturnValue(false), +})); + +jest.mock('./pathJoin', () => ({ + pathJoin: jest.fn((...args: string[]) => args.reduce((acc, arg) => acc + arg, '')), +})); + +describe('GlobalConstants', () => { + let windowSpy: jest.SpyInstance; + + beforeEach(() => { + windowSpy = jest.spyOn(window, 'window', 'get'); + }); + + afterEach(() => { + windowSpy.mockRestore(); + }); + + beforeEach(() => { + (global as any)._DEV_ = true; + (global as any)._VERSION_ = '1.0.0'; + (global as any)._ROOT_URI_ = '{ROOT_URI}'; + + windowSpy.mockImplementation( + () => + ({ + location: { + protocol: 'http:', + host: 'localhost', + }, + }) as any, + ); + }); + + it('should return correct dev value', () => { + expect(GlobalConstants.dev).toBe(true); + }); + + it('should return correct version value', () => { + expect(GlobalConstants.version).toBe('1.0.0'); + }); + + it('should return correct protocol value', () => { + expect(GlobalConstants.protocol).toBe('http:'); + }); + + it('should return correct host value', () => { + expect(GlobalConstants.host).toBe('localhost'); + }); + + it('should return correct websocket protocol', () => { + expect(GlobalConstants.wsProtocol).toBe('ws:'); + + windowSpy.mockImplementationOnce( + () => + ({ + location: { + protocol: 'https:', + }, + }) as any, + ); + + expect(GlobalConstants.wsProtocol).toBe('wss:'); + }); + + it('should return correct rootURI value', () => { + expect(GlobalConstants.rootURI).toBe('/'); + + (global as any)._ROOT_URI_ = 'http://localhost:8080'; + (require('./isValidUrl').isValidUrl as jest.Mock).mockReturnValueOnce(true); + + expect(GlobalConstants.rootURI).toBe('/'); + + (global as any)._ROOT_URI_ = '/dbeaver'; + expect(GlobalConstants.rootURI).toBe('/dbeaver/'); + }); + + it('should return correct serviceURI value', () => { + expect(GlobalConstants.serviceURI).toBe('/api'); + }); + + it('should return correct health check url', () => { + expect(GlobalConstants.getHealthCheckUrl('http://localhost')).toBe('http://localhost/status'); + expect(GlobalConstants.getHealthCheckUrl('')).toBe('/status'); + }); + + it('should generate absolute root url', () => { + expect(GlobalConstants.absoluteRootUrl('test/', 'test2')).toBe('/test/test2'); + }); + + it('should generate absolute service url', () => { + expect(GlobalConstants.absoluteServiceUrl('/test/', 'test2')).toBe('/api/test/test2'); + }); + + it('should generate absoluteServiceHTTPUrl', () => { + expect(GlobalConstants.absoluteServiceHTTPUrl('/test/', 'test2')).toBe('http://localhost/api/test/test2'); + }); + + it('should generate absoluteServiceWSUrl', () => { + expect(GlobalConstants.absoluteServiceWSUrl('/test/', 'test2')).toBe('ws://localhost/api/test/test2'); + + windowSpy.mockImplementationOnce( + () => + ({ + location: { + protocol: 'https:', + host: 'localhost', + }, + }) as any, + ); + + expect(GlobalConstants.absoluteServiceWSUrl('/test/', 'test2')).toBe('wss://localhost/api/test/test2'); + }); + + it('should generate absolute url', () => { + expect(GlobalConstants.absoluteUrl('test/', 'test2')).toBe('/test/test2'); + expect(GlobalConstants.absoluteUrl('platform:test/', 'test2')).toBe('/apiimagesplatform:test/test2'); + }); +}); diff --git a/webapp/packages/core-utils/src/ILoadableState.test.ts b/webapp/packages/core-utils/src/ILoadableState.test.ts new file mode 100644 index 0000000000..7d141c08f0 --- /dev/null +++ b/webapp/packages/core-utils/src/ILoadableState.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 { getFirstException, ILoadableState, isContainsException, isLoadableStateHasException } from './ILoadableState'; + +const getMockedLoadableState = (state: Partial): ILoadableState => ({ + exception: null, + isLoading: () => false, + isLoaded: () => false, + isError: () => false, + load: () => undefined, + ...state, +}); + +describe('isContainsException', () => { + it('should return true if exception is present', () => { + expect(isContainsException(new Error())).toBe(true); + }); + + it('should return false if exception is not present', () => { + expect(isContainsException(null)).toBe(false); + }); + + it('should check array of exceptions', () => { + expect(isContainsException([null, new Error()])).toBe(true); + expect(isContainsException([null, null])).toBe(false); + expect(isContainsException([])).toBe(false); + }); +}); + +describe('getFirstException', () => { + it('should return first exception', () => { + const error = new Error(); + const error2 = new Error('error2'); + + expect(getFirstException([null, error, error2])).toBe(error); + expect(getFirstException([null, error])).toBe(error); + expect(getFirstException([error])).toBe(error); + expect(getFirstException(error)).toBe(error); + }); + + it('should not get exception', () => { + expect(getFirstException([null, null])).toBe(null); + expect(getFirstException([null])).toBe(null); + expect(getFirstException([])).toBe(null); + expect(getFirstException(null)).toBe(null); + }); +}); + +describe('isLoadableStateHasException', () => { + it('should return true if exception is present', () => { + expect( + isLoadableStateHasException( + getMockedLoadableState({ + exception: [new Error(), new Error('error2')], + isError() { + return true; + }, + }), + ), + ).toBe(true); + + expect( + isLoadableStateHasException( + getMockedLoadableState({ + exception: [new Error()], + isError() { + return true; + }, + }), + ), + ).toBe(true); + + expect( + isLoadableStateHasException( + getMockedLoadableState({ + exception: new Error(), + isError() { + return true; + }, + }), + ), + ).toBe(true); + + expect( + isLoadableStateHasException( + getMockedLoadableState({ + exception: [null, new Error()], + isError() { + return true; + }, + }), + ), + ).toBe(true); + }); + + it('should return false if exception is not present', () => { + expect( + isLoadableStateHasException( + getMockedLoadableState({ + exception: [], + isError() { + return false; + }, + }), + ), + ).toBe(false); + + expect( + isLoadableStateHasException( + getMockedLoadableState({ + exception: null, + isError() { + return false; + }, + }), + ), + ).toBe(false); + + expect( + isLoadableStateHasException( + getMockedLoadableState({ + exception: [null, null], + isError() { + return false; + }, + }), + ), + ).toBe(false); + }); +}); diff --git a/webapp/packages/core-utils/src/ILoadableState.ts b/webapp/packages/core-utils/src/ILoadableState.ts index 31db51562f..455f5ce063 100644 --- a/webapp/packages/core-utils/src/ILoadableState.ts +++ b/webapp/packages/core-utils/src/ILoadableState.ts @@ -21,10 +21,6 @@ export interface ILoadableState { cancel?: () => void; } -// export function composeLoadableState(...states: ILoadableState[]): ILoadableState { - -// } - export function isLoadableStateHasException(state: ILoadableState): boolean { return isContainsException(state.exception); } diff --git a/webapp/packages/core-utils/src/MetadataMap.test.ts b/webapp/packages/core-utils/src/MetadataMap.test.ts new file mode 100644 index 0000000000..e3e8417fd4 --- /dev/null +++ b/webapp/packages/core-utils/src/MetadataMap.test.ts @@ -0,0 +1,219 @@ +/* + * 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 { z } from 'zod'; + +import { MetadataMap } from './MetadataMap'; + +describe('MetadataMap', () => { + it('should create an empty map', () => { + const map = new MetadataMap(); + + expect(map.size).toBe(0); + }); + + it('should sync items', () => { + const map = new MetadataMap(); + const emptyMap = new MetadataMap(); + + const data: [number, string][] = [ + [1, 'one'], + [2, 'two'], + [3, 'three'], + [Infinity, 'infinity'], + [NaN, 'nan'], + ]; + + map.sync(data); + emptyMap.sync([]); + + data.forEach(([key, value]) => { + expect(map.get(key)).toBe(value); + }); + + expect(emptyMap.size).toBe(0); + }); + + it('should set items', () => { + const map = new MetadataMap(); + map.set(1, 'one'); + map.set(2, 'two'); + map.set(3, 'three'); + map.set(Infinity, 'infinity'); + map.set(NaN, 'nan'); + + expect(map.get(1)).toBe('one'); + expect(map.get(2)).toBe('two'); + expect(map.get(3)).toBe('three'); + expect(map.get(Infinity)).toBe('infinity'); + expect(map.get(NaN)).toBe('nan'); + }); + + it('should throw an error if validation failed and no default value getter', () => { + const map = new MetadataMap(); + map.sync([ + [1, 'one'], + [2, 'two'], + [3, 'three'], + ]); + + expect(() => map.get(1, undefined, z.object({}))).toThrow(); + }); + + it('should get function default value if validation failed', () => { + const map = new MetadataMap(); + map.sync([ + [1, 'one'], + [2, 'two'], + [3, 'three'], + ]); + + expect(map.get(1, key => `default ${key}`, z.object({}))).toBe('default 1'); + }); + + it('should return default value for non existing element using default getter in constructor', () => { + const map = new MetadataMap(key => `default ${key}`); + + expect(map.get(1)).toBe('default 1'); + expect(map.get(Infinity)).toBe('default Infinity'); + expect(map.get(NaN)).toBe('default NaN'); + }); + + it('should return size', () => { + const map = new MetadataMap(); + map.sync([ + [1, 'one'], + [2, 'two'], + [3, 'three'], + ]); + + expect(map.size).toBe(3); + }); + + it('should return iterator', () => { + const map = new MetadataMap(); + const data: [number, string][] = [ + [1, 'one'], + [2, 'two'], + [3, 'three'], + ]; + map.sync(data); + + for (const [key, value] of map) { + expect(map.get(key)).toBe(value); + } + }); + + it('should iterate keys', () => { + const map = new MetadataMap(); + const data: [number, string][] = [ + [1, 'one'], + [2, 'two'], + [3, 'three'], + ]; + const keys = data.map(([key]) => key); + + map.sync(data); + + for (const key of map.keys()) { + expect(key).toBe(keys.shift()); + } + }); + + it('should iterate values', () => { + const map = new MetadataMap(); + const data: [number, string][] = [ + [1, 'one'], + [2, 'two'], + [3, 'three'], + ]; + const values = data.map(([, value]) => value); + map.sync(data); + + for (const value of map.values()) { + expect(value).toBe(values.shift()); + } + }); + + it('should iterate entries', () => { + const map = new MetadataMap(); + const data: [number, string][] = [ + [1, 'one'], + [2, 'two'], + [3, 'three'], + ]; + map.sync(data); + + for (const [key, value] of map.entries()) { + expect([key, value]).toStrictEqual(data.shift()); + } + }); + + it('should check if key exists', () => { + const map = new MetadataMap(); + map.sync([ + [1, 'one'], + [2, 'two'], + [3, 'three'], + ]); + + expect(map.has(1)).toBeTruthy(); + expect(map.has(2)).toBeTruthy(); + expect(map.has(3)).toBeTruthy(); + expect(map.has(4)).toBeFalsy(); + }); + + it('should clear', () => { + const map = new MetadataMap(); + map.sync([ + [1, 'one'], + [2, 'two'], + [3, 'three'], + ]); + + map.clear(); + + expect(map.size).toBe(0); + }); + + it('should delete items', () => { + const map = new MetadataMap(); + const data: [number, string][] = [ + [1, 'one'], + [2, 'two'], + [3, 'three'], + ]; + + map.sync(data); + + map.delete(2); + map.delete(4); + data.splice(1, 1); + + expect(map.has(2)).toBeFalsy(); + expect(Array.from(map)).toEqual(data); + }); + + it('should forEach', () => { + const map = new MetadataMap(); + map.sync([ + [1, 'one'], + [2, 'two'], + [3, 'three'], + ]); + + map.forEach((value, key) => { + expect(map.get(key)).toBe(value); + }); + }); + + it('has to string tag', () => { + const map = new MetadataMap(); + + expect(map[Symbol.toStringTag]).toBe('MetadataMap'); + }); +}); diff --git a/webapp/packages/core-utils/src/MetadataMap.ts b/webapp/packages/core-utils/src/MetadataMap.ts index 7f31c47c37..1ab435c3f0 100644 --- a/webapp/packages/core-utils/src/MetadataMap.ts +++ b/webapp/packages/core-utils/src/MetadataMap.ts @@ -75,6 +75,8 @@ export class MetadataMap implements Map { return this; } + // TODO replace zod schema with just validation callback returning true/false. + // In case we use something else than zod get(key: TKey, defaultValue?: DefaultValueGetter, schema?: schema.AnyZodObject): TValue { const value = this.temp.get(key); let invalidate = !this.temp.has(key); @@ -95,7 +97,9 @@ export class MetadataMap implements Map { } const value = provider(key, this); - this.temp.set(key, observable(value as any)); + const isNotPrimitiveValue = typeof value === 'object' && value !== null; + + this.temp.set(key, isNotPrimitiveValue ? observable(value) : value); } return this.temp.get(key)!; diff --git a/webapp/packages/core-utils/src/Promises/CancellablePromise.test.ts b/webapp/packages/core-utils/src/Promises/CancellablePromise.test.ts new file mode 100644 index 0000000000..6567d21db3 --- /dev/null +++ b/webapp/packages/core-utils/src/Promises/CancellablePromise.test.ts @@ -0,0 +1,52 @@ +/* + * 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 { CancellablePromise } from './CancellablePromise'; +import { PromiseCancelledError } from './PromiseCancelledError'; + +describe('CancellablePromise', () => { + jest.mock('./PromiseCancelledError', () => ({ + PromiseCancelledError: jest.fn(), + })); + + it('cancels promise', async () => { + const PromiseCancelledErrorMockInstance = new PromiseCancelledError(); + const promise = new CancellablePromise(resolve => { + const token = setTimeout(() => resolve(), 0); + return () => { + clearTimeout(token); + }; + }); + + promise.cancel(); + + await expect(promise).rejects.toThrow(PromiseCancelledErrorMockInstance); + }); + + it('should resolve promise', async () => { + const promise = new CancellablePromise(resolve => { + const token = setTimeout(() => resolve(777), 0); + return () => { + clearTimeout(token); + }; + }); + + await expect(promise).resolves.toBe(777); + }); + + it('should reject promise', async () => { + const error = new Error('test'); + const promise = new CancellablePromise((resolve, reject) => { + const token = setTimeout(() => reject(error), 0); + return () => { + clearTimeout(token); + }; + }); + + await expect(promise).rejects.toThrow(error); + }); +}); diff --git a/webapp/packages/core-utils/src/Promises/cancellableTimeout.test.ts b/webapp/packages/core-utils/src/Promises/cancellableTimeout.test.ts new file mode 100644 index 0000000000..e2dae82cba --- /dev/null +++ b/webapp/packages/core-utils/src/Promises/cancellableTimeout.test.ts @@ -0,0 +1,31 @@ +/* + * 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 { cancellableTimeout } from './cancellableTimeout'; + +jest.mock('./CancellablePromise', () => ({ + CancellablePromise: jest.fn().mockImplementation(() => ({ + cancel: jest.fn(), + })), +})); + +describe('cancellableTimeout', () => { + jest.useFakeTimers(); + + it('resolves after the specified timeout', async () => { + const timeout = 0; + const start = Date.now(); + + const promise = cancellableTimeout(timeout); + + await promise; + + jest.advanceTimersByTime(timeout); + + expect(Date.now() - start).toBe(timeout); + }); +}); diff --git a/webapp/packages/core-utils/src/Promises/cancellableTimeout.ts b/webapp/packages/core-utils/src/Promises/cancellableTimeout.ts new file mode 100644 index 0000000000..3b20a14c59 --- /dev/null +++ b/webapp/packages/core-utils/src/Promises/cancellableTimeout.ts @@ -0,0 +1,17 @@ +/* + * 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 { CancellablePromise } from './CancellablePromise'; + +export function cancellableTimeout(timeout: number): CancellablePromise { + return new CancellablePromise(resolve => { + const token = setTimeout(() => resolve(), timeout); + return () => { + clearTimeout(token); + }; + }); +} diff --git a/webapp/packages/core-utils/src/Promises/deferPromise.ts b/webapp/packages/core-utils/src/Promises/deferPromise.ts deleted file mode 100644 index 36b8eda9a9..0000000000 --- a/webapp/packages/core-utils/src/Promises/deferPromise.ts +++ /dev/null @@ -1,35 +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 { CancellablePromise } from './CancellablePromise'; - -type PromiseExecutor = (resolve: (value: T) => void, reject: (reason?: any) => void) => void; - -/** - * wait timeout milliseconds then start to execute promise. - * During the timeout the execution of promise can be cancelled. - * - * @param executor - * @param timeout - */ -export function deferPromise(executor: PromiseExecutor, timeout: number): CancellablePromise { - return new CancellablePromise((resolve, reject) => { - const token = setTimeout(() => executor(resolve, reject), timeout); - return () => { - clearTimeout(token); - }; - }); -} - -export function cancellableTimeout(timeout: number): CancellablePromise { - return new CancellablePromise(resolve => { - const token = setTimeout(() => resolve(), timeout); - return () => { - clearTimeout(token); - }; - }); -} diff --git a/webapp/packages/core-utils/src/TempMap.test.ts b/webapp/packages/core-utils/src/TempMap.test.ts new file mode 100644 index 0000000000..a26134b192 --- /dev/null +++ b/webapp/packages/core-utils/src/TempMap.test.ts @@ -0,0 +1,283 @@ +/* + * 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 { TempMap } from './TempMap'; + +describe('TempMap', () => { + it('should create empty map', () => { + const target = new Map(); + const map = new TempMap(target); + + expect(map.size).toBe(0); + expect(target.size).toBe(0); + expect(Array.from(map.entries()).length).toBe(0); + }); + + it('should set and get values', () => { + const map = new TempMap(new Map()); + + map.set('test', 'test value'); + map.set('test2', 'test value2'); + + expect(map.get('test')).toBe('test value'); + expect(map.get('test2')).toBe('test value2'); + }); + + it('should delete values', () => { + const map = new TempMap(new Map()); + + map.set('test', 'test value'); + map.delete('test'); + + expect(map.get('test')).toBeUndefined(); + expect(map.isDeleted('test')).toBe(true); + }); + + it('should clear values', () => { + const map = new TempMap(new Map()); + + map.set('test', 'test value'); + map.set('test2', 'test value2'); + map.clear(); + + expect(map.get('test')).toBeUndefined(); + expect(map.get('test2')).toBeUndefined(); + }); + + it('should delete deleted values after map being cleared', () => { + const map = new TempMap(new Map()); + + map.set('test', 'test value'); + map.delete('test'); + expect(map.isDeleted('test')).toBe(true); + map.clear(); + + expect(map.get('test')).toBeUndefined(); + expect(map.isDeleted('test')).toBe(false); + }); + + it('should remove item from deleted map if it was set again', () => { + const map = new TempMap(new Map()); + + map.set('test', 'test value'); + map.delete('test'); + + expect(map.isDeleted('test')).toBe(true); + + map.set('test', 'test value2'); + + expect(map.get('test')).toBe('test value2'); + expect(map.isDeleted('test')).toBe(false); + }); + + it('should have for each items', () => { + const map = new TempMap(new Map()); + + map.set('test', 'test value'); + map.set('test2', 'test value2'); + + const values: string[] = []; + const keys: string[] = []; + map.forEach((value, key) => { + values.push(value); + keys.push(key); + }); + + expect(values).toEqual(['test value', 'test value2']); + expect(keys).toEqual(['test', 'test2']); + }); + + it('should for each items and not include deleted', () => { + const map = new TempMap(new Map()); + + map.set('test', 'test value'); + map.set('test2', 'test value2'); + map.delete('test'); + + const values: string[] = []; + const keys: string[] = []; + map.forEach((value, key) => { + values.push(value); + keys.push(key); + }); + + expect(values).toEqual(['test value2']); + expect(keys).toEqual(['test2']); + expect(map.isDeleted('test')).toBe(true); + }); + + it('should check if key exists in the map', () => { + const map = new TempMap(new Map()); + + map.set('test', 'test value'); + map.set('test2', 'test value2'); + map.delete('test'); + + expect(map.has('test')).toBe(false); + expect(map.has('test2')).toBe(true); + }); + + it('should get keys', () => { + const map = new TempMap(new Map()); + + map.set('test', 'test value'); + map.set('test2', 'test value2'); + + expect(Array.from(map.keys())).toEqual(['test', 'test2']); + }); + + it('should get values', () => { + const map = new TempMap(new Map()); + + map.set('test', 'test value'); + map.set('test2', 'test value2'); + + expect(Array.from(map.values())).toEqual(['test value', 'test value2']); + }); + + it('should get entries', () => { + const map = new TempMap(new Map()); + + map.set('test', 'test value'); + map.set('test2', 'test value2'); + + expect(Array.from(map.entries())).toEqual([ + ['test', 'test value'], + ['test2', 'test value2'], + ]); + }); + + it('should get size', () => { + const map = new TempMap(new Map()); + + map.set('test', 'test value'); + map.set('test2', 'test value2'); + + expect(map.size).toBe(2); + }); + + it('should only delete target value', () => { + const target = new Map(); + const map = new TempMap(target); + + map.set('test', 'test value'); + target.delete('test'); + + expect(map.get('test')).toBe('test value'); + expect(target.get('test')).toBeUndefined(); + }); + + it('should set target value and also be available in temp map', () => { + const target = new Map(); + const map = new TempMap(target); + + target.set('test', 'test value2'); + + expect(map.get('test')).toBe('test value2'); + expect(target.get('test')).toBe('test value2'); + }); + + it('should combine keys from target and temp map', () => { + const target = new Map(); + const map = new TempMap(target); + + target.set('test', 'test value2'); + map.set('test2', 'test value'); + + expect(Array.from(map.keys())).toEqual(['test', 'test2']); + }); + + it('should combine entries from target and temp map', () => { + const target = new Map(); + const map = new TempMap(target); + + target.set('test', 'test value2'); + map.set('test2', 'test value'); + + expect(Array.from(map.entries())).toEqual([ + ['test', 'test value2'], + ['test2', 'test value'], + ]); + }); + + it('should combine values from target and temp map', () => { + const target = new Map(); + const map = new TempMap(target); + + target.set('test', 'test value2'); + map.set('test2', 'test value'); + + expect(Array.from(map.values())).toEqual(['test value2', 'test value']); + }); + + it('should has target value in temp map', () => { + const target = new Map(); + const map = new TempMap(target); + + target.set('test', 'test value2'); + + expect(map.has('test')).toBe(true); + }); + + it('should be iterable', () => { + const target = new Map(); + const map = new TempMap(target); + + target.set('test', 'test value2'); + map.set('test2', 'test value'); + + const values: string[] = []; + const keys: string[] = []; + for (const [key, value] of map) { + values.push(value); + keys.push(key); + } + + expect(values).toEqual(['test value2', 'test value']); + expect(keys).toEqual(['test', 'test2']); + }); + + it('should not get deleted keys', () => { + const target = new Map(); + const map = new TempMap(target); + + map.set('test', 'test value2'); + map.set('test2', 'test value'); + map.delete('test2'); + + expect(Array.from(map.keys())).toEqual(['test']); + }); + + it('should not get deleted values', () => { + const target = new Map(); + const map = new TempMap(target); + + map.set('test', 'test value2'); + map.set('test2', 'test value'); + map.delete('test2'); + + expect(Array.from(map.values())).toEqual(['test value2']); + }); + + it('should not get deleted entries', () => { + const target = new Map(); + const map = new TempMap(target); + + map.set('test', 'test value2'); + map.set('test2', 'test value'); + map.delete('test2'); + + expect(Array.from(map.entries())).toEqual([['test', 'test value2']]); + }); + + it('should has toStringTag', () => { + const target = new Map(); + const map = new TempMap(target); + + expect(map[Symbol.toStringTag]).toBe('TempMap'); + }); +}); diff --git a/webapp/packages/core-utils/src/errorOf.test.ts b/webapp/packages/core-utils/src/errorOf.test.ts new file mode 100644 index 0000000000..1919afc252 --- /dev/null +++ b/webapp/packages/core-utils/src/errorOf.test.ts @@ -0,0 +1,56 @@ +/* + * 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 { errorOf } from './errorOf'; + +describe('errorOf', () => { + it('should return error of the specified type', () => { + class TestError extends Error {} + + const error = new TestError('test'); + const result = errorOf(error, TestError); + + expect(result).toBe(error); + expect(result).toBeInstanceOf(TestError); + expect(result?.message).toBe('test'); + }); + + it('should return error of the specified type from the cause', () => { + class TestError extends Error {} + class AnotherError extends Error { + cause: Error; + + constructor(message: string, cause: Error) { + super(message); + this.cause = cause; + } + } + + const error = new TestError('test'); + const testError = new AnotherError('another', error); + const result = errorOf(testError, TestError); + + expect(result).toBeInstanceOf(TestError); + }); + + it('should return undefined if error is not of the specified type', () => { + const error = new Error('test'); + const result = errorOf(error, TypeError); + + expect(result).toBeUndefined(); + }); + + it('should return undefined if error is not an instance of Error', () => { + expect(errorOf({ message: 'test' }, Error)).toBeUndefined(); + expect(errorOf(undefined, Error)).toBeUndefined(); + expect(errorOf(null, Error)).toBeUndefined(); + expect(errorOf(0, Error)).toBeUndefined(); + expect(errorOf('', Error)).toBeUndefined(); + expect(errorOf(true, Error)).toBeUndefined(); + expect(errorOf(Symbol(), Error)).toBeUndefined(); + }); +}); diff --git a/webapp/packages/core-utils/src/index.ts b/webapp/packages/core-utils/src/index.ts index a421b6e031..a54e923531 100644 --- a/webapp/packages/core-utils/src/index.ts +++ b/webapp/packages/core-utils/src/index.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ export * from './Promises/CancellablePromise'; -export * from './Promises/deferPromise'; +export * from './Promises/cancellableTimeout'; export * from './Promises/Deferred'; export * from './Promises/PromiseCancelledError'; export * from './Promises/PromiseExecutor'; @@ -72,7 +72,6 @@ export * from './createPath'; export * from './debounce'; export * from './schema'; export * from './schemaValidationError'; -export * from './setByPath'; export * from './svgToDataUri'; export * from './TempMap'; export * from './textToHex'; @@ -80,7 +79,6 @@ export * from './uriToBlob'; export * from './utf8ToBase64'; export * from './createLastPromiseGetter'; export * from './removeMetadataFromDataURL'; -export * from './renamePathName'; export * from './removeLineBreak'; export * from './replaceSubstring'; export * from './formatNumber'; diff --git a/webapp/packages/core-utils/src/openCenteredPopup.test.ts b/webapp/packages/core-utils/src/openCenteredPopup.test.ts new file mode 100644 index 0000000000..887ab553ad --- /dev/null +++ b/webapp/packages/core-utils/src/openCenteredPopup.test.ts @@ -0,0 +1,67 @@ +/* + * 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 { openCenteredPopup } from './openCenteredPopup'; + +describe('openCenteredPopup', () => { + let windowSpy: jest.SpyInstance; + const params = { + url: 'http://localhost:3000', + target: 'target', + width: 500, + height: 500, + features: 'features', + }; + + beforeEach(() => { + windowSpy = jest.spyOn(window, 'window', 'get'); + }); + + afterEach(() => { + windowSpy.mockRestore(); + }); + + it('should open centered popup', () => { + const windowMock = { + top: { + outerWidth: 1000, + screenY: 100, + screenX: 100, + outerHeight: 1000, + }, + screen: { + availWidth: 1000, + }, + open: jest.fn(), + }; + + windowSpy.mockImplementation(() => windowMock); + + const { url, target, width, height, features } = params; + + const result = openCenteredPopup({ url, target, width, height, features }); + + expect(result).toBe(windowMock.open()); + expect(windowMock.open).toHaveBeenCalledWith( + url, + target, + `toolbar=no, menubar=no, width=${width}, height=${height}, top=350, left=350${features}`, + ); + }); + + it('should return null if window.top is null', () => { + const { url, target, width, height, features } = params; + + windowSpy.mockImplementation(() => ({ + top: null, + })); + + const result = openCenteredPopup({ url, target, width, height, features }); + + expect(result).toBe(null); + }); +}); diff --git a/webapp/packages/core-utils/src/renamePathName.ts b/webapp/packages/core-utils/src/renamePathName.ts deleted file mode 100644 index 1d05cd36fc..0000000000 --- a/webapp/packages/core-utils/src/renamePathName.ts +++ /dev/null @@ -1,13 +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 { createPath } from './createPath'; -import { getPathParent } from './getPathParent'; - -export function renamePathName(path: string, name: string): string { - return createPath(getPathParent(path), name); -} diff --git a/webapp/packages/core-utils/src/setByPath.ts b/webapp/packages/core-utils/src/setByPath.ts deleted file mode 100644 index 2312e60ab0..0000000000 --- a/webapp/packages/core-utils/src/setByPath.ts +++ /dev/null @@ -1,27 +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. - */ - -export function setByPath(object: any, path: string, value: any) { - const parts = path.split('.'); - const last = parts.pop(); - - if (!last) { - return; - } - - let current = object; - - for (const part of parts) { - if (current[part] === undefined) { - current[part] = {}; - } - current = current[part]; - } - - current[last] = value; -} diff --git a/webapp/packages/core-utils/src/utf8ToBase64.test.ts b/webapp/packages/core-utils/src/utf8ToBase64.test.ts new file mode 100644 index 0000000000..0584c9d3a3 --- /dev/null +++ b/webapp/packages/core-utils/src/utf8ToBase64.test.ts @@ -0,0 +1,21 @@ +/* + * 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 { utf8ToBase64 } from './utf8ToBase64'; + +describe('utf8ToBase64', () => { + it('should convert utf8 to base64', () => { + expect(utf8ToBase64('test')).toBe('dGVzdA=='); + expect(utf8ToBase64('')).toBe(''); + expect(utf8ToBase64('test test')).toBe('dGVzdCB0ZXN0'); + }); + + it('should convert utf8 to base64 with special characters', () => { + expect(utf8ToBase64('test % test')).toBe('dGVzdCAlIHRlc3Q='); + expect(utf8ToBase64('%')).toBe('JQ=='); + }); +});