CB-3898 add base CachedMapResource tests (#2013)

* CB-3898 add base CachedMapResource tests

* CB-3908 add tests for the handlers

* CB-3908 add error tests

* CB-3908 add initial tests for the CachedDataResource

* CB-3908 remove extra test

* CB-3908 add clear data test

* CB-3908 add CachedTreeResource tests

* CB-3908 review fixes

* CB-3908 transform mock data to getters
This commit is contained in:
alex
2023-09-26 18:27:00 +03:00
committed by GitHub
parent 0d941b61c8
commit c4975475a1
4 changed files with 488 additions and 0 deletions
+1
View File
@@ -17,6 +17,7 @@
"gql:gen:dev": "graphql-codegen --watch",
"lint": "eslint ./src/ --ext .ts,.tsx",
"lint-fix": "eslint ./src/ --ext .ts,.tsx --fix",
"test": "core-cli-test",
"validate-dependencies": "core-cli-validate-dependencies"
},
"dependencies": {
@@ -0,0 +1,95 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { CachedDataResource } from './CachedDataResource';
interface IEntityData {
id: string;
value: number;
}
const DEFAULT_STATE_GETTER: () => IEntityData[] = () => [];
const DATA_MOCK_GETTER: () => IEntityData[] = () => [
{
id: '1',
value: 1,
},
{
id: '2',
value: 2,
},
];
async function fetchMock(): Promise<IEntityData[]> {
return new Promise(resolve => {
setTimeout(() => {
resolve(DATA_MOCK_GETTER());
}, 1);
});
}
class TestDataResource extends CachedDataResource<IEntityData[]> {
constructor() {
super(DEFAULT_STATE_GETTER);
}
protected async loader() {
const data = await fetchMock();
return data;
}
}
describe('CachedDataResource', () => {
let dataResource: TestDataResource;
beforeEach(() => {
dataResource = new TestDataResource();
});
test('should instantiate correctly', () => {
expect(dataResource).toBeInstanceOf(CachedDataResource);
});
test('should initialize with the default state', () => {
expect(dataResource.data).toEqual(DEFAULT_STATE_GETTER());
});
test('should load data', async () => {
await dataResource.load();
expect(dataResource.data).toEqual(DATA_MOCK_GETTER());
});
test('should be able to outdate the data', () => {
dataResource.markOutdated();
expect(dataResource.isOutdated()).toBe(true);
});
test('should run onDataOutdated handlers on data outdate', () => {
const handler = jest.fn();
dataResource.onDataOutdated.addHandler(handler);
dataResource.markOutdated();
expect(handler).toHaveBeenCalled();
});
test('should run onDataUpdate handlers on data update', () => {
const handler = jest.fn();
dataResource.onDataUpdate.addHandler(handler);
dataResource.dataUpdate();
expect(handler).toHaveBeenCalled();
});
test('should be able to clear the data', async () => {
await dataResource.load();
dataResource.clear();
expect(dataResource.data).toEqual([]);
});
});
@@ -0,0 +1,254 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { toJS } from 'mobx';
import { CachedMapResource } from './CachedMapResource';
import type { ResourceKey } from './ResourceKey';
import { resourceKeyList } from './ResourceKeyList';
interface IEntityData {
id: string;
value: number;
}
const ERROR_ITEM_ID = 'error';
const DATA_MOCK_GETTER: () => IEntityData[] = () => [
{
id: '1',
value: 1,
},
{
id: '2',
value: 2,
},
{
id: ERROR_ITEM_ID,
value: 3,
},
];
const TEST_ERROR_MESSAGE = 'Test error';
const DEFAULT_STATE_GETTER = () => new Map();
async function fetchMock(key: ResourceKey<string> | undefined): Promise<IEntityData[]> {
const data = DATA_MOCK_GETTER();
return new Promise((resolve, reject) => {
setTimeout(() => {
if (key) {
if (key === ERROR_ITEM_ID) {
reject(new Error(TEST_ERROR_MESSAGE));
}
const item = data.find(d => d.id === key);
if (item) {
resolve([item]);
}
} else {
resolve(data);
}
}, 1);
});
}
class TestMapResource extends CachedMapResource<string, IEntityData> {
constructor() {
super(DEFAULT_STATE_GETTER);
}
protected async loader(key: ResourceKey<string>) {
const data = await fetchMock(key);
this.replace(resourceKeyList(data.map(d => d.id)), data);
return this.data;
}
protected validateKey(key: string): boolean {
return typeof key === 'string';
}
}
describe('CachedMapResource', () => {
let mapResource: TestMapResource;
beforeEach(() => {
mapResource = new TestMapResource();
});
test('should instantiate correctly', () => {
expect(mapResource).toBeInstanceOf(CachedMapResource);
});
test('should initialize with the initial value', () => {
expect(toJS(mapResource.data)).toEqual(DEFAULT_STATE_GETTER());
});
test('should return all entries', () => {
mapResource.set('key1', { id: 'key1', value: 1 });
mapResource.set('key2', { id: 'key2', value: 2 });
expect(mapResource.entries).toEqual([
['key1', { id: 'key1', value: 1 }],
['key2', { id: 'key2', value: 2 }],
]);
});
test('should return all values', () => {
mapResource.set('key1', { id: 'key1', value: 1 });
mapResource.set('key2', { id: 'key2', value: 2 });
expect(mapResource.values).toEqual([
{ id: 'key1', value: 1 },
{ id: 'key2', value: 2 },
]);
});
test('should return all keys', () => {
mapResource.set('key1', { id: 'key1', value: 1 });
mapResource.set('key2', { id: 'key2', value: 2 });
expect(mapResource.keys).toEqual(['key1', 'key2']);
});
test('should load data for a specific key', async () => {
await mapResource.load('1');
expect(mapResource.get('1')).toEqual({ id: '1', value: 1 });
expect(mapResource.get('2')).toBeUndefined(); // This key was not loaded
});
test('should NOT load data for a key that produces an error', async () => {
await expect(mapResource.load(ERROR_ITEM_ID)).rejects.toThrow(TEST_ERROR_MESSAGE);
expect(mapResource.get(ERROR_ITEM_ID)).toBeUndefined();
});
test('should mark loaded data as loaded', async () => {
await mapResource.load('1');
expect(mapResource.isLoaded('1')).toBe(true);
});
test('should set and get a value', () => {
mapResource.set('key1', { id: 'key1', value: 1 });
expect(mapResource.get('key1')).toStrictEqual({ id: 'key1', value: 1 });
});
test('should delete a value', () => {
mapResource.set('key1', { id: 'key1', value: 1 });
mapResource.delete('key1');
expect(mapResource.get('key1')).toBeUndefined();
});
test('should check if a key exists', () => {
mapResource.set('key1', { id: 'key1', value: 1 });
expect(mapResource.has('key1')).toBe(true);
expect(mapResource.has('key2')).toBe(false);
});
test('should replace multiple keys', () => {
mapResource.set('key1', { id: 'key1', value: 1 });
mapResource.set('key2', { id: 'key2', value: 2 });
mapResource.set('key4', { id: 'key4', value: 4 });
mapResource.replace(resourceKeyList(['key1', 'key3']), [
{ id: 'key1', value: 11 },
{ id: 'key3', value: 33 },
]);
expect(mapResource.get('key1')).toStrictEqual({ id: 'key1', value: 11 });
expect(mapResource.get('key2')).toBeUndefined();
expect(mapResource.get('key3')).toStrictEqual({ id: 'key3', value: 33 });
expect(mapResource.data.size).toBe(2);
});
test('should outdated certain keys', () => {
mapResource.set('key1', { id: 'key1', value: 1 });
mapResource.set('key2', { id: 'key2', value: 2 });
mapResource.markOutdated('key1');
expect(mapResource.isOutdated('key1')).toBe(true);
expect(mapResource.isOutdated('key2')).toBe(false);
});
test('should run onDataOutdated handlers on data outdate', () => {
const handler = jest.fn();
mapResource.set('key1', { id: 'key1', value: 1 });
mapResource.set('key2', { id: 'key2', value: 2 });
mapResource.onDataOutdated.addHandler(key => {
handler();
expect(key).toBe('key1');
});
mapResource.markOutdated('key1');
expect(handler).toHaveBeenCalled();
});
test('should run onDataUpdate handlers on data update', () => {
const handler = jest.fn();
mapResource.set('key1', { id: 'key1', value: 1 });
mapResource.set('key2', { id: 'key2', value: 2 });
mapResource.onDataUpdate.addHandler(key => {
handler();
expect(key).toBe('key2');
});
mapResource.dataUpdate('key2');
expect(handler).toHaveBeenCalled();
});
test('should run onItemDelete handlers on data delete', () => {
const handler = jest.fn();
mapResource.set('key1', { id: 'key1', value: 1 });
mapResource.set('key2', { id: 'key2', value: 2 });
mapResource.onItemDelete.addHandler(key => {
handler();
expect(key).toBe('key1');
});
mapResource.delete('key1');
expect(handler).toHaveBeenCalled();
});
test('should run onItemUpdate handlers on item update', () => {
const handler = jest.fn();
mapResource.set('key1', { id: 'key1', value: 1 });
mapResource.set('key2', { id: 'key2', value: 2 });
mapResource.onItemUpdate.addHandler(key => {
handler();
expect(key).toBe('key2');
});
mapResource.set('key2', { id: 'key2', value: 22 });
expect(handler).toHaveBeenCalled();
});
test('should run onDataError handlers on data error', async () => {
const handler = jest.fn();
mapResource.set('key1', { id: 'key1', value: 1 });
mapResource.set('key2', { id: 'key2', value: 2 });
mapResource.onDataError.addHandler(data => {
handler();
expect(data.param).toBe(ERROR_ITEM_ID);
expect(data.exception.message).toBe(TEST_ERROR_MESSAGE);
});
await expect(mapResource.load(ERROR_ITEM_ID)).rejects.toThrow(TEST_ERROR_MESSAGE);
expect(handler).toHaveBeenCalled();
});
test('should be able to get an exception if the one occurred for the key', async () => {
await expect(mapResource.load(ERROR_ITEM_ID)).rejects.toThrow(TEST_ERROR_MESSAGE);
expect(mapResource.getException(ERROR_ITEM_ID)?.message).toBe(TEST_ERROR_MESSAGE);
});
});
@@ -0,0 +1,138 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { CachedTreeChildrenKey, CachedTreeResource, CachedTreeRootChildrenKey } from './CachedTreeResource';
import type { ResourceKey } from './ResourceKey';
import { resourceKeyList } from './ResourceKeyList';
interface IMockDataEntity {
name: string;
}
class TestTreeResource extends CachedTreeResource<IMockDataEntity> {
constructor() {
super();
}
protected async loader(key: ResourceKey<string>) {
return this.data;
}
protected validateKey(key: string): boolean {
return typeof key === 'string';
}
}
describe('CachedMapResource', () => {
let treeResource: TestTreeResource;
beforeEach(() => {
treeResource = new TestTreeResource();
});
test('should instantiate correctly', () => {
expect(treeResource).toBeInstanceOf(CachedTreeResource);
});
test('should set data correctly', () => {
treeResource.set('root', { name: 'root' });
expect(treeResource.get('root')).toEqual({ name: 'root' });
});
test('.has should check if node exists correctly', () => {
treeResource.set('root', { name: 'root' });
treeResource.set('root/level2', { name: 'level2' });
expect(treeResource.has('root/level2')).toBe(true);
expect(treeResource.has('root/level3')).toBe(false);
});
test('the node should be outdated after markOutdated is called on it', () => {
treeResource.set('root', { name: 'root' });
treeResource.set('root/level2', { name: 'level2' });
treeResource.markOutdated('root');
expect(treeResource.isOutdated('root')).toBe(true);
expect(treeResource.isOutdated('root/level2')).toBe(false);
});
test('the nodes metadata should be outdated after markOutdated is called on it', () => {
treeResource.set('root', { name: 'root' });
treeResource.set('root/level2', { name: 'level2' });
treeResource.markOutdated('root');
expect(treeResource.getMetadata('root').outdated).toBe(true);
expect(treeResource.getMetadata('root/level2').outdated).toBe(false);
});
test('should run onDataOutdated handlers on data outdate', () => {
const handler = jest.fn();
treeResource.set('root', { name: 'root' });
treeResource.set('root/level2', { name: 'level2' });
treeResource.onDataOutdated.addHandler(key => {
handler();
expect(key).toBe('root');
});
treeResource.markOutdated('root');
expect(treeResource.isOutdated('root')).toBe(true);
expect(handler).toHaveBeenCalled();
});
test('CachedTreeChildrenKey alias should return key children of the node', () => {
const handler = jest.fn();
treeResource.set('root', { name: 'root' });
treeResource.set('root/level2', { name: 'level2' });
treeResource.set('root/level2/level3', { name: 'level3' });
treeResource.onDataOutdated.addHandler(key => {
handler();
expect(key).toEqual(resourceKeyList(['root/level2']));
});
treeResource.markOutdated(CachedTreeChildrenKey('root'));
expect(handler).toHaveBeenCalled();
});
test('CachedTreeRootChildrenKey alias should return children for the root node', () => {
treeResource.set('root', { name: 'root' });
treeResource.set('root/level2', { name: 'level2' });
treeResource.set('root/level2/level3', { name: 'level3' });
expect(treeResource.get(CachedTreeRootChildrenKey)).toEqual([{ name: 'root' }]);
});
test('should be able to delete the node', () => {
treeResource.set('root', { name: 'root' });
treeResource.set('root/level2', { name: 'level2' });
treeResource.delete('root/level2');
expect(treeResource.has('root/level2')).toBe(false);
expect(treeResource.has('root')).toBe(true);
});
test('should run onItemDelete handlers on data delete', () => {
const handler = jest.fn();
treeResource.set('root', { name: 'root' });
treeResource.set('root/level2', { name: 'level2' });
treeResource.onItemDelete.addHandler(key => {
handler();
expect(key).toBe('root/level2');
});
treeResource.delete('root/level2');
expect(handler).toHaveBeenCalled();
});
});