Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CB-3898 add base CachedMapResource tests #2013

Merged
merged 10 commits into from
Sep 26, 2023
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 },
]);
Wroud marked this conversation as resolved.
Show resolved Hide resolved

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
Loading