From e7b735fd9ad29a670a07054c6c5d9a5ce8547174 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Tue, 4 Jun 2013 16:19:56 +0200 Subject: [PATCH] GraphViewer: Added an abstract version of the adapter which does plain handling of nodes, server-connection is implemented in a more specific class --- .../js/graphViewer/graph/abstractAdapter.js | 387 ++++ .../js/graphViewer/graph/arangoAdapter.js | 362 +--- .../specAdapter/arangoAdapterSpec.js | 44 +- .../specAdapter/foxxAdapterSpec.js | 1713 +++++++++++++++++ 4 files changed, 2149 insertions(+), 357 deletions(-) create mode 100644 html/admin/js/graphViewer/graph/abstractAdapter.js create mode 100644 html/admin/js/graphViewer/jasmine_test/specAdapter/foxxAdapterSpec.js diff --git a/html/admin/js/graphViewer/graph/abstractAdapter.js b/html/admin/js/graphViewer/graph/abstractAdapter.js new file mode 100644 index 0000000000..c382d44a03 --- /dev/null +++ b/html/admin/js/graphViewer/graph/abstractAdapter.js @@ -0,0 +1,387 @@ +/*jslint indent: 2, nomen: true, maxlen: 100, white: true plusplus: true */ +/*global $, _ */ +/*global NodeReducer */ +//////////////////////////////////////////////////////////////////////////////// +/// @brief Graph functionality +/// +/// @file +/// +/// DISCLAIMER +/// +/// Copyright 2010-2012 triagens GmbH, Cologne, Germany +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// +/// Copyright holder is triAGENS GmbH, Cologne, Germany +/// +/// @author Michael Hackstein +/// @author Copyright 2011-2013, triAGENS GmbH, Cologne, Germany +//////////////////////////////////////////////////////////////////////////////// + + +function AbstractAdapter(nodes, edges) { + "use strict"; + + if (nodes === undefined) { + throw "The nodes have to be given."; + } + if (edges === undefined) { + throw "The edges have to be given."; + } + + var self = this, + initialX = {}, + initialY = {}, + cachedCommunities = {}, + joinedInCommunities = {}, + limit, + reducer, + childLimit, + exports = {}, + + setWidth = function(w) { + initialX.range = w / 2; + initialX.start = w / 4; + initialX.getStart = function () { + return this.start + Math.random() * this.range; + }; + }, + + setHeight = function(h) { + initialY.range = h / 2; + initialY.start = h / 4; + initialY.getStart = function () { + return this.start + Math.random() * this.range; + }; + }, + + findNode = function(id) { + var intId = joinedInCommunities[id] || id, + res = $.grep(nodes, function(e){ + return e._id === intId; + }); + if (res.length === 0) { + return false; + } + if (res.length === 1) { + return res[0]; + } + throw "Too many nodes with the same ID, should never happen"; + }, + + findEdge = function(id) { + var res = $.grep(edges, function(e){ + return e._id === id; + }); + if (res.length === 0) { + return false; + } + if (res.length === 1) { + return res[0]; + } + throw "Too many edges with the same ID, should never happen"; + }, + + insertNode = function(data) { + var node = { + _data: data, + _id: data._id + }, + n = findNode(node._id); + if (n) { + return n; + } + node.x = initialX.getStart(); + node.y = initialY.getStart(); + nodes.push(node); + node._outboundCounter = 0; + node._inboundCounter = 0; + return node; + }, + + insertEdge = function(data) { + var source, + target, + edge = { + _data: data, + _id: data._id + }, + e = findEdge(edge._id), + edgeToPush; + if (e) { + return e; + } + source = findNode(data._from); + target = findNode(data._to); + if (!source) { + throw "Unable to insert Edge, source node not existing " + edge._from; + } + if (!target) { + throw "Unable to insert Edge, target node not existing " + edge._to; + } + edge.source = source; + edge.target = target; + edges.push(edge); + + + if (cachedCommunities[source._id] !== undefined) { + edgeToPush = {}; + edgeToPush.type = "s"; + edgeToPush.id = edge._id; + edgeToPush.source = $.grep(cachedCommunities[source._id].nodes, function(e){ + return e._id === data._from; + })[0]; + edgeToPush.source._outboundCounter++; + cachedCommunities[source._id].edges.push(edgeToPush); + } else { + source._outboundCounter++; + } + if (cachedCommunities[target._id] !== undefined) { + edgeToPush = {}; + edgeToPush.type = "t"; + edgeToPush.id = edge._id; + edgeToPush.target = $.grep(cachedCommunities[target._id].nodes, function(e){ + return e._id === data._to; + })[0]; + edgeToPush.target._inboundCounter++; + cachedCommunities[target._id].edges.push(edgeToPush); + } else { + target._inboundCounter++; + } + return edge; + }, + + removeNode = function (node) { + var i; + for ( i = 0; i < nodes.length; i++ ) { + if ( nodes[i] === node ) { + nodes.splice( i, 1 ); + return; + } + } + }, + + removeEdge = function (edge) { + var i; + for ( i = 0; i < edges.length; i++ ) { + if ( edges[i] === edge ) { + edges.splice( i, 1 ); + return; + } + } + }, + + removeEdgesForNode = function (node) { + var i; + for (i = 0; i < edges.length; i++ ) { + if (edges[i].source === node) { + node._outboundCounter--; + edges[i].target._inboundCounter--; + edges.splice( i, 1 ); + i--; + } else if (edges[i].target === node) { + node._inboundCounter--; + edges[i].source._outboundCounter--; + edges.splice( i, 1 ); + i--; + } + } + }, + + combineCommunityEdges = function (nodes, commNode) { + var i, j, s, t, + cachedCommEdges = cachedCommunities[commNode._id].edges, + edgeToPush; + for (i = 0; i < edges.length; i++ ) { + edgeToPush = {}; + // s and t keep old values yay! + s = edges[i].source; + t = edges[i].target; + for (j = 0; j < nodes.length; j++) { + if (s === nodes[j]) { + if (edgeToPush.type !== undefined) { + edges[i].target = edgeToPush.target; + delete edgeToPush.target; + edgeToPush.type = "b"; + edgeToPush.edge = edges[i]; + edges.splice( i, 1 ); + i--; + break; + } + edges[i].source = commNode; + edgeToPush.type = "s"; + edgeToPush.id = edges[i]._id; + edgeToPush.source = s; + } + if (t === nodes[j]) { + if (edgeToPush.type !== undefined) { + edges[i].source = edgeToPush.source; + delete edgeToPush.source; + edgeToPush.type = "b"; + edgeToPush.edge = edges[i]; + edges.splice( i, 1 ); + i--; + break; + } + edges[i].target = commNode; + edgeToPush.type = "t"; + edgeToPush.id = edges[i]._id; + edgeToPush.target = t; + } + } + if (edgeToPush.type !== undefined) { + cachedCommEdges.push(edgeToPush); + } + } + }, + + // Helper function to easily remove all outbound edges for one node + removeOutboundEdgesFromNode = function ( node ) { + if (node._outboundCounter > 0) { + var removed = [], + i; + for ( i = 0; i < edges.length; i++ ) { + if ( edges[i].source === node ) { + removed.push(edges[i]); + node._outboundCounter--; + edges[i].target._inboundCounter--; + edges.splice( i, 1 ); + if (node._outboundCounter === 0) { + break; + } + i--; + } + } + return removed; + } + }, + + collapseCommunity = function (community) { + var commId = "*community_" + Math.floor(Math.random()* 1000000), + commNode = { + _id: commId, + edges: [] + }, + nodesToRemove = _.map(community, function(id) { + return findNode(id); + }); + commNode.x = nodesToRemove[0].x; + commNode.y = nodesToRemove[0].y; + cachedCommunities[commId] = {}; + cachedCommunities[commId].nodes = nodesToRemove; + cachedCommunities[commId].edges = []; + + combineCommunityEdges(nodesToRemove, commNode); + _.each(nodesToRemove, function(n) { + joinedInCommunities[n._id] = commId; + removeNode(n); + }); + nodes.push(commNode); + }, + + expandCommunity = function (commNode) { + var commId = commNode._id, + nodesToAdd = cachedCommunities[commId].nodes, + edgesToChange = cachedCommunities[commId].edges, + com; + removeNode(commNode); + if (limit < nodes.length + nodesToAdd.length) { + com = reducer.getCommunity(limit); + collapseCommunity(com); + } + _.each(nodesToAdd, function(n) { + delete joinedInCommunities[n._id]; + nodes.push(n); + }); + _.each(edgesToChange, function(e) { + var edge; + switch(e.type) { + case "t": + edge = findEdge(e.id); + edge.target = e.target; + break; + case "s": + edge = findEdge(e.id); + edge.source = e.source; + break; + case "b": + edges.push(e.edge); + break; + } + }); + delete cachedCommunities[commId]; + }, + + checkSizeOfInserted = function (inserted) { + var buckets; + if (_.size(inserted) > childLimit) { + buckets = reducer.bucketNodes(_.values(inserted), childLimit); + _.each(buckets, function(b) { + if (b.length > 1) { + var ids = _.map(b, function(n) { + return n._id; + }); + collapseCommunity(ids); + } + }); + } + }, + + checkNodeLimit = function (focus) { + if (limit < nodes.length) { + var com = reducer.getCommunity(limit, focus); + collapseCommunity(com); + } + }, + + setNodeLimit = function (pLimit, callback) { + limit = pLimit; + if (limit < nodes.length) { + var com = reducer.getCommunity(limit); + collapseCommunity(com); + if (callback !== undefined) { + callback(); + } + } + }, + + setChildLimit = function (pLimit) { + childLimit = pLimit; + }; + + childLimit = Number.POSITIVE_INFINITY; + + reducer = new NodeReducer(nodes, edges); + + initialX.getStart = function() {return 0;}; + initialY.getStart = function() {return 0;}; + + exports.setWidth = setWidth; + exports.setHeight = setHeight; + exports.insertNode = insertNode; + exports.insertEdge = insertEdge; + + exports.removeNode = removeNode; + exports.removeEdge = removeEdge; + exports.removeEdgesForNode = removeEdgesForNode; + + exports.expandCommunity = expandCommunity; + + exports.setNodeLimit = setNodeLimit; + exports.setChildLimit = setChildLimit; + + exports.checkSizeOfInserted = checkSizeOfInserted; + exports.checkNodeLimit = checkNodeLimit; + + return exports; +} \ No newline at end of file diff --git a/html/admin/js/graphViewer/graph/arangoAdapter.js b/html/admin/js/graphViewer/graph/arangoAdapter.js index 343ca6d061..0f0338e741 100644 --- a/html/admin/js/graphViewer/graph/arangoAdapter.js +++ b/html/admin/js/graphViewer/graph/arangoAdapter.js @@ -1,6 +1,6 @@ /*jslint indent: 2, nomen: true, maxlen: 100, white: true plusplus: true */ /*global $, d3, _, console, document*/ -/*global NodeReducer*/ +/*global AbstractAdapter*/ //////////////////////////////////////////////////////////////////////////////// /// @brief Graph functionality /// @@ -48,41 +48,15 @@ function ArangoAdapter(nodes, edges, config) { } var self = this, - initialX = {}, - initialY = {}, + absAdapter = new AbstractAdapter(nodes, edges), api = {}, queries = {}, - cachedCommunities = {}, - joinedInCommunities = {}, nodeCollection, edgeCollection, - limit, - childLimit, - reducer, arangodb, - width, - height, direction, - - setWidth = function(w) { - initialX.range = w / 2; - initialX.start = w / 4; - initialX.getStart = function () { - return this.start + Math.random() * this.range; - }; - }, - - setHeight = function(h) { - initialY.range = h / 2; - initialY.start = h / 4; - initialY.getStart = function () { - return this.start + Math.random() * this.range; - }; - }, - + parseConfig = function(config) { - initialX.getStart = function() {return 0;}; - initialY.getStart = function() {return 0;}; nodeCollection = config.nodeCollection; edgeCollection = config.edgeCollection; if (config.host === undefined) { @@ -91,10 +65,10 @@ function ArangoAdapter(nodes, edges, config) { arangodb = config.host; } if (config.width !== undefined) { - setWidth(config.width); + absAdapter.setWidth(config.width); } if (config.height !== undefined) { - setHeight(config.height); + absAdapter.setHeight(config.height); } if (config.undirected !== undefined) { if (config.undirected === true) { @@ -107,208 +81,6 @@ function ArangoAdapter(nodes, edges, config) { } }, - findNode = function(id) { - var intId = joinedInCommunities[id] || id, - res = $.grep(nodes, function(e){ - return e._id === intId; - }); - if (res.length === 0) { - return false; - } - if (res.length === 1) { - return res[0]; - } - throw "Too many nodes with the same ID, should never happen"; - }, - - findEdge = function(id) { - var res = $.grep(edges, function(e){ - return e._id === id; - }); - if (res.length === 0) { - return false; - } - if (res.length === 1) { - return res[0]; - } - throw "Too many edges with the same ID, should never happen"; - }, - - insertNode = function(data) { - var node = { - _data: data, - _id: data._id - }, - n = findNode(node._id); - if (n) { - return n; - } - node.x = initialX.getStart(); - node.y = initialY.getStart(); - nodes.push(node); - node._outboundCounter = 0; - node._inboundCounter = 0; - return node; - }, - - insertEdge = function(data) { - var source, - target, - edge = { - _data: data, - _id: data._id - }, - e = findEdge(edge._id), - edgeToPush; - if (e) { - return e; - } - source = findNode(data._from); - target = findNode(data._to); - if (!source) { - throw "Unable to insert Edge, source node not existing " + edge._from; - } - if (!target) { - throw "Unable to insert Edge, target node not existing " + edge._to; - } - edge.source = source; - edge.target = target; - edges.push(edge); - - - if (cachedCommunities[source._id] !== undefined) { - edgeToPush = {}; - edgeToPush.type = "s"; - edgeToPush.id = edge._id; - edgeToPush.source = $.grep(cachedCommunities[source._id].nodes, function(e){ - return e._id === data._from; - })[0]; - edgeToPush.source._outboundCounter++; - cachedCommunities[source._id].edges.push(edgeToPush); - } else { - source._outboundCounter++; - } - if (cachedCommunities[target._id] !== undefined) { - edgeToPush = {}; - edgeToPush.type = "t"; - edgeToPush.id = edge._id; - edgeToPush.target = $.grep(cachedCommunities[target._id].nodes, function(e){ - return e._id === data._to; - })[0]; - edgeToPush.target._inboundCounter++; - cachedCommunities[target._id].edges.push(edgeToPush); - } else { - target._inboundCounter++; - } - return edge; - }, - - removeNode = function (node) { - var i; - for ( i = 0; i < nodes.length; i++ ) { - if ( nodes[i] === node ) { - nodes.splice( i, 1 ); - return; - } - } - }, - - removeEdge = function (edge) { - var i; - for ( i = 0; i < edges.length; i++ ) { - if ( edges[i] === edge ) { - edges.splice( i, 1 ); - return; - } - } - }, - - removeEdgesForNode = function (node) { - var i; - for (i = 0; i < edges.length; i++ ) { - if (edges[i].source === node) { - node._outboundCounter--; - edges[i].target._inboundCounter--; - edges.splice( i, 1 ); - i--; - } else if (edges[i].target === node) { - node._inboundCounter--; - edges[i].source._outboundCounter--; - edges.splice( i, 1 ); - i--; - } - } - }, - - combineCommunityEdges = function (nodes, commNode) { - var i, j, s, t, - cachedCommEdges = cachedCommunities[commNode._id].edges, - edgeToPush; - for (i = 0; i < edges.length; i++ ) { - edgeToPush = {}; - // s and t keep old values yay! - s = edges[i].source; - t = edges[i].target; - for (j = 0; j < nodes.length; j++) { - if (s === nodes[j]) { - if (edgeToPush.type !== undefined) { - edges[i].target = edgeToPush.target; - delete edgeToPush.target; - edgeToPush.type = "b"; - edgeToPush.edge = edges[i]; - edges.splice( i, 1 ); - i--; - break; - } - edges[i].source = commNode; - edgeToPush.type = "s"; - edgeToPush.id = edges[i]._id; - edgeToPush.source = s; - } - if (t === nodes[j]) { - if (edgeToPush.type !== undefined) { - edges[i].source = edgeToPush.source; - delete edgeToPush.source; - edgeToPush.type = "b"; - edgeToPush.edge = edges[i]; - edges.splice( i, 1 ); - i--; - break; - } - edges[i].target = commNode; - edgeToPush.type = "t"; - edgeToPush.id = edges[i]._id; - edgeToPush.target = t; - } - } - if (edgeToPush.type !== undefined) { - cachedCommEdges.push(edgeToPush); - } - } - }, - - // Helper function to easily remove all outbound edges for one node - removeOutboundEdgesFromNode = function ( node ) { - if (node._outboundCounter > 0) { - var removed = [], - i; - for ( i = 0; i < edges.length; i++ ) { - if ( edges[i].source === node ) { - removed.push(edges[i]); - node._outboundCounter--; - edges[i].target._inboundCounter--; - edges.splice( i, 1 ); - if (node._outboundCounter === 0) { - break; - } - i--; - } - } - return removed; - } - }, - - sendQuery = function(query, bindVars, onSuccess) { if (query !== queries.connectedEdges) { bindVars["@nodes"] = nodeCollection; @@ -343,106 +115,37 @@ function ArangoAdapter(nodes, edges, config) { } }); }, - - collapseCommunity = function (community) { - var commId = "*community_" + Math.floor(Math.random()* 1000000), - commNode = { - _id: commId, - edges: [] - }, - nodesToRemove = _.map(community, function(id) { - return findNode(id); - }); - commNode.x = nodesToRemove[0].x; - commNode.y = nodesToRemove[0].y; - cachedCommunities[commId] = {}; - cachedCommunities[commId].nodes = nodesToRemove; - cachedCommunities[commId].edges = []; - - combineCommunityEdges(nodesToRemove, commNode); - _.each(nodesToRemove, function(n) { - joinedInCommunities[n._id] = commId; - removeNode(n); - }); - nodes.push(commNode); - }, - - expandCommunity = function (commNode) { - var commId = commNode._id, - nodesToAdd = cachedCommunities[commId].nodes, - edgesToChange = cachedCommunities[commId].edges, - com; - removeNode(commNode); - if (limit < nodes.length + nodesToAdd.length) { - com = reducer.getCommunity(limit); - collapseCommunity(com); - } - _.each(nodesToAdd, function(n) { - delete joinedInCommunities[n._id]; - nodes.push(n); - }); - _.each(edgesToChange, function(e) { - var edge; - switch(e.type) { - case "t": - edge = findEdge(e.id); - edge.target = e.target; - break; - case "s": - edge = findEdge(e.id); - edge.source = e.source; - break; - case "b": - edges.push(e.edge); - break; - } - }); - delete cachedCommunities[commId]; - }, parseResultOfTraversal = function (result, callback) { result = result[0]; var inserted = {}, - n = insertNode(result[0].vertex), + n = absAdapter.insertNode(result[0].vertex), com, buckets; _.each(result, function(visited) { - var node = insertNode(visited.vertex), + var node = absAdapter.insertNode(visited.vertex), path = visited.path; inserted[node._id] = node; _.each(path.vertices, function(connectedNode) { - var ins = insertNode(connectedNode); + var ins = absAdapter.insertNode(connectedNode); inserted[ins._id] = ins; }); _.each(path.edges, function(edge) { - insertEdge(edge); + absAdapter.insertEdge(edge); }); }); delete inserted[n._id]; - if (_.size(inserted) > childLimit) { - buckets = reducer.bucketNodes(_.values(inserted), childLimit); - _.each(buckets, function(b) { - if (b.length > 1) { - var ids = _.map(b, function(n) { - return n._id; - }); - collapseCommunity(ids); - } - }); - } - if (limit < nodes.length) { - com = reducer.getCommunity(limit, n); - collapseCommunity(com); - } + absAdapter.checkSizeOfInserted(inserted); + absAdapter.checkNodeLimit(n); if (callback) { callback(n); } }, - + /* Archive parseResultOfQuery = function (result, callback) { _.each(result, function (node) { var n = findNode(node._id); if (!n) { - insertNode(node); + absAdapter.insertNode(node); n = node; } else { n.children = node.children; @@ -454,14 +157,14 @@ function ArangoAdapter(nodes, edges, config) { var check = findNode(id), newnode; if (check) { - insertEdge(n, check); + absAdapter.insertEdge(n, check); self.requestCentralityChildren(id, function(c) { n._centrality = c; }); } else { newnode = {_id: id}; - insertNode(newnode); - insertEdge(n, newnode); + absAdapter.insertNode(newnode); + absAdapter.insertEdge(n, newnode); self.requestCentralityChildren(id, function(c) { newnode._centrality = c; }); @@ -472,7 +175,7 @@ function ArangoAdapter(nodes, edges, config) { } }); }, - + */ permanentlyRemoveEdgesOfNode = function (nodeId) { sendQuery(queries.connectedEdges, { id: nodeId @@ -536,11 +239,7 @@ function ArangoAdapter(nodes, edges, config) { + " FILTER e._to == @id" + " || e._from == @id" + " RETURN e"; - - childLimit = Number.POSITIVE_INFINITY; - - reducer = new NodeReducer(nodes, edges); - + /* Archive self.oldLoadNodeFromTreeById = function(nodeId, callback) { sendQuery(queries.nodeById, { id: nodeId @@ -548,7 +247,7 @@ function ArangoAdapter(nodes, edges, config) { parseResultOfQuery(res, callback); }); }; - + */ self.loadNode = function(nodeId, callback) { self.loadNodeFromTreeById(nodeId, callback); }; @@ -590,7 +289,7 @@ function ArangoAdapter(nodes, edges, config) { data._from = edgeToAdd.source._id; data._to = edgeToAdd.target._id; delete data.error; - var edge = insertEdge(data); + var edge = absAdapter.insertEdge(data); callback(edge); }, error: function(data) { @@ -608,7 +307,7 @@ function ArangoAdapter(nodes, edges, config) { dataType: "json", processData: false, success: function() { - removeEdge(edgeToRemove); + absAdapter.removeEdge(edgeToRemove); if (callback !== undefined && _.isFunction(callback)) { callback(); } @@ -649,7 +348,7 @@ function ArangoAdapter(nodes, edges, config) { contentType: "application/json", processData: false, success: function(data) { - insertNode(data); + absAdapter.insertNode(data); callback(data); }, error: function(data) { @@ -667,9 +366,9 @@ function ArangoAdapter(nodes, edges, config) { contentType: "application/json", processData: false, success: function() { - removeEdgesForNode(nodeToRemove); + absAdapter.removeEdgesForNode(nodeToRemove); permanentlyRemoveEdgesOfNode(nodeToRemove._id); - removeNode(nodeToRemove); + absAdapter.removeNode(nodeToRemove); if (callback !== undefined && _.isFunction(callback)) { callback(); } @@ -714,22 +413,15 @@ function ArangoAdapter(nodes, edges, config) { }; self.setNodeLimit = function (pLimit, callback) { - limit = pLimit; - if (limit < nodes.length) { - var com = reducer.getCommunity(limit); - collapseCommunity(com); - if (callback !== undefined) { - callback(); - } - } + absAdapter.setNodeLimit(pLimit, callback); }; self.setChildLimit = function (pLimit) { - childLimit = pLimit; + absAdapter.setChildLimit(pLimit); }; self.expandCommunity = function (commNode, callback) { - expandCommunity(commNode); + absAdapter.expandCommunity(commNode); if (callback !== undefined) { callback(); } diff --git a/html/admin/js/graphViewer/jasmine_test/specAdapter/arangoAdapterSpec.js b/html/admin/js/graphViewer/jasmine_test/specAdapter/arangoAdapterSpec.js index f182904938..11727f4fc0 100644 --- a/html/admin/js/graphViewer/jasmine_test/specAdapter/arangoAdapterSpec.js +++ b/html/admin/js/graphViewer/jasmine_test/specAdapter/arangoAdapterSpec.js @@ -558,7 +558,7 @@ waitsFor(function() { return callbackCheck; - }); + }, 1000); runs(function() { existNodes([c0, c1, c2, c3, c4]); @@ -619,7 +619,7 @@ waitsFor(function() { return callbackCheck; - }); + }, 1000); runs(function() { expect(nodes[0]._data).toEqual({ @@ -793,7 +793,7 @@ waitsFor(function() { return callbackCheck; - }); + }, 1000); runs(function() { var callNodesIds = _.map(callNodes, function(n) { @@ -858,7 +858,7 @@ waitsFor(function() { return callbackCheck; - }); + }, 1000); runs(function() { var callNodesIds = _.map(callNodes, function(n) { @@ -1005,7 +1005,7 @@ waitsFor(function() { return callbackCheck; - }); + }, 1000); runs(function() { existNodes([c0, c1, c2, c3, c4, c5, c6, c7]); @@ -1027,7 +1027,7 @@ waitsFor(function() { return callbackCheck; - }); + }, 1000); runs(function() { expect(toPatch._data.hello).toEqual("world"); @@ -1050,7 +1050,7 @@ waitsFor(function() { return callbackCheck; - }); + }, 1000); runs(function() { expect(toPatch._data.hello).toEqual("world"); @@ -1073,7 +1073,7 @@ waitsFor(function() { return callbackCheck; - }); + }, 1000); runs(function() { expect($.ajax).toHaveBeenCalledWith( @@ -1097,7 +1097,7 @@ waitsFor(function() { return callbackCheck; - }); + }, 1000); runs(function() { expect($.ajax).toHaveBeenCalledWith( @@ -1152,7 +1152,7 @@ waitsFor(function() { return callbackCheck; - }); + }, 1000); runs(function() { var commId = getCommunityNodesIds()[0]; @@ -1177,7 +1177,7 @@ waitsFor(function() { return callbackCheck; - }); + }, 1000); runs(function() { var commId = getCommunityNodesIds()[0]; @@ -1236,7 +1236,7 @@ waitsFor(function() { return called === 2; - }); + }, 1000); runs(function() { adapter.loadNode(v2, counterCallback); @@ -1245,7 +1245,7 @@ waitsFor(function() { return called === 3; - }); + }, 1000); runs(function() { var commId = commNode._id; @@ -1264,7 +1264,7 @@ waitsFor(function() { return called === 4; - }); + }, 1000); runs(function() { existNodes([v0, v1, v2, v3, v4]); @@ -1320,7 +1320,7 @@ waitsFor(function() { return called === 2; - }); + }, 1000); runs(function() { adapter.loadNode(v2, counterCallback); @@ -1329,7 +1329,7 @@ waitsFor(function() { return called === 3; - }); + }, 1000); runs(function() { adapter.setNodeLimit(20); @@ -1338,7 +1338,7 @@ waitsFor(function() { return called === 4; - }); + }, 1000); runs(function() { var checkNodeWithInAndOut = function(id, inbound, outbound) { @@ -1413,7 +1413,7 @@ waitsFor(function() { return callbackCheck; - }); + }, 1000); runs(function() { var newCommId = getCommunityNodesIds()[0]; @@ -1476,7 +1476,7 @@ waitsFor(function() { return callbackCheck; - }); + }, 1000); runs(function() { existEdge(c3, firstCommId); @@ -1523,7 +1523,7 @@ waitsFor(function() { return callbackCheck; - }); + }, 1000); runs(function() { existNodes([c0, c1, c2, c3, c4, c8, c9]); @@ -1557,7 +1557,7 @@ waitsFor(function() { return callbackCheck; - }); + }, 1000); runs(function() { expect($.ajax).toHaveBeenCalledWith( @@ -1666,7 +1666,7 @@ waitsFor(function() { return callbackCheck; - }); + }, 1000); runs(function() { callbackCheck = false; diff --git a/html/admin/js/graphViewer/jasmine_test/specAdapter/foxxAdapterSpec.js b/html/admin/js/graphViewer/jasmine_test/specAdapter/foxxAdapterSpec.js new file mode 100644 index 0000000000..7b84161502 --- /dev/null +++ b/html/admin/js/graphViewer/jasmine_test/specAdapter/foxxAdapterSpec.js @@ -0,0 +1,1713 @@ +/*jslint indent: 2, nomen: true, maxlen: 100, white: true plusplus: true */ +/*global beforeEach, afterEach */ +/*global describe, it, expect, jasmine */ +/*global runs, spyOn, waitsFor, waits */ +/*global window, eb, loadFixtures, document */ +/*global $, _, d3*/ +/*global describeInterface*/ +/*global ArangoAdapter*/ + +//////////////////////////////////////////////////////////////////////////////// +/// @brief Graph functionality +/// +/// @file +/// +/// DISCLAIMER +/// +/// Copyright 2010-2012 triagens GmbH, Cologne, Germany +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// +/// Copyright holder is triAGENS GmbH, Cologne, Germany +/// +/// @author Michael Hackstein +/// @author Copyright 2011-2013, triAGENS GmbH, Cologne, Germany +//////////////////////////////////////////////////////////////////////////////// + + +(function () { + "use strict"; + + describe('Foxx Adapter', function () { + + describeInterface(new FoxxAdapter([], [], { + nodeCollection: "", + edgeCollection: "" + })); + + var adapter, + nodes, + edges, + arangodb = "http://localhost:8529", + nodesCollection, + altNodesCollection, + edgesCollection, + altEdgesCollection, + mockCollection, + callbackCheck, + checkCallbackFunction = function() { + callbackCheck = true; + }, + + getCommunityNodes = function() { + return _.filter(nodes, function(n) { + return n._id.match(/^\*community/); + }); + }, + + getCommunityNodesIds = function() { + return _.pluck(getCommunityNodes(), "_id"); + }, + + nodeWithID = function(id) { + return $.grep(nodes, function(e){ + return e._id === id; + })[0]; + }, + edgeWithSourceAndTargetId = function(sourceId, targetId) { + return $.grep(edges, function(e){ + return e.source._id === sourceId + && e.target._id === targetId; + })[0]; + }, + existNode = function(id) { + var node = nodeWithID(id); + expect(node).toBeDefined(); + expect(node._id).toEqual(id); + }, + + notExistNode = function(id) { + var node = nodeWithID(id); + expect(node).toBeUndefined(); + }, + + existEdge = function(source, target) { + var edge = edgeWithSourceAndTargetId(source, target); + expect(edge).toBeDefined(); + expect(edge.source._id).toEqual(source); + expect(edge.target._id).toEqual(target); + }, + + notExistEdge = function(source, target) { + var edge = edgeWithSourceAndTargetId(source, target); + expect(edge).toBeUndefined(); + }, + + existNodes = function(ids) { + _.each(ids, existNode); + }, + + notExistNodes = function(ids) { + _.each(ids, notExistNode); + }, + + insertEdge = function (collectionID, from, to, cont) { + var key = String(Math.floor(Math.random()*100000)), + id = collectionID + "/" + key; + cont = cont || {}; + mockCollection[collectionID] = mockCollection[collectionID] || {}; + mockCollection[collectionID][from] = mockCollection[collectionID][from] || {}; + cont._id = id; + cont._key = key; + cont._rev = key; + cont._from = from; + cont._to = to; + mockCollection[collectionID][from][to] = cont; + return id; + }, + insertNode = function (collectionID, nodeId, cont) { + var key = String(Math.floor(Math.random()*100000)), + id = collectionID + "/" + key; + cont = cont || {}; + mockCollection[collectionID] = mockCollection[collectionID] || {}; + cont.id = nodeId; + cont._id = id; + cont._key = key; + cont._rev = key; + mockCollection[collectionID][id] = cont; + return id; + }, + readEdge = function (collectionID, from, to) { + return mockCollection[collectionID][from._id][to._id]; + }, + readNode = function (collectionID, id) { + return mockCollection[collectionID][id]; + }, + constructPath = function(colNodes, colEdges, from, to) { + var obj = {}, + src = readNode(colNodes, from), + tar = readNode(colNodes, to); + obj.vertex = tar; + obj.path = { + edges: [ + readEdge(colEdges, src, tar) + ], + vertices: [ + src, + tar + ] + }; + return obj; + }; + + beforeEach(function() { + nodes = []; + edges = []; + mockCollection = {}; + nodesCollection = "TestNodes321"; + edgesCollection = "TestEdges321"; + altNodesCollection = "TestNodes654"; + altEdgesCollection = "TestEdges654"; + + this.addMatchers({ + toHaveCorrectCoordinates: function() { + var list = this.actual, + evil; + _.each(list, function(n) { + if (isNaN(n.x) || isNaN(n.y)) { + evil = n; + } + }); + this.message = function() { + return "Expected " + JSON.stringify(evil) + " to contain Numbers as X and Y."; + }; + return evil === undefined; + } + }); + }); + + afterEach(function() { + expect(nodes).toHaveCorrectCoordinates(); + }); + + it('should throw an error if no nodes are given', function() { + expect( + function() { + var t = new ArangoAdapter(); + } + ).toThrow("The nodes have to be given."); + }); + + it('should throw an error if no edges are given', function() { + expect( + function() { + var t = new ArangoAdapter([]); + } + ).toThrow("The edges have to be given."); + }); + + it('should throw an error if no nodeCollection is given', function() { + expect( + function() { + var t = new ArangoAdapter([], [], { + edgeCollection: "" + }); + } + ).toThrow("The nodeCollection has to be given."); + }); + + it('should throw an error if no edgeCollection is given', function() { + expect( + function() { + var t = new ArangoAdapter([], [], { + nodeCollection: "" + }); + } + ).toThrow("The edgeCollection has to be given."); + }); + + it('should not throw an error if everything is given', function() { + expect( + function() { + var t = new ArangoAdapter([], [], { + nodeCollection: "", + edgeCollection: "" + }); + } + ).not.toThrow(); + }); + + it('should automatically determine the host of not given', function() { + var adapter = new ArangoAdapter( + nodes, + edges, + { + nodeCollection: nodesCollection, + edgeCollection: edgesCollection, + width: 100, + height: 40 + } + ), + args, + host; + spyOn($, "ajax"); + adapter.createNode({}, function() {}); + args = $.ajax.mostRecentCall.args[0]; + host = window.location.protocol + "//" + window.location.host; + expect(args.url).toContain(host); + }); + + it('should create a nodeReducer instance', function() { + spyOn(window, "NodeReducer"); + var adapter = new ArangoAdapter( + nodes, + edges, + { + nodeCollection: nodesCollection, + edgeCollection: edgesCollection, + width: 100, + height: 40 + } + ); + expect(window.NodeReducer).wasCalledWith(nodes, edges); + }); + + describe('setup correctly', function() { + + var traversalQuery, + filterQuery, + childrenQuery, + loadGraph, + requests; + + beforeEach(function() { + var self = this, + host = window.location.protocol + "//" + window.location.host, + apibase = host + "/_api/", + apiCursor = apibase + 'cursor'; + self.fakeReducerRequest = function() {}; + self.fakeReducerBucketRequest = function() {}; + spyOn(window, "NodeReducer").andCallFake(function(v, e) { + return { + getCommunity: function(limit, focus) { + if (focus !== undefined) { + return self.fakeReducerRequest(limit, focus); + } + return self.fakeReducerRequest(limit); + }, + bucketNodes: function(toSort, numBuckets) { + return self.fakeReducerBucketRequest(toSort, numBuckets); + } + }; + }); + adapter = new ArangoAdapter( + nodes, + edges, + { + nodeCollection: nodesCollection, + edgeCollection: edgesCollection, + width: 100, + height: 40 + } + ); + traversalQuery = function(id, nods, edgs, undirected) { + var dir; + if (undirected === true) { + dir = "any"; + } else { + dir = "outbound"; + } + return JSON.stringify({ + query: "RETURN TRAVERSAL(@@nodes, @@edges, @id, @dir," + + " {strategy: \"depthfirst\",maxDepth: 1,paths: true})", + bindVars: { + id: id, + "@nodes": nods, + dir: dir, + "@edges": edgs + } + }); + }; + filterQuery = function(v, nods, edgs, undirected) { + var dir; + if (undirected === true) { + dir = "any"; + } else { + dir = "outbound"; + } + return JSON.stringify({ + query: "FOR n IN @@nodes FILTER n.id == @value" + + " RETURN TRAVERSAL(@@nodes, @@edges, n._id, @dir," + + " {strategy: \"depthfirst\",maxDepth: 1,paths: true})", + bindVars: { + value: v, + "@nodes": nods, + dir: dir, + "@edges": edgs + } + }); + }; + childrenQuery = function(id, nods, edgs) { + return JSON.stringify({ + query: "FOR u IN @@nodes FILTER u._id == @id" + + " LET g = ( FOR l in @@edges FILTER l._from == u._id RETURN 1 )" + + " RETURN length(g)", + bindVars: { + id: id, + "@nodes": nods, + "@edges": edgs + } + }); + }; + loadGraph = function(vars) { + var nid = vars.id, + ncol = vars["@nodes"], + ecol = vars["@edges"], + res = [], + inner = [], + first = {}, + node1 = readNode(ncol, nid); + res.push(inner); + first.vertex = node1; + first.path = { + edges: [], + vertices: [ + node1 + ] + }; + inner.push(first); + if (mockCollection[ecol][nid] !== undefined) { + _.each(mockCollection[ecol][nid], function(val, key) { + inner.push(constructPath(ncol, ecol, nid, key)); + }); + } + return res; + }; + + + requests = {}; + requests.cursor = function(data) { + return { + type: 'POST', + url: apiCursor, + data: data, + contentType: 'application/json', + dataType: 'json', + success: jasmine.any(Function), + error: jasmine.any(Function), + processData: false + }; + }; + requests.node = function(col) { + var read = apibase + "document?collection=" + col, + write = apibase + "document/", + base = { + cache: false, + dataType: "json", + contentType: "application/json", + processData: false, + success: jasmine.any(Function), + error: jasmine.any(Function) + }; + return { + create: function(data) { + return $.extend(base, {url: read, type: "POST", data: JSON.stringify(data)}); + }, + patch: function(id, data) { + return $.extend(base, {url: write + id, type: "PUT", data: JSON.stringify(data)}); + }, + del: function(id) { + return $.extend(base, {url: write + id, type: "DELETE"}); + } + }; + }; + requests.edge = function(col) { + var create = apibase + "edge?collection=" + col, + base = { + cache: false, + dataType: "json", + contentType: "application/json", + processData: false, + success: jasmine.any(Function), + error: jasmine.any(Function) + }; + return { + create: function(from, to, data) { + return $.extend(base, { + url: create + "&from=" + from + "&to=" + to, + type: "POST", + data: JSON.stringify(data) + }); + } + }; + }; + }); + + it('should offer lists of available collections', function() { + var collections = [], + sys1 = {id: "1", name: "_sys1", status: 3, type: 2}, + sys2 = {id: "2", name: "_sys2", status: 2, type: 2}, + doc1 = {id: "3", name: "doc1", status: 3, type: 2}, + doc2 = {id: "4", name: "doc2", status: 2, type: 2}, + doc3 = {id: "5", name: "doc3", status: 3, type: 2}, + edge1 = {id: "6", name: "edge1", status: 3, type: 3}, + edge2 = {id: "7", name: "edge2", status: 2, type: 3}; + + collections.push(sys1); + collections.push(sys2); + collections.push(doc1); + collections.push(doc2); + collections.push(doc3); + collections.push(edge1); + collections.push(edge2); + + spyOn($, "ajax").andCallFake(function(request) { + request.success({collections: collections}); + }); + + adapter.getCollections(function(docs, edge) { + expect(docs).toContain("doc1"); + expect(docs).toContain("doc2"); + expect(docs).toContain("doc3"); + + expect(docs.length).toEqual(3); + + expect(edge).toContain("edge1"); + expect(edge).toContain("edge2"); + + expect(edge.length).toEqual(2); + }); + }); + + it('should be able to load a tree node from ' + + 'ArangoDB by internal _id attribute', function() { + + var c0, c1, c2, c3, c4; + + runs(function() { + spyOn($, "ajax").andCallFake(function(request) { + var vars = JSON.parse(request.data).bindVars; + if (vars !== undefined) { + request.success({result: loadGraph(vars)}); + } + }); + + c0 = insertNode(nodesCollection, 0); + c1 = insertNode(nodesCollection, 1); + c2 = insertNode(nodesCollection, 2); + c3 = insertNode(nodesCollection, 3); + c4 = insertNode(nodesCollection, 4); + + insertEdge(edgesCollection, c0, c1); + insertEdge(edgesCollection, c0, c2); + insertEdge(edgesCollection, c0, c3); + insertEdge(edgesCollection, c0, c4); + + callbackCheck = false; + adapter.loadNodeFromTreeById(c0, checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }, 1000); + + runs(function() { + existNodes([c0, c1, c2, c3, c4]); + expect(nodes.length).toEqual(5); + expect($.ajax).toHaveBeenCalledWith( + requests.cursor(traversalQuery(c0, nodesCollection, edgesCollection)) + ); + }); + }); + + it('should map loadNode to loadByID', function() { + spyOn(adapter, "loadNodeFromTreeById"); + adapter.loadNode("a", "b"); + expect(adapter.loadNodeFromTreeById).toHaveBeenCalledWith("a", "b"); + }); + + it('should be able to load a tree node from ArangoDB' + + ' by internal attribute and value', function() { + + var c0, c1, c2, c3, c4; + + runs(function() { + spyOn($, "ajax").andCallFake(function(request) { + var vars = JSON.parse(request.data).bindVars; + if (vars !== undefined) { + vars.id = c0; + request.success({result: loadGraph(vars)}); + } + }); + + + c0 = insertNode(nodesCollection, 0); + c1 = insertNode(nodesCollection, 1); + c2 = insertNode(nodesCollection, 2); + c3 = insertNode(nodesCollection, 3); + c4 = insertNode(nodesCollection, 4); + + insertEdge(edgesCollection, c0, c1); + insertEdge(edgesCollection, c0, c2); + insertEdge(edgesCollection, c0, c3); + insertEdge(edgesCollection, c0, c4); + + callbackCheck = false; + adapter.loadNodeFromTreeByAttributeValue("id", 0, checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + existNodes([c0, c1, c2, c3, c4]); + expect(nodes.length).toEqual(5); + expect($.ajax).toHaveBeenCalledWith( + requests.cursor(filterQuery(0, nodesCollection, edgesCollection)) + ); + }); + }); + + it('should be able to request the number of children centrality', function() { + var c0, + children; + runs(function() { + c0 = insertNode(nodesCollection, 0); + spyOn($, "ajax").andCallFake(function(request) { + request.success({result: [4]}); + }); + + callbackCheck = false; + adapter.requestCentralityChildren(c0, function(count) { + callbackCheck = true; + children = count; + }); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + expect(children).toEqual(4); + expect($.ajax).toHaveBeenCalledWith( + requests.cursor(childrenQuery(c0, nodesCollection, edgesCollection)) + ); + }); + }); + + it('should encapsulate all attributes of nodes and edges in _data', function() { + var c0, c1, e1_2; + + runs(function() { + + spyOn($, "ajax").andCallFake(function(request) { + var vars = JSON.parse(request.data).bindVars; + if (vars !== undefined) { + request.success({result: loadGraph(vars)}); + } + }); + + c0 = insertNode(nodesCollection, 0, {name: "Alice", age: 42}); + c1 = insertNode(nodesCollection, 1, {name: "Bob", age: 1337}); + e1_2 = insertEdge(edgesCollection, c0, c1, {label: "knows"}); + + callbackCheck = false; + adapter.loadNodeFromTreeById(c0, checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + expect(nodes[0]._data).toEqual({ + _id: c0, + _key: jasmine.any(String), + _rev: jasmine.any(String), + id: 0, + name: "Alice", + age: 42 + }); + expect(nodes[1]._data).toEqual({ + _id: c1, + _key: jasmine.any(String), + _rev: jasmine.any(String), + id: 1, + name: "Bob", + age: 1337 + }); + expect(edges[0]._data).toEqual({ + _id: e1_2, + _from: c0, + _to: c1, + _key: jasmine.any(String), + _rev: jasmine.any(String), + label: "knows" + }); + expect($.ajax).toHaveBeenCalledWith( + requests.cursor(traversalQuery(c0, nodesCollection, edgesCollection)) + ); + }); + + + }); + + it('should be able to switch to different collections', function() { + var c0, c1, e1_2, insertedId; + + runs(function() { + + spyOn($, "ajax").andCallFake(function(request) { + var vars = JSON.parse(request.data).bindVars; + if (vars !== undefined) { + request.success({result: loadGraph(vars)}); + } else { + request.success({result: {}}); + } + }); + + c0 = insertNode(altNodesCollection, 0); + c1 = insertNode(altNodesCollection, 1); + e1_2 = insertEdge(altEdgesCollection, c0, c1); + + adapter.changeTo(altNodesCollection, altEdgesCollection); + + callbackCheck = false; + adapter.loadNodeFromTreeById(c0, checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }, 1000); + + runs(function() { + existNodes([c0, c1]); + expect(nodes.length).toEqual(2); + expect($.ajax).toHaveBeenCalledWith( + requests.cursor(traversalQuery(c0, altNodesCollection, altEdgesCollection)) + ); + + callbackCheck = false; + adapter.createNode({}, function(node) { + insertedId = node._id; + callbackCheck = true; + }); + }); + + waitsFor(function() { + return callbackCheck; + }, 1000); + + runs(function() { + existNode(insertedId); + expect($.ajax).toHaveBeenCalledWith( + requests.node(altNodesCollection).create({}) + ); + }); + + }); + + it('should be able to switch to different collections and change to directed', function() { + + runs(function() { + + spyOn($, "ajax"); + + adapter.changeTo(altNodesCollection, altEdgesCollection, false); + + adapter.loadNode("42"); + + expect($.ajax).toHaveBeenCalledWith( + requests.cursor(traversalQuery("42", altNodesCollection, altEdgesCollection, false)) + ); + + }); + }); + + it('should be able to switch to different collections' + + ' and change to undirected', function() { + + runs(function() { + + spyOn($, "ajax"); + + adapter.changeTo(altNodesCollection, altEdgesCollection, true); + + adapter.loadNode("42"); + + expect($.ajax).toHaveBeenCalledWith( + requests.cursor(traversalQuery("42", altNodesCollection, altEdgesCollection, true)) + ); + + }); + }); + + it('should add at most the upper bound of children in one step', function() { + var inNodeCol, callNodes; + + runs(function() { + var addNNodes = function(n) { + var i = 0, + res = []; + for (i = 0; i < n; i++) { + res.push(insertNode(nodesCollection, i)); + } + return res; + }, + connectToAllButSelf = function(source, ns) { + _.each(ns, function(target) { + if (source !== target) { + insertEdge(edgesCollection, source, target); + } + }); + }; + + inNodeCol = addNNodes(21); + connectToAllButSelf(inNodeCol[0], inNodeCol); + adapter.setChildLimit(5); + + spyOn($, "ajax").andCallFake(function(request) { + var vars = JSON.parse(request.data).bindVars; + if (vars !== undefined) { + request.success({result: loadGraph(vars)}); + } + }); + spyOn(this, "fakeReducerBucketRequest").andCallFake(function(ns) { + var i = 0, + res = [], + pos; + callNodes = ns; + for (i = 0; i < 5; i++) { + pos = i*4; + res.push(ns.slice(pos, pos + 4)); + } + return res; + }); + + callbackCheck = false; + adapter.loadNodeFromTreeById(inNodeCol[0], checkCallbackFunction); + + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + var callNodesIds = _.map(callNodes, function(n) { + return n._id; + }); + expect(this.fakeReducerBucketRequest).toHaveBeenCalledWith( + jasmine.any(Array), + 5 + ); + expect(callNodesIds).toEqual(inNodeCol.slice(1)); + expect(nodes.length).toEqual(6); + expect(getCommunityNodes().length).toEqual(5); + }); + + }); + + it('should not replace single nodes by communities', function() { + var inNodeCol, callNodes; + + runs(function() { + var addNNodes = function(n) { + var i = 0, + res = []; + for (i = 0; i < n; i++) { + res.push(insertNode(nodesCollection, i)); + } + return res; + }, + connectToAllButSelf = function(source, ns) { + _.each(ns, function(target) { + if (source !== target) { + insertEdge(edgesCollection, source, target); + } + }); + }; + + inNodeCol = addNNodes(7); + connectToAllButSelf(inNodeCol[0], inNodeCol); + adapter.setChildLimit(5); + + spyOn($, "ajax").andCallFake(function(request) { + var vars = JSON.parse(request.data).bindVars; + if (vars !== undefined) { + request.success({result: loadGraph(vars)}); + } + }); + spyOn(this, "fakeReducerBucketRequest").andCallFake(function(ns) { + var i = 0, + res = [], + pos; + for (i = 0; i < 4; i++) { + res.push([ns[i]]); + } + res.push([ns[4], ns[5]]); + return res; + }); + + callbackCheck = false; + adapter.loadNodeFromTreeById(inNodeCol[0], checkCallbackFunction); + + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + var callNodesIds = _.map(callNodes, function(n) { + return n._id; + }); + expect(this.fakeReducerBucketRequest).toHaveBeenCalledWith( + jasmine.any(Array), + 5 + ); + expect(nodes.length).toEqual(6); + expect(getCommunityNodes().length).toEqual(1); + }); + + }); + + describe('that has already loaded one graph', function() { + var c0, c1, c2, c3, c4, c5, c6, c7, + fakeResult, spyHook; + + + beforeEach(function() { + + runs(function() { + spyOn($, "ajax").andCallFake(function(request) { + if (spyHook !== undefined) { + if(!spyHook(request)) { + return; + } + } + if (request.url.indexOf("cursor", request.url.length - "cursor".length) !== -1) { + var vars = JSON.parse(request.data).bindVars; + if (vars !== undefined) { + request.success({result: loadGraph(vars)}); + } + } else { + request.success(fakeResult); + } + + }); + c0 = insertNode(nodesCollection, 0); + c1 = insertNode(nodesCollection, 1); + c2 = insertNode(nodesCollection, 2); + c3 = insertNode(nodesCollection, 3); + c4 = insertNode(nodesCollection, 4); + c5 = insertNode(nodesCollection, 5); + c6 = insertNode(nodesCollection, 6); + c7 = insertNode(nodesCollection, 7); + + insertEdge(edgesCollection, c0, c1); + insertEdge(edgesCollection, c0, c2); + insertEdge(edgesCollection, c0, c3); + insertEdge(edgesCollection, c0, c4); + insertEdge(edgesCollection, c1, c5); + insertEdge(edgesCollection, c1, c6); + insertEdge(edgesCollection, c1, c7); + + callbackCheck = false; + adapter.loadNodeFromTreeById(c0, checkCallbackFunction); + + this.addMatchers({ + toBeStoredPermanently: function() { + var id = this.actual, + res = false; + $.ajax({ + type: "GET", + url: arangodb + "/_api/document/" + id, + contentType: "application/json", + processData: false, + async: false, + success: function(data) { + res = true; + }, + error: function(data) { + try { + var temp = JSON.parse(data); + throw "[" + temp.errorNum + "] " + temp.errorMessage; + } + catch (e) { + throw "Undefined ERROR"; + } + } + }); + return res; + }, + + toNotBeStoredPermanently: function() { + var id = this.actual, + res = false; + $.ajax({ + type: "GET", + url: arangodb + "/_api/document/" + id, + contentType: "application/json", + processData: false, + async: false, + success: function(data) { + + }, + error: function(data) { + if (data.status === 404) { + res = true; + } + + } + }); + return res; + }, + + toHavePermanentAttributeWithValue: function(attribute, value) { + var id = this.actual, + res = false; + $.ajax({ + type: "GET", + url: arangodb + "/_api/document/" + id, + contentType: "application/json", + processData: false, + async: false, + success: function(data) { + if (data[attribute] === value) { + res = true; + } + }, + error: function(data) { + } + }); + return res; + } + }); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + callbackCheck = false; + }); + }); + + it('should be able to add nodes from another query', function() { + + runs(function() { + adapter.loadNodeFromTreeById(c1, checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + existNodes([c0, c1, c2, c3, c4, c5, c6, c7]); + expect(nodes.length).toEqual(8); + expect($.ajax).toHaveBeenCalledWith( + requests.cursor(traversalQuery(c1, nodesCollection, edgesCollection)) + ); + }); + }); + + it('should be able to change a value of one node permanently', function() { + var toPatch; + + runs(function() { + fakeResult = {hello: "world"}; + toPatch = nodeWithID(c0); + adapter.patchNode(toPatch, {hello: "world"}, checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + expect(toPatch._data.hello).toEqual("world"); + expect($.ajax).toHaveBeenCalledWith( + requests.node(nodesCollection).patch(c0, fakeResult) + ); + + }); + + }); + + it('should be able to change a value of one edge permanently', function() { + var toPatch; + + runs(function() { + fakeResult = {hello: "world"}; + toPatch = edgeWithSourceAndTargetId(c0, c1); + adapter.patchEdge(toPatch, {hello: "world"}, checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + expect(toPatch._data.hello).toEqual("world"); + expect($.ajax).toHaveBeenCalledWith( + requests.node(edgesCollection).patch(toPatch._id, fakeResult) + ); + }); + }); + + it('should be able to remove an edge permanently', function() { + + var toDelete; + + runs(function() { + fakeResult = ""; + toDelete = edgeWithSourceAndTargetId(c0, c4); + existEdge(c0, c4); + adapter.deleteEdge(toDelete, checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + expect($.ajax).toHaveBeenCalledWith( + requests.node(edgesCollection).del(toDelete._id) + ); + notExistEdge(c0, c4); + }); + + }); + + it('should be able to add a node permanently', function() { + + var insertedId; + + runs(function() { + adapter.createNode({}, function(node) { + insertedId = node._id; + callbackCheck = true; + }); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + expect($.ajax).toHaveBeenCalledWith( + requests.node(nodesCollection).create({}) + ); + + existNode(insertedId); + }); + }); + + it('should trigger the reducer if too many nodes are added', function() { + + runs(function() { + adapter.setNodeLimit(6); + spyOn(this, "fakeReducerRequest").andCallFake(function() { + return [c0]; + }); + adapter.loadNodeFromTreeById(c1, checkCallbackFunction); + expect(this.fakeReducerRequest).toHaveBeenCalledWith(6, nodeWithID(c1)); + }); + }); + + + describe('checking community nodes', function() { + + it('should not trigger the reducer if the limit is set large enough', function() { + spyOn(this, "fakeReducerRequest").andCallFake(function() { + return [c0]; + }); + adapter.setNodeLimit(10); + expect(this.fakeReducerRequest).not.toHaveBeenCalled(); + }); + + it('should trigger the reducer if the limit is set too small', function() { + spyOn(this, "fakeReducerRequest").andCallFake(function() { + return [c0]; + }); + adapter.setNodeLimit(2); + expect(this.fakeReducerRequest).toHaveBeenCalledWith(2); + }); + + it('should create a community node if limit is set too small', function() { + var called; + + runs(function() { + callbackCheck = false; + spyOn(this, "fakeReducerRequest").andCallFake(function() { + return [c0, c1, c2]; + }); + adapter.setNodeLimit(2, checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + var commId = getCommunityNodesIds()[0]; + notExistNodes([c0, c1, c2]); + existNode(commId); + existNodes([c3, c4]); + expect(nodes.length).toEqual(3); + existEdge(commId, c3); + existEdge(commId, c4); + expect(edges.length).toEqual(2); + }); + }); + + it('should create a community node if too many nodes are added', function() { + runs(function() { + adapter.setNodeLimit(6); + spyOn(this, "fakeReducerRequest").andCallFake(function() { + return [c0, c1, c2, c3]; + }); + adapter.loadNodeFromTreeById(c1, checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + var commId = getCommunityNodesIds()[0]; + notExistNodes([c0, c1, c2, c3]); + existNode(commId); + existNodes([c4, c5, c6, c7]); + expect(nodes.length).toEqual(5); + + existEdge(commId, c4); + existEdge(commId, c5); + existEdge(commId, c6); + existEdge(commId, c7); + expect(edges.length).toEqual(4); + }); + + }); + + describe('expanding after a while', function() { + + it('should connect edges of internal nodes accordingly', function() { + + var commNode, called, counterCallback, + v0, v1, v2, v3, v4, + e0_1, e0_2, e1_3, e1_4, e2_3, e2_4; + + runs(function() { + var v = "vertices", + e = "edges"; + nodes.length = 0; + edges.length = 0; + v0 = insertNode(v, 0); + v1 = insertNode(v, 1); + v2 = insertNode(v, 2); + v3 = insertNode(v, 3); + v4 = insertNode(v, 4); + e0_1 = insertEdge(e, v0, v1); + e0_2 = insertEdge(e, v0, v2); + e1_3 = insertEdge(e, v1, v3); + e1_4 = insertEdge(e, v1, v4); + e2_3 = insertEdge(e, v2, v3); + e2_4 = insertEdge(e, v2, v4); + called = 0; + counterCallback = function() { + called++; + }; + spyOn(this, "fakeReducerRequest").andCallFake(function() { + return [v1, v3, v4]; + }); + adapter.setNodeLimit(3); + + adapter.changeTo(v, e); + adapter.loadNode(v0, counterCallback); + adapter.loadNode(v1, counterCallback); + + }); + + waitsFor(function() { + return called === 2; + }); + + runs(function() { + adapter.loadNode(v2, counterCallback); + commNode = getCommunityNodes()[0]; + }); + + waitsFor(function() { + return called === 3; + }); + + runs(function() { + var commId = commNode._id; + // Check start condition + existNodes([commId, v0, v2]); + expect(nodes.length).toEqual(3); + + existEdge(v0, v2); + existEdge(v0, commId); + existEdge(v2, commId); + expect(edges.length).toEqual(4); + + adapter.setNodeLimit(20); + adapter.expandCommunity(commNode, counterCallback); + }); + + waitsFor(function() { + return called === 4; + }); + + runs(function() { + existNodes([v0, v1, v2, v3, v4]); + expect(nodes.length).toEqual(5); + + existEdge(v0, v1); + existEdge(v0, v2); + existEdge(v1, v3); + existEdge(v1, v4); + existEdge(v2, v3); + existEdge(v2, v4); + expect(edges.length).toEqual(6); + + }); + }); + + it('set inbound and outboundcounter correctly', function() { + + var commNode, called, counterCallback, + v0, v1, v2, v3, v4, + e0_1, e0_2, e1_3, e1_4, e2_3, e2_4; + + runs(function() { + var v = "vertices", + e = "edges"; + nodes.length = 0; + edges.length = 0; + v0 = insertNode(v, 0); + v1 = insertNode(v, 1); + v2 = insertNode(v, 2); + v3 = insertNode(v, 3); + v4 = insertNode(v, 4); + e0_1 = insertEdge(e, v0, v1); + e0_2 = insertEdge(e, v0, v2); + e1_3 = insertEdge(e, v1, v3); + e1_4 = insertEdge(e, v1, v4); + e2_3 = insertEdge(e, v2, v3); + e2_4 = insertEdge(e, v2, v4); + called = 0; + counterCallback = function() { + called++; + }; + spyOn(this, "fakeReducerRequest").andCallFake(function() { + return [v1, v3, v4]; + }); + adapter.setNodeLimit(3); + + adapter.changeTo(v, e); + adapter.loadNode(v0, counterCallback); + adapter.loadNode(v1, counterCallback); + + }); + + waitsFor(function() { + return called === 2; + }); + + runs(function() { + adapter.loadNode(v2, counterCallback); + commNode = getCommunityNodes()[0]; + }); + + waitsFor(function() { + return called === 3; + }); + + runs(function() { + adapter.setNodeLimit(20); + adapter.expandCommunity(commNode, counterCallback); + }); + + waitsFor(function() { + return called === 4; + }); + + runs(function() { + var checkNodeWithInAndOut = function(id, inbound, outbound) { + var n = nodeWithID(id); + expect(n._outboundCounter).toEqual(outbound); + expect(n._inboundCounter).toEqual(inbound); + }; + checkNodeWithInAndOut(v0, 0, 2); + checkNodeWithInAndOut(v1, 1, 2); + checkNodeWithInAndOut(v2, 1, 2); + checkNodeWithInAndOut(v3, 2, 0); + checkNodeWithInAndOut(v4, 2, 0); + }); + }); + + }); + + describe('that displays a community node already', function() { + + var firstCommId, + fakeResult; + + beforeEach(function() { + runs(function() { + callbackCheck = false; + adapter.setNodeLimit(7); + fakeResult = [c0, c2]; + spyOn(this, "fakeReducerRequest").andCallFake(function() { + return fakeResult; + }); + adapter.loadNodeFromTreeById(c1, checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + firstCommId = getCommunityNodesIds()[0]; + }); + }); + + it('should expand a community if enough space is available', function() { + runs(function() { + adapter.setNodeLimit(10); + callbackCheck = false; + adapter.expandCommunity(nodeWithID(firstCommId), checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + expect(getCommunityNodes().length).toEqual(0); + existNodes([c0, c1, c2, c3, c4, c5, c6, c7]); + existEdge(c0, c1); + existEdge(c0, c2); + existEdge(c0, c3); + existEdge(c0, c4); + }); + + }); + + it('should expand a community and join another ' + + 'one if not enough space is available', function() { + runs(function() { + fakeResult = [c1, c7]; + callbackCheck = false; + adapter.expandCommunity(nodeWithID(firstCommId), checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + var newCommId = getCommunityNodesIds()[0]; + expect(getCommunityNodes().length).toEqual(1); + existNodes([c0, c2, c3, c4, c5, c6, newCommId]); + notExistNodes([c1, c7]); + + existEdge(c0, c2); + existEdge(c0, c3); + existEdge(c0, c4); + + existEdge(c0, newCommId); + existEdge(newCommId, c5); + existEdge(newCommId, c6); + }); + }); + + it('should join another community if space is further reduced', function() { + runs(function() { + fakeResult = [c1, c7]; + callbackCheck = false; + adapter.setNodeLimit(6, checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + expect(getCommunityNodes().length).toEqual(2); + var ids = getCommunityNodesIds(), + newCommId; + + if (firstCommId === ids[0]) { + newCommId = ids[1]; + } else { + newCommId = ids[0]; + } + + existNodes([c3, c4, c5, c6, firstCommId, newCommId]); + notExistNodes([c0, c1, c2, c7]); + + existEdge(firstCommId, c3); + existEdge(firstCommId, c4); + existEdge(firstCommId, newCommId); + existEdge(newCommId, c5); + existEdge(newCommId, c6); + }); + }); + + it('should connect edges to internal nodes', function() { + + runs(function() { + insertEdge(edgesCollection, c3, c0); + + adapter.setNodeLimit(20); + callbackCheck = false; + adapter.loadNode(c3, checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + existEdge(c3, firstCommId); + }); + + }); + + }); + + }); + + describe('that has loaded several queries', function() { + var c8, c9, e2_8; + + beforeEach(function() { + + runs(function() { + c8 = insertNode(nodesCollection, 8); + c9 = insertNode(nodesCollection, 9); + + e2_8 = insertEdge(edgesCollection, c2, c8); + insertEdge(edgesCollection, c3, c8); + insertEdge(edgesCollection, c3, c9); + + callbackCheck = false; + adapter.loadNodeFromTreeById(c2, checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + callbackCheck = false; + }); + + }); + + it('should not add a node to the list twice', function() { + + runs(function() { + adapter.loadNodeFromTreeById(c3, checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + existNodes([c0, c1, c2, c3, c4, c8, c9]); + expect(nodes.length).toEqual(7); + }); + }); + + it('should be able to add an edge permanently', function() { + var insertedId, + source, + target, + insertedEdge; + + + runs(function() { + source = nodeWithID(c0); + target = nodeWithID(c8); + fakeResult = { + _id: edgesCollection + "/123", + _key: "123", + _rev: "123", + _from: source._id, + _to: target._id + }; + adapter.createEdge({source: source, target: target}, function(edge) { + insertedId = edge._id; + callbackCheck = true; + insertedEdge = edge; + }); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + expect($.ajax).toHaveBeenCalledWith( + requests.edge(edgesCollection).create(source._id, target._id, {}) + ); + existEdge(source._id, target._id); + expect(insertedEdge).toEqual({ + source: source, + target: target, + _id: insertedId, + _data: { + _id: insertedId, + _from: source._id, + _to: target._id, + _rev: jasmine.any(String), + _key: jasmine.any(String) + } + }); + }); + + }); + + it('should be able to remove a node and all connected edges permanently', function() { + var toDelete; + runs(function() { + spyHook = function(request) { + if (request.data !== undefined) { + request.success({result: [ + {_id: e2_8} + ]}); + return false; + } + return true; + }; + fakeResult = ""; + toDelete = nodeWithID(c2); + adapter.deleteNode(toDelete, checkCallbackFunction); + }); + + waits(2000); + + runs(function() { + expect($.ajax).toHaveBeenCalledWith( + requests.node(nodesCollection).del(toDelete._id) + ); + notExistNode(c2); + expect($.ajax).toHaveBeenCalledWith( + requests.node(edgesCollection).del(e2_8) + ); + notExistEdge(c2, c8); + }); + }); + + }); + + }); + + describe('displaying only parts of the graph', function() { + + it('should be able to remove a node and all ' + + 'connected edges including not visible ones', function() { + var s0, s1, t0, toDel, + s0_toDel, s1_toDel, toDel_t0; + + runs(function() { + + callbackCheck = false; + s0 = insertNode(nodesCollection, 0); + s1 = insertNode(nodesCollection, 1); + t0 = insertNode(nodesCollection, 2); + toDel = insertNode(nodesCollection, 3); + + s0_toDel = insertEdge(edgesCollection, s0, toDel); + s1_toDel = insertEdge(edgesCollection, s1, toDel); + toDel_t0 = insertEdge(edgesCollection, toDel, t0); + + var loaded = false, + fakeResult = ""; + + spyOn($, "ajax").andCallFake(function(request) { + if (request.url.indexOf("cursor", request.url.length - "cursor".length) !== -1) { + if (!loaded) { + var vars = JSON.parse(request.data).bindVars; + if (vars !== undefined) { + loaded = true; + request.success({result: loadGraph(vars)}); + } + } else { + request.success({result: [ + { + _id: s0_toDel + },{ + _id: s1_toDel + },{ + _id: toDel_t0 + } + ]}); + } + } else { + request.success(fakeResult); + } + }); + + adapter.loadNodeFromTreeById(s0, checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + callbackCheck = false; + adapter.deleteNode(nodeWithID(toDel), checkCallbackFunction); + }); + + // Wait 2 seconds as no handle for the deletion of edges exists. + waits(2000); + + runs(function() { + notExistNodes([toDel, s1, t0]); + existNode(s0); + + notExistEdge(s0, toDel); + notExistEdge(s1, toDel); + notExistEdge(toDel, t0); + + expect($.ajax).toHaveBeenCalledWith( + requests.node(nodesCollection).del(toDel) + ); + expect($.ajax).toHaveBeenCalledWith( + requests.node(edgesCollection).del(s0_toDel) + ); + expect($.ajax).toHaveBeenCalledWith( + requests.node(edgesCollection).del(s1_toDel) + ); + expect($.ajax).toHaveBeenCalledWith( + requests.node(edgesCollection).del(toDel_t0) + ); + + // Check if counter is set correctly + expect(nodeWithID(s0)._outboundCounter).toEqual(0); + }); + + }); + + }); + + + }); + + }); + +}()); \ No newline at end of file