Skip to content

Commit

Permalink
CB-3898 add base CachedMapResource tests (#2013)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
devnaumov authored Sep 26, 2023
1 parent 0d941b6 commit c497547
Show file tree
Hide file tree
Showing 4 changed files with 488 additions and 0 deletions.
1 change: 1 addition & 0 deletions webapp/packages/core-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
95 changes: 95 additions & 0 deletions webapp/packages/core-sdk/src/Resource/CachedDataResource.test.ts
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
254 changes: 254 additions & 0 deletions webapp/packages/core-sdk/src/Resource/CachedMapResource.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading

0 comments on commit c497547

Please sign in to comment.