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
94 changes: 94 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,94 @@
/*
* 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 ILoaderData {
id: string;
value: number;
}

const LOADER_DATA_MOCK: ILoaderData[] = [
{
id: '1',
value: 1,
},
{
id: '2',
value: 2,
},
];

class TestDataResource extends CachedDataResource<ILoaderData[]> {
constructor() {
super(() => []);
}

private async fetchFromAPI(): Promise<ILoaderData[]> {
return new Promise(resolve => {
setTimeout(() => {
resolve(LOADER_DATA_MOCK);
}, 1);
});
}
Wroud marked this conversation as resolved.
Show resolved Hide resolved

protected async loader() {
const data = await this.fetchFromAPI();
return data;
}
}

describe('CachedDataResource', () => {
let dataResource: TestDataResource;

beforeEach(() => {
dataResource = new TestDataResource();
});

test('should instantiate correctly', () => {
expect(dataResource).toBeInstanceOf(CachedDataResource);
});

test('should initialize with an empty array', () => {
expect(dataResource.data).toEqual([]);
});
Wroud marked this conversation as resolved.
Show resolved Hide resolved

test('should load data', async () => {
await dataResource.load();
expect(dataResource.data).toEqual(LOADER_DATA_MOCK);
});

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([]);
});
});
253 changes: 253 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,253 @@
/*
* 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 { CachedMapResource } from './CachedMapResource';
import type { ResourceKey } from './ResourceKey';
import { resourceKeyList } from './ResourceKeyList';

interface ILoaderData {
id: string;
value: number;
}

const ERROR_ITEM_ID = 'error';

const LOADER_DATA_MOCK: ILoaderData[] = [
{
id: '1',
value: 1,
},
{
id: '2',
value: 2,
},
{
id: ERROR_ITEM_ID,
value: 3,
},
];

const TEST_ERROR_MESSAGE = 'Test error';

class TestMapResource extends CachedMapResource<string, ILoaderData> {
constructor() {
super();
}

async fetchAnError() {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(TEST_ERROR_MESSAGE));
}, 1);
});
}

private async fetchFromAPI(key: ResourceKey<string> | undefined): Promise<ILoaderData[]> {
return new Promise(resolve => {
setTimeout(() => {
if (key) {
const data = LOADER_DATA_MOCK.find(d => d.id === key);
if (data) {
resolve([data]);
}
} else {
resolve(LOADER_DATA_MOCK);
}
}, 1);
});
}
Wroud marked this conversation as resolved.
Show resolved Hide resolved

protected async loader(key: ResourceKey<string>) {
if (key === ERROR_ITEM_ID) {
await this.fetchAnError();
}

const data = await this.fetchFromAPI(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 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 initialize with an empty map', () => {
expect(mapResource.entries).toEqual([]);
});
Wroud marked this conversation as resolved.
Show resolved Hide resolved

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 change loaded state when data is loaded ', async () => {
await mapResource.load('1');
expect(mapResource.isLoaded('1')).toBe(true);
});
Wroud marked this conversation as resolved.
Show resolved Hide resolved

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.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 });
});

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 data delete', () => {
Wroud marked this conversation as resolved.
Show resolved Hide resolved
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