mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
This is the first version of our Tree implementation. At this point is a very simple tree without no abstractions and with code that eventually is not very performant, but this is only the first iteration and we are trying to follow the 'Last Responsible Moment Principle' [1]. Implemention details: - Creation of PGBrowser.treeMenu - Initial version of the Tree Adaptor 'pgadmin/static/js/tree/tree.js' - TreeFake test double that can replace the Tree for testing purposes - Tests, As an interesting asside because Fake’s need to behave like the real object you will noticed that there are tests for this type of double and they the same as of the real object. [1] https://medium.com/@aidanjcasey/guiding-principles-for-an-evolutionary-software-architecture-b6dc2cb24680 Patched by: Victoria && Joao Reviewed by: Khushboo & Ashesh
421 lines
13 KiB
JavaScript
421 lines
13 KiB
JavaScript
//////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// pgAdmin 4 - PostgreSQL Tools
|
|
//
|
|
// Copyright (C) 2013 - 2018, The pgAdmin Development Team
|
|
// This software is released under the PostgreSQL Licence
|
|
//
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
import {Tree, TreeNode} from '../../../pgadmin/static/js/tree/tree';
|
|
import {TreeFake} from './tree_fake';
|
|
|
|
const context = describe;
|
|
|
|
const treeTests = (treeClass, setDefaultCallBack) => {
|
|
let tree;
|
|
beforeEach(() => {
|
|
tree = new treeClass();
|
|
});
|
|
|
|
describe('#addNewNode', () => {
|
|
describe('when add a new root element', () => {
|
|
context('using [] as the parent', () => {
|
|
beforeEach(() => {
|
|
tree.addNewNode('some new node', {data: 'interesting'}, undefined, []);
|
|
});
|
|
|
|
it('can be retrieved', () => {
|
|
const node = tree.findNode(['some new node']);
|
|
expect(node.data).toEqual({data: 'interesting'});
|
|
});
|
|
|
|
it('return false for #hasParent()', () => {
|
|
const node = tree.findNode(['some new node']);
|
|
expect(node.hasParent()).toBe(false);
|
|
});
|
|
|
|
it('return null for #parent()', () => {
|
|
const node = tree.findNode(['some new node']);
|
|
expect(node.parent()).toBeNull();
|
|
});
|
|
});
|
|
|
|
context('using null as the parent', () => {
|
|
beforeEach(() => {
|
|
tree.addNewNode('some new node', {data: 'interesting'}, undefined, null);
|
|
});
|
|
|
|
it('can be retrieved', () => {
|
|
const node = tree.findNode(['some new node']);
|
|
expect(node.data).toEqual({data: 'interesting'});
|
|
});
|
|
|
|
it('return false for #hasParent()', () => {
|
|
const node = tree.findNode(['some new node']);
|
|
expect(node.hasParent()).toBe(false);
|
|
});
|
|
|
|
it('return null for #parent()', () => {
|
|
const node = tree.findNode(['some new node']);
|
|
expect(node.parent()).toBeNull();
|
|
});
|
|
});
|
|
|
|
context('using undefined as the parent', () => {
|
|
beforeEach(() => {
|
|
tree.addNewNode('some new node', {data: 'interesting'});
|
|
});
|
|
|
|
it('can be retrieved', () => {
|
|
const node = tree.findNode(['some new node']);
|
|
expect(node.data).toEqual({data: 'interesting'});
|
|
});
|
|
|
|
it('return false for #hasParent()', () => {
|
|
const node = tree.findNode(['some new node']);
|
|
expect(node.hasParent()).toBe(false);
|
|
});
|
|
|
|
it('return null for #parent()', () => {
|
|
const node = tree.findNode(['some new node']);
|
|
expect(node.parent()).toBeNull();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('when add a new element as a child', () => {
|
|
let parentNode;
|
|
beforeEach(() => {
|
|
parentNode = tree.addNewNode('parent node', {data: 'parent data'}, undefined, []);
|
|
tree.addNewNode('some new node', {data: 'interesting'}, undefined, ['parent' +
|
|
' node']);
|
|
});
|
|
|
|
it('can be retrieved', () => {
|
|
const node = tree.findNode(['parent node', 'some new node']);
|
|
expect(node.data).toEqual({data: 'interesting'});
|
|
});
|
|
|
|
it('return true for #hasParent()', () => {
|
|
const node = tree.findNode(['parent node', 'some new node']);
|
|
expect(node.hasParent()).toBe(true);
|
|
});
|
|
|
|
it('return "parent node" object for #parent()', () => {
|
|
const node = tree.findNode(['parent node', 'some new node']);
|
|
expect(node.parent()).toEqual(parentNode);
|
|
});
|
|
});
|
|
|
|
describe('when add an element that already exists under a parent', () => {
|
|
beforeEach(() => {
|
|
tree.addNewNode('parent node', {data: 'parent data'}, undefined, []);
|
|
tree.addNewNode('some new node', {data: 'interesting'}, undefined, ['parent' +
|
|
' node']);
|
|
});
|
|
|
|
it('does not add a new child', () => {
|
|
tree.addNewNode('some new node', {data: 'interesting 1'}, undefined, ['parent' +
|
|
' node']);
|
|
const parentNode = tree.findNode(['parent node']);
|
|
expect(parentNode.children.length).toBe(1);
|
|
});
|
|
|
|
it('updates the existing node data', () => {
|
|
tree.addNewNode('some new node', {data: 'interesting 1'}, undefined, ['parent' +
|
|
' node']);
|
|
const node = tree.findNode(['parent node', 'some new node']);
|
|
expect(node.data).toEqual({data: 'interesting 1'});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#translateTreeNodeIdFromACITree', () => {
|
|
let aciTreeApi;
|
|
beforeEach(() => {
|
|
aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
|
|
'hasParent',
|
|
'parent',
|
|
'getId',
|
|
]);
|
|
|
|
aciTreeApi.getId.and.callFake((node) => {
|
|
return node[0].id;
|
|
});
|
|
tree.aciTreeApi = aciTreeApi;
|
|
});
|
|
|
|
describe('When tree as a single level', () => {
|
|
beforeEach(() => {
|
|
aciTreeApi.hasParent.and.returnValue(false);
|
|
});
|
|
|
|
it('returns an array with the ID of the first level', () => {
|
|
let node = [{
|
|
id: 'some id',
|
|
}];
|
|
tree.addNewNode('some id', {}, undefined, []);
|
|
|
|
expect(tree.translateTreeNodeIdFromACITree(node)).toEqual(['some id']);
|
|
});
|
|
});
|
|
|
|
describe('When tree as a 2 levels', () => {
|
|
describe('When we try to retrieve the node in the second level', () => {
|
|
it('returns an array with the ID of the first level and second level', () => {
|
|
aciTreeApi.hasParent.and.returnValues(true, false);
|
|
aciTreeApi.parent.and.returnValue([{
|
|
id: 'parent id',
|
|
}]);
|
|
let node = [{
|
|
id: 'some id',
|
|
}];
|
|
|
|
tree.addNewNode('parent id', {}, undefined, []);
|
|
tree.addNewNode('some id', {}, undefined, ['parent id']);
|
|
|
|
expect(tree.translateTreeNodeIdFromACITree(node))
|
|
.toEqual(['parent id', 'some id']);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#selected', () => {
|
|
context('a node in the tree is selected', () => {
|
|
it('returns that node object', () => {
|
|
let selectedNode = new TreeNode('bamm', {}, []);
|
|
setDefaultCallBack(tree, selectedNode);
|
|
expect(tree.selected()).toEqual(selectedNode);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#findNodeByTreeElement', () => {
|
|
context('retrieve data from node not found', () => {
|
|
it('return undefined', () => {
|
|
let aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
|
|
'hasParent',
|
|
'parent',
|
|
'getId',
|
|
]);
|
|
|
|
aciTreeApi.getId.and.callFake((node) => {
|
|
return node[0].id;
|
|
});
|
|
tree.aciTreeApi = aciTreeApi;
|
|
expect(tree.findNodeByDomElement(['<li>something</li>'])).toBeUndefined();
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
describe('tree tests', () => {
|
|
describe('TreeNode', () => {
|
|
describe('#hasParent', () => {
|
|
context('parent is null', () => {
|
|
it('returns false', () => {
|
|
let treeNode = new TreeNode('123', {}, [], null);
|
|
expect(treeNode.hasParent()).toBe(false);
|
|
});
|
|
});
|
|
context('parent is undefined', () => {
|
|
it('returns false', () => {
|
|
let treeNode = new TreeNode('123', {}, [], undefined);
|
|
expect(treeNode.hasParent()).toBe(false);
|
|
});
|
|
});
|
|
context('parent exists', () => {
|
|
it('returns true', () => {
|
|
let parentNode = new TreeNode('456', {}, []);
|
|
let treeNode = new TreeNode('123', {}, [], parentNode);
|
|
expect(treeNode.hasParent()).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#reload', () => {
|
|
let tree;
|
|
let level2;
|
|
beforeEach(() => {
|
|
tree = new TreeFake();
|
|
tree.addNewNode('level1', {data: 'interesting'}, [{id: 'level1'}], []);
|
|
level2 = tree.addNewNode('level2', {data: 'data'}, [{id: 'level2'}], ['level1']);
|
|
tree.addNewNode('level3', {data: 'more data'}, [{id: 'level3'}], ['level1', 'level2']);
|
|
|
|
tree.aciTreeApi = jasmine.createSpyObj(
|
|
'ACITreeApi', ['setInode', 'unload', 'deselect', 'select']);
|
|
});
|
|
|
|
it('reloads the node and its children', () => {
|
|
level2.reload(tree);
|
|
expect(tree.findNodeByDomElement([{id: 'level2'}])).toEqual(level2);
|
|
});
|
|
|
|
it('does not reload the children of node', () => {
|
|
level2.reload(tree);
|
|
expect(tree.findNodeByDomElement([{id: 'level3'}])).toBeNull();
|
|
});
|
|
|
|
it('select the node', (done) => {
|
|
level2.reload(tree);
|
|
setTimeout(() => {
|
|
expect(tree.selected()).toEqual([{id: 'level2'}]);
|
|
done();
|
|
}, 20);
|
|
});
|
|
|
|
describe('ACITree specific', () => {
|
|
it('sets the current node as a Inode, changing the Icon back to +', () => {
|
|
level2.reload(tree);
|
|
expect(tree.aciTreeApi.setInode).toHaveBeenCalledWith([{id: 'level2'}]);
|
|
});
|
|
|
|
it('deselect the node and selects it again to trigger ACI tree' +
|
|
' events', (done) => {
|
|
level2.reload(tree);
|
|
setTimeout(() => {
|
|
expect(tree.aciTreeApi.deselect).toHaveBeenCalledWith([{id: 'level2'}]);
|
|
done();
|
|
}, 20);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#unload', () => {
|
|
let tree;
|
|
let level2;
|
|
beforeEach(() => {
|
|
tree = new TreeFake();
|
|
tree.addNewNode('level1', {data: 'interesting'}, ['<li>level1</li>'], []);
|
|
level2 = tree.addNewNode('level2', {data: 'data'}, ['<li>level2</li>'], ['level1']);
|
|
tree.addNewNode('level3', {data: 'more data'}, ['<li>level3</li>'], ['level1', 'level2']);
|
|
tree.aciTreeApi = jasmine.createSpyObj('ACITreeApi', ['unload']);
|
|
});
|
|
|
|
it('unloads the children of the current node', () => {
|
|
level2.unload(tree);
|
|
expect(tree.findNodeByDomElement([{id: 'level2'}])).toEqual(level2);
|
|
expect(tree.findNodeByDomElement([{id: 'level3'}])).toBeNull();
|
|
});
|
|
|
|
it('calls unload on the ACI Tree', () => {
|
|
level2.unload(tree);
|
|
expect(tree.aciTreeApi.unload).toHaveBeenCalledWith(['<li>level2</li>']);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Tree', () => {
|
|
function realTreeSelectNode(tree, selectedNode) {
|
|
let aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
|
|
'selected',
|
|
]);
|
|
tree.aciTreeApi = aciTreeApi;
|
|
aciTreeApi.selected.and.returnValue(selectedNode);
|
|
}
|
|
|
|
treeTests(Tree, realTreeSelectNode);
|
|
});
|
|
|
|
describe('TreeFake', () => {
|
|
function fakeTreeSelectNode(tree, selectedNode) {
|
|
tree.selectNode(selectedNode);
|
|
}
|
|
|
|
treeTests(TreeFake, fakeTreeSelectNode);
|
|
|
|
describe('#hasParent', () => {
|
|
context('tree contains multiple levels', () => {
|
|
let tree;
|
|
beforeEach(() => {
|
|
tree = new TreeFake();
|
|
tree.addNewNode('level1', {data: 'interesting'}, undefined, []);
|
|
tree.addNewNode('level2', {data: 'interesting'}, undefined, ['level1']);
|
|
});
|
|
|
|
context('node is at the first level', () => {
|
|
it('returns false', () => {
|
|
expect(tree.hasParent([{id: 'level1'}])).toBe(false);
|
|
});
|
|
});
|
|
|
|
context('node is at the second level', () => {
|
|
it('returns true', () => {
|
|
expect(tree.hasParent([{id: 'level2'}])).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#parent', () => {
|
|
let tree;
|
|
beforeEach(() => {
|
|
tree = new TreeFake();
|
|
tree.addNewNode('level1', {data: 'interesting'}, undefined, []);
|
|
tree.addNewNode('level2', {data: 'interesting'}, undefined, ['level1']);
|
|
});
|
|
|
|
context('node is the root', () => {
|
|
it('returns null', () => {
|
|
expect(tree.parent([{id: 'level1'}])).toBeNull();
|
|
});
|
|
});
|
|
|
|
context('node is not root', () => {
|
|
it('returns root element', () => {
|
|
expect(tree.parent([{id: 'level2'}])).toEqual([{id: 'level1'}]);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#itemData', () => {
|
|
let tree;
|
|
beforeEach(() => {
|
|
tree = new TreeFake();
|
|
tree.addNewNode('level1', {data: 'interesting'}, undefined, []);
|
|
tree.addNewNode('level2', {data: 'expected data'}, undefined, ['level1']);
|
|
});
|
|
|
|
context('retrieve data from the node', () => {
|
|
it('return the node data', () => {
|
|
expect(tree.itemData([{id: 'level2'}])).toEqual({
|
|
data: 'expected' +
|
|
' data',
|
|
});
|
|
});
|
|
});
|
|
|
|
context('retrieve data from node not found', () => {
|
|
it('return undefined', () => {
|
|
expect(tree.itemData([{id: 'bamm'}])).toBeUndefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#addChild', () => {
|
|
let root, child;
|
|
beforeEach(() => {
|
|
let tree = new TreeFake();
|
|
root = tree.addNewNode('root', {}, [{id: 'root'}]);
|
|
child = new TreeNode('node.1', {}, [{id: 'node.1'}]);
|
|
tree.addChild(root, child);
|
|
});
|
|
|
|
it('adds a new child to a node', () => {
|
|
expect(root.children).toEqual([child]);
|
|
});
|
|
|
|
it('changes the parent of the child node', () => {
|
|
expect(root.children[0].parentNode).toEqual(root);
|
|
expect(child.parentNode).toEqual(root);
|
|
});
|
|
|
|
it('changes the path of the child', () => {
|
|
expect(child.path).toEqual('root.node.1');
|
|
});
|
|
});
|
|
});
|
|
});
|