How to use edges method in tracetest

Best JavaScript code snippet using tracetest

Network.test.js

Source:Network.test.js Github

copy

Full Screen

1/**2 *3 * Useful during debugging4 * =======================5 *6 * console.log(JSON.stringify(output, null, 2));7 *8 * for (let i in network.body.edges) {9 * let edge = network.body.edges[i];10 * console.log("" + i + ": from: " + edge.fromId + ", to: " + edge.toId);11 * }12 */13var fs = require('fs');14var assert = require('assert');15var vis = require('../dist/vis');16var Network = vis.network;17var jsdom_global = require('jsdom-global');18var stdout = require('test-console').stdout;19var Validator = require("./../lib/shared/Validator").default;20var {allOptions, configureOptions} = require('./../lib/network/options.js');21//var {printStyle} = require('./../lib/shared/Validator');22/**23 * Merge all options of object b into object b24 * @param {Object} a25 * @param {Object} b26 * @return {Object} a27 *28 * Adapted merge() in dotparser.js29 */30function merge (a, b) {31 if (!a) {32 a = {};33 }34 if (b) {35 for (var name in b) {36 if (b.hasOwnProperty(name)) {37 if (typeof b[name] === 'object') {38 a[name] = merge(a[name], b[name]);39 } else {40 a[name] = b[name];41 }42 }43 }44 }45 return a;46}47/**48 * Load legacy-style (i.e. not module) javascript files into the given context.49 */50function include(list, context) {51 if (!(list instanceof Array)) {52 list = [list];53 }54 for (var n in list) {55 var path = list[n];56 var arr = [fs.readFileSync(path) + ''];57 eval.apply(context, arr);58 }59}60/**61 * Defined network consists of two sub-networks:62 *63 * - 1-2-3-464 * - 11-12-13-1465 *66 * For reference, this is the sample network of issue #121867 */68function createSampleNetwork(options) {69 var NumInitialNodes = 8;70 var NumInitialEdges = 6;71 var nodes = new vis.DataSet([72 {id: 1, label: '1'},73 {id: 2, label: '2'},74 {id: 3, label: '3'},75 {id: 4, label: '4'},76 {id: 11, label: '11'},77 {id: 12, label: '12'},78 {id: 13, label: '13'},79 {id: 14, label: '14'},80 ]);81 var edges = new vis.DataSet([82 {from: 1, to: 2},83 {from: 2, to: 3},84 {from: 3, to: 4},85 {from: 11, to: 12},86 {from: 12, to: 13},87 {from: 13, to: 14},88 ]); 89 // create a network90 var container = document.getElementById('mynetwork');91 var data = {92 nodes: nodes,93 edges: edges94 };95 var defaultOptions = {96 layout: {97 randomSeed: 898 },99 edges: {100 smooth: {101 type: 'continuous' // avoid dynamic here, it adds extra hidden nodes102 }103 }104 };105 options = merge(defaultOptions, options);106 var network = new vis.Network(container, data, options);107 assertNumNodes(network, NumInitialNodes);108 assertNumEdges(network, NumInitialEdges);109 return [network, data, NumInitialNodes, NumInitialEdges];110};111/**112 * Create a cluster for the dynamic data change cases.113 *114 * Works on the network created by createSampleNetwork().115 *116 * This is actually a pathological case; there are two separate sub-networks and 117 * a cluster is made of two nodes, each from one of the sub-networks.118 */119function createCluster(network) {120 var clusterOptionsByData = {121 joinCondition: function(node) {122 if (node.id == 1 || node.id == 11) return true;123 return false;124 },125 clusterNodeProperties: {id:"c1", label:'c1'}126 }127 network.cluster(clusterOptionsByData);128}129/**130 * Display node/edge state, useful during debugging131 */132function log(network) {133 console.log(Object.keys(network.body.nodes));134 console.log(network.body.nodeIndices);135 console.log(Object.keys(network.body.edges));136 console.log(network.body.edgeIndices);137};138/**139 * Note that only the node and edges counts are asserted.140 * This might be done more thoroughly by explicitly checking the id's141 */142function assertNumNodes(network, expectedPresent, expectedVisible) {143 if (expectedVisible === undefined) expectedVisible = expectedPresent;144 assert.equal(Object.keys(network.body.nodes).length, expectedPresent, "Total number of nodes does not match");145 assert.equal(network.body.nodeIndices.length, expectedVisible, "Number of visible nodes does not match");146};147/**148 * Comment at assertNumNodes() also applies.149 */150function assertNumEdges(network, expectedPresent, expectedVisible) {151 if (expectedVisible === undefined) expectedVisible = expectedPresent;152 assert.equal(Object.keys(network.body.edges).length, expectedPresent, "Total number of edges does not match");153 assert.equal(network.body.edgeIndices.length, expectedVisible, "Number of visible edges does not match");154};155/**156 * Check if the font options haven't changed.157 *158 * This is to guard against future code changes; a lot of the code deals with particular properties of159 * the font options.160 * If any assertion fails here, all code in Network handling fonts should be checked.161 */162function checkFontProperties(fontItem, checkStrict = true) {163 var knownProperties = [164 'color',165 'size',166 'face',167 'background',168 'strokeWidth',169 'strokeColor',170 'align',171 'multi',172 'vadjust',173 'bold',174 'boldital',175 'ital',176 'mono',177 ];178 // All properties in fontItem should be known179 for (var prop in fontItem) {180 if (prop === '__type__') continue; // Skip special field in options definition181 if (!fontItem.hasOwnProperty(prop)) continue;182 assert(knownProperties.indexOf(prop) !== -1, "Unknown font option '" + prop + "'");183 }184 if (!checkStrict) return;185 // All known properties should be present186 var keys = Object.keys(fontItem);187 for (var n in knownProperties) {188 var prop = knownProperties[n];189 assert(keys.indexOf(prop) !== -1, "Missing known font option '" + prop + "'");190 }191}192describe('Network', function () {193 before(function() {194 this.jsdom_global = jsdom_global(195 "<div id='mynetwork'></div>",196 { skipWindowCheck: true}197 );198 this.container = document.getElementById('mynetwork');199 });200 after(function() {201 try {202 this.jsdom_global();203 } catch(e) {204 if (e.message() === 'window is undefined') {205 console.warning("'" + e.message() + "' happened again");206 } else {207 throw e;208 }209 }210 });211/////////////////////////////////////////////////////212// Local helper methods for Edge and Node testing213/////////////////////////////////////////////////////214 /**215 * Simplify network creation for local tests216 */217 function createNetwork(options) {218 var [network, data, numNodes, numEdges] = createSampleNetwork(options);219 return network;220 }221 function firstNode(network) {222 for (var id in network.body.nodes) {223 return network.body.nodes[id];224 }225 return undefined;226 }227 function firstEdge(network) {228 for (var id in network.body.edges) {229 return network.body.edges[id];230 }231 return undefined;232 }233 function checkChooserValues(item, chooser, labelChooser) {234 if (chooser === 'function') {235 assert.equal(typeof item.chooser, 'function');236 } else {237 assert.equal(item.chooser, chooser);238 }239 if (labelChooser === 'function') {240 assert.equal(typeof item.labelModule.fontOptions.chooser, 'function');241 } else {242 assert.equal(item.labelModule.fontOptions.chooser, labelChooser);243 }244 }245/////////////////////////////////////////////////////246// End Local helper methods for Edge and Node testing247/////////////////////////////////////////////////////248 /**249 * Helper function for clustering250 */251 function clusterTo(network, clusterId, nodeList, allowSingle) { 252 var options = {253 joinCondition: function(node) {254 return nodeList.indexOf(node.id) !== -1;255 },256 clusterNodeProperties: {257 id: clusterId,258 label: clusterId259 }260 }261 if (allowSingle === true) {262 options.clusterNodeProperties.allowSingleNodeCluster = true263 }264 network.cluster(options);265 }266 /**267 * At time of writing, this test detected 22 out of 33 'illegal' loops.268 * The real deterrent is eslint rule 'guard-for-in`.269 */270 it('can deal with added fields in Array.prototype', function (done) {271 Array.prototype.foo = 1; // Just add anything to the prototype272 Object.prototype.bar = 2; // Let's screw up hashes as well273 // The network should just run without throwing errors274 try {275 var [network, data, numNodes, numEdges] = createSampleNetwork({});276 // Do some stuff to trigger more errors277 clusterTo(network, 'c1', [1,2,3]);278 data.nodes.remove(1);279 network.openCluster('c1');280 clusterTo(network, 'c1', [4], true); 281 clusterTo(network, 'c2', ['c1'], true); 282 clusterTo(network, 'c3', ['c2'], true); 283 data.nodes.remove(4);284 285 } catch(e) {286 delete Array.prototype.foo; // Remove it again so as not to confuse other tests.287 delete Object.prototype.bar;288 assert(false, "Got exception:\n" + e.stack);289 }290 delete Array.prototype.foo; // Remove it again so as not to confuse other tests.291 delete Object.prototype.bar;292 done();293 });294describe('Node', function () {295 it('has known font options', function () {296 var network = createNetwork({});297 checkFontProperties(network.nodesHandler.defaultOptions.font);298 checkFontProperties(allOptions.nodes.font);299 checkFontProperties(configureOptions.nodes.font, false);300 });301 /**302 * NOTE: choosify tests of Node and Edge are parallel303 * TODO: consolidate this is necessary304 */305 it('properly handles choosify input', function () {306 // check defaults307 var options = {};308 var network = createNetwork(options);309 checkChooserValues(firstNode(network), true, true);310 // There's no point in checking invalid values here; these are detected by the options parser311 // and subsequently handled as missing input, thus assigned defaults312 // check various combinations of valid input313 options = {nodes: {chosen: false}};314 network = createNetwork(options);315 checkChooserValues(firstNode(network), false, false);316 options = {nodes: {chosen: { node:true, label:false}}};317 network = createNetwork(options);318 checkChooserValues(firstNode(network), true, false);319 options = {nodes: {chosen: {320 node:true,321 label: function(value, id, selected, hovering) {}322 }}};323 network = createNetwork(options);324 checkChooserValues(firstNode(network), true, 'function');325 options = {nodes: {chosen: {326 node: function(value, id, selected, hovering) {},327 label:false,328 }}};329 network = createNetwork(options);330 checkChooserValues(firstNode(network), 'function', false);331 });332}); // Node333describe('Edge', function () {334 it('has known font options', function () {335 var network = createNetwork({});336 checkFontProperties(network.edgesHandler.defaultOptions.font);337 checkFontProperties(allOptions.edges.font);338 checkFontProperties(configureOptions.edges.font, false);339 });340 /**341 * NOTE: choosify tests of Node and Edge are parallel342 * TODO: consolidate this is necessary343 */344 it('properly handles choosify input', function () {345 // check defaults346 var options = {};347 var network = createNetwork(options);348 checkChooserValues(firstEdge(network), true, true);349 // There's no point in checking invalid values here; these are detected by the options parser350 // and subsequently handled as missing input, thus assigned defaults351 // check various combinations of valid input352 options = {edges: {chosen: false}};353 network = createNetwork(options);354 checkChooserValues(firstEdge(network), false, false);355 options = {edges: {chosen: { edge:true, label:false}}};356 network = createNetwork(options);357 checkChooserValues(firstEdge(network), true, false);358 options = {edges: {chosen: {359 edge:true,360 label: function(value, id, selected, hovering) {}361 }}};362 network = createNetwork(options);363 checkChooserValues(firstEdge(network), true, 'function');364 options = {edges: {chosen: {365 edge: function(value, id, selected, hovering) {},366 label:false,367 }}};368 network = createNetwork(options);369 checkChooserValues(firstEdge(network), 'function', false);370 });371 /**372 * Support routine for next unit test373 */374 function createDataforColorChange() {375 var nodes = new vis.DataSet([376 {id: 1, label: 'Node 1' }, // group:'Group1'},377 {id: 2, label: 'Node 2', group:'Group2'},378 {id: 3, label: 'Node 3'},379 ]);380 // create an array with edges381 var edges = new vis.DataSet([382 {id: 1, from: 1, to: 2},383 {id: 2, from: 1, to: 3, color: { inherit: 'to'}},384 {id: 3, from: 3, to: 3, color: { color: '#00FF00'}},385 {id: 4, from: 2, to: 3, color: { inherit: 'from'}},386 ]);387 var data = {388 nodes: nodes,389 edges: edges390 };391 return data;392 }393 /**394 * Unit test for fix of #3350395 *396 * The issue is that changing color options is not registered in the nodes.397 * We test the updates the color options in the general edges options here.398 */399 it('sets inherit color option for edges on call to Network.setOptions()', function () {400 var container = document.getElementById('mynetwork');401 var data = createDataforColorChange();402 var options = {403 "edges" : { "color" : { "inherit" : "to" } },404 };405 // Test passing options on init.406 var network = new vis.Network(container, data, options);407 var edges = network.body.edges;408 assert.equal(edges[1].options.color.inherit, 'to'); // new default409 assert.equal(edges[2].options.color.inherit, 'to'); // set in edge410 assert.equal(edges[3].options.color.inherit, false); // has explicit color411 assert.equal(edges[4].options.color.inherit, 'from'); // set in edge412 // Sanity check: colors should still be defaults413 assert.equal(edges[1].options.color.color, network.edgesHandler.options.color.color);414 // Override the color value - inherit returns to default415 network.setOptions({ edges:{color: {}}});416 assert.equal(edges[1].options.color.inherit, 'from'); // default417 assert.equal(edges[2].options.color.inherit, 'to'); // set in edge418 assert.equal(edges[3].options.color.inherit, false); // has explicit color419 assert.equal(edges[4].options.color.inherit, 'from'); // set in edge420 // Check no options421 network = new vis.Network(container, data, {});422 edges = network.body.edges;423 assert.equal(edges[1].options.color.inherit, 'from'); // default424 assert.equal(edges[2].options.color.inherit, 'to'); // set in edge425 assert.equal(edges[3].options.color.inherit, false); // has explicit color426 assert.equal(edges[4].options.color.inherit, 'from'); // set in edge427 // Set new value428 network.setOptions(options);429 assert.equal(edges[1].options.color.inherit, 'to');430 assert.equal(edges[2].options.color.inherit, 'to'); // set in edge431 assert.equal(edges[3].options.color.inherit, false); // has explicit color432 assert.equal(edges[4].options.color.inherit, 'from'); // set in edge433/*434 // Useful for debugging435 console.log('===================================');436 console.log(edges[1].options.color);437 console.log(edges[1].options.color.__proto__);438 console.log(edges[1].options);439 console.log(edges[1].options.__proto__);440 console.log(edges[1].edgeOptions);441*/442 });443 it('sets inherit color option for specific edge', function () {444 var container = document.getElementById('mynetwork');445 var data = createDataforColorChange();446 // Check no options447 var network = new vis.Network(container, data, {});448 var edges = network.body.edges;449 assert.equal(edges[1].options.color.inherit, 'from'); // default450 assert.equal(edges[2].options.color.inherit, 'to'); // set in edge451 assert.equal(edges[3].options.color.inherit, false); // has explicit color452 assert.equal(edges[4].options.color.inherit, 'from'); // set in edge453 // Set new value454 data.edges.update({id: 1, color: { inherit: 'to'}});455 assert.equal(edges[1].options.color.inherit, 'to'); // Only this changed456 assert.equal(edges[2].options.color.inherit, 'to');457 assert.equal(edges[3].options.color.inherit, false); // has explicit color458 assert.equal(edges[4].options.color.inherit, 'from');459 });460 /**461 * Perhaps TODO: add unit test for passing string value for color option462 */463 it('sets color value for edges on call to Network.setOptions()', function () {464 var container = document.getElementById('mynetwork');465 var data = createDataforColorChange();466 var defaultColor = '#848484'; // From defaults467 var color = '#FF0000';468 var options = {469 "edges" : { "color" : { "color" : color } },470 };471 // Test passing options on init.472 var network = new vis.Network(container, data, options);473 var edges = network.body.edges;474 assert.equal(edges[1].options.color.color, color);475 assert.equal(edges[1].options.color.inherit, false); // Explicit color, so no inherit476 assert.equal(edges[2].options.color.color, color);477 assert.equal(edges[2].options.color.inherit, 'to'); // Local value overrides! (bug according to docs)478 assert.notEqual(edges[3].options.color.color, color); // Has own value479 assert.equal(edges[3].options.color.inherit, false); // Explicit color, so no inherit480 assert.equal(edges[4].options.color.color, color);481 // Override the color value - all should return to default482 network.setOptions({ edges:{color: {}}});483 assert.equal(edges[1].options.color.color, defaultColor);484 assert.equal(edges[1].options.color.inherit, 'from');485 assert.equal(edges[2].options.color.color, defaultColor);486 assert.notEqual(edges[3].options.color.color, color); // Has own value487 assert.equal(edges[4].options.color.color, defaultColor);488 // Check no options489 network = new vis.Network(container, data, {});490 edges = network.body.edges;491 // At this point, color has not changed yet492 assert.equal(edges[1].options.color.color, defaultColor);493 assert.equal(edges[1].options.color.highlight, defaultColor);494 assert.equal(edges[1].options.color.inherit, 'from');495 assert.notEqual(edges[3].options.color.color, color); // Has own value496 // Set new Value497 network.setOptions(options);498 assert.equal(edges[1].options.color.color, color);499 assert.equal(edges[1].options.color.highlight, defaultColor); // Should not be changed500 assert.equal(edges[1].options.color.inherit, false); // Explicit color, so no inherit501 assert.equal(edges[2].options.color.color, color);502 assert.notEqual(edges[3].options.color.color, color); // Has own value503 assert.equal(edges[4].options.color.color, color);504 });505 /**506 * Unit test for fix of #3500507 * Checking to make sure edges that become unconnected due to node removal get reconnected508 */ 509 it('has reconnected edges', function () {510 var node1 = {id:1, label:"test1"};511 var node2 = {id:2, label:"test2"};512 var nodes = new vis.DataSet([node1, node2]);513 514 var edge = {id:1, from: 1, to:2};515 var edges = new vis.DataSet([edge]);516 var data = {517 nodes: nodes,518 edges: edges519 }; 520 var container = document.getElementById('mynetwork');521 var network = new vis.Network(container, data);522 //remove node causing edge to become disconnected523 nodes.remove(node2.id);524 525 var foundEdge = network.body.edges[edge.id];526 assert.ok(foundEdge===undefined, "edge is still in state cache");527 //add node back reconnecting edge528 nodes.add(node2);529 530 foundEdge = network.body.edges[edge.id];531 532 assert.ok(foundEdge!==undefined, "edge is missing from state cache");533 });534}); // Edge535describe('Clustering', function () {536 it('properly handles options allowSingleNodeCluster', function() {537 var [network, data, numNodes, numEdges] = createSampleNetwork();538 data.edges.update({from: 1, to: 11,});539 numEdges += 1;540 assertNumNodes(network, numNodes);541 assertNumEdges(network, numEdges);542 clusterTo(network, 'c1', [3,4]); 543 numNodes += 1; // A clustering node is now hiding two nodes544 numEdges += 1; // One clustering edges now hiding two edges545 assertNumNodes(network, numNodes, numNodes - 2);546 assertNumEdges(network, numEdges, numEdges - 2);547 // Cluster of single node should fail, because by default allowSingleNodeCluster == false548 clusterTo(network, 'c2', [14]); 549 assertNumNodes(network, numNodes, numNodes - 2); // Nothing changed550 assertNumEdges(network, numEdges, numEdges - 2);551 assert(network.body.nodes['c2'] === undefined); // Cluster not created552 // Redo with allowSingleNodeCluster == true553 clusterTo(network, 'c2', [14], true); 554 numNodes += 1;555 numEdges += 1;556 assertNumNodes(network, numNodes, numNodes - 3);557 assertNumEdges(network, numEdges, numEdges - 3);558 assert(network.body.nodes['c2'] !== undefined); // Cluster created559 // allowSingleNodeCluster: true with two nodes560 // removing one clustered node should retain cluster561 clusterTo(network, 'c3', [11, 12], true); 562 numNodes += 1; // Added cluster563 numEdges += 2;564 assertNumNodes(network, numNodes, 6);565 assertNumEdges(network, numEdges, 5);566 data.nodes.remove(12);567 assert(network.body.nodes['c3'] !== undefined); // Cluster should still be present568 numNodes -= 1; // removed node 569 numEdges -= 3; // cluster edge C3-13 should be removed570 assertNumNodes(network, numNodes, 6);571 assertNumEdges(network, numEdges, 4);572 });573 it('removes nested clusters with allowSingleNodeCluster === true', function() {574 var [network, data, numNodes, numEdges] = createSampleNetwork();575 // Create a chain of nested clusters, three deep576 clusterTo(network, 'c1', [4], true); 577 clusterTo(network, 'c2', ['c1'], true); 578 clusterTo(network, 'c3', ['c2'], true); 579 numNodes += 3;580 numEdges += 3;581 assertNumNodes(network, numNodes, numNodes - 3);582 assertNumEdges(network, numEdges, numEdges - 3);583 assert(network.body.nodes['c1'] !== undefined);584 assert(network.body.nodes['c2'] !== undefined);585 assert(network.body.nodes['c3'] !== undefined);586 // The whole chain should be removed when the bottom-most node is deleted587 data.nodes.remove(4);588 numNodes -= 4;589 numEdges -= 4;590 assertNumNodes(network, numNodes);591 assertNumEdges(network, numEdges);592 assert(network.body.nodes['c1'] === undefined);593 assert(network.body.nodes['c2'] === undefined);594 assert(network.body.nodes['c3'] === undefined);595 });596 /**597 * Check on fix for #1218598 */599 it('connects a new edge to a clustering node instead of the clustered node', function () {600 var [network, data, numNodes, numEdges] = createSampleNetwork();601 createCluster(network);602 numNodes += 1; // A clustering node is now hiding two nodes603 numEdges += 2; // Two clustering edges now hide two edges604 assertNumNodes(network, numNodes, numNodes - 2);605 assertNumEdges(network, numEdges, numEdges - 2);606 //console.log("Creating node 21")607 data.nodes.update([{id: 21, label: '21'}]);608 numNodes += 1; // New unconnected node added609 assertNumNodes(network, numNodes, numNodes - 2);610 assertNumEdges(network, numEdges, numEdges - 2); // edges unchanged611 //console.log("Creating edge 21 pointing to 1");612 // '1' is part of the cluster so should613 // connect to cluster instead614 data.edges.update([{from: 21, to: 1}]);615 numEdges += 2; // A new clustering edge is hiding a new edge616 assertNumNodes(network, numNodes, numNodes - 2); // nodes unchanged617 assertNumEdges(network, numEdges, numEdges - 3);618 });619 /**620 * Check on fix for #1315621 */622 it('can uncluster a clustered node when a node is removed that has an edge to that cluster', function () {623 // NOTE: this block is same as previous test624 var [network, data, numNodes, numEdges] = createSampleNetwork();625 createCluster(network);626 numNodes += 1; // A clustering node is now hiding two nodes627 numEdges += 2; // Two clustering edges now hide two edges628 assertNumNodes(network, numNodes, numNodes - 2);629 assertNumEdges(network, numEdges, numEdges - 2);630 // End block same as previous test631 //console.log("removing 12");632 data.nodes.remove(12);633 // NOTE:634 // At this particular point, there are still the two edges for node 12 in the edges DataSet.635 // If you want to do the delete correctly, these should also be deleted explictly from636 // the edges DataSet. In the Network instance, however, this.body.nodes and this.body.edges637 // should be correct, with the edges of 12 all cleared out.638 // 12 was connected to 11, which is clustered639 numNodes -= 1; // 12 removed, one less node640 numEdges -= 3; // clustering edge c1-12 and 2 edges of 12 gone641 assertNumNodes(network, numNodes, numNodes - 2);642 assertNumEdges(network, numEdges, numEdges - 1);643 //console.log("Unclustering c1");644 network.openCluster("c1");645 numNodes -= 1; // cluster node removed, one less node646 numEdges -= 1; // clustering edge gone, regular edge visible647 assertNumNodes(network, numNodes, numNodes); // all are visible again648 assertNumEdges(network, numEdges, numEdges); // all are visible again649 });650 /**651 * Check on fix for #1291652 */653 it('can remove a node inside a cluster and then open that cluster', function () {654 var [network, data, numNodes, numEdges] = createSampleNetwork();655 var clusterOptionsByData = {656 joinCondition: function(node) {657 if (node.id == 1 || node.id == 2 || node.id == 3) return true;658 return false;659 },660 clusterNodeProperties: {id:"c1", label:'c1'}661 }662 network.cluster(clusterOptionsByData);663 numNodes += 1; // new cluster node664 numEdges += 1; // 1 cluster edge expected665 assertNumNodes(network, numNodes, numNodes - 3); // 3 clustered nodes666 assertNumEdges(network, numEdges, numEdges - 3); // 3 edges hidden667 //console.log("removing node 2, which is inside the cluster");668 data.nodes.remove(2);669 numNodes -= 1; // clustered node removed670 numEdges -= 2; // edges removed hidden in cluster671 assertNumNodes(network, numNodes, numNodes - 2); // view doesn't change672 assertNumEdges(network, numEdges, numEdges - 1); // view doesn't change673 //console.log("Unclustering c1");674 network.openCluster("c1")675 numNodes -= 1; // cluster node gone676 numEdges -= 1; // cluster edge gone677 assertNumNodes(network, numNodes, numNodes); // all visible678 assertNumEdges(network, numEdges, numEdges); // all visible679 //log(network);680 });681 /**682 * Helper function for setting up a graph for testing clusterByEdgeCount()683 */684 function createOutlierGraph() {685 // create an array with nodes686 var nodes = new vis.DataSet([687 {id: 1, label: '1', group:'Group1'},688 {id: 2, label: '2', group:'Group2'},689 {id: 3, label: '3', group:'Group3'},690 {id: 4, label: '4', group:'Group4'},691 {id: 5, label: '5', group:'Group4'}692 ]);693 // create an array with edges694 var edges = new vis.DataSet([695 {from: 1, to: 3},696 {from: 1, to: 2},697 {from: 2, to: 4},698 {from: 2, to: 5}699 ]);700 // create a network701 var container = document.getElementById('mynetwork');702 var data = {703 nodes: nodes,704 edges: edges705 };706 var options = {707 "groups" : {708 "Group1" : { level:1 },709 "Group2" : { level:2 },710 "Group3" : { level:3 },711 "Group4" : { level:4 }712 }713 };714 var network = new vis.Network (container, data, options);715 return network;716 }717 /**718 * Check on fix for #3367719 */720 it('correctly handles edge cases of clusterByEdgeCount()', function () {721 /**722 * Collect clustered id's723 *724 * All node id's in clustering nodes are collected into an array;725 * The results for all clusters are returned as an array.726 *727 * Ordering of output depends on the order in which they are defined728 * within nodes.clustering; strictly, speaking, the array and its items729 * are collections, so order should not matter. 730 */731 var collectClusters = function(network) {732 var clusters = [];733 for(var n in network.body.nodes) {734 var node = network.body.nodes[n];735 if (node.containedNodes === undefined) continue; // clusters only736 // Collect id's of nodes in the cluster737 var nodes = [];738 for(var m in node.containedNodes) {739 nodes.push(m);740 }741 clusters.push(nodes);742 }743 return clusters;744 }745 /**746 * Compare cluster data747 *748 * params are arrays of arrays of id's, e.g:749 *750 * [[1,3],[2,4]]751 *752 * Item arrays are the id's of nodes in a given cluster753 *754 * This comparison depends on the ordering; better755 * would be to treat the items and values as collections.756 */757 var compareClusterInfo = function(recieved, expected) {758 if (recieved.length !== expected.length) return false;759 for (var n = 0; n < recieved.length; ++n) {760 var itema = recieved[n];761 var itemb = expected[n];762 if (itema.length !== itemb.length) return false;763 for (var m = 0; m < itema.length; ++m) {764 if (itema[m] != itemb[m]) return false; // != because values can be string or number765 }766 }767 return true;768 }769 var assertJoinCondition = function(joinCondition, expected) {770 var network = createOutlierGraph();771 network.clusterOutliers({joinCondition: joinCondition});772 var recieved = collectClusters(network);773 //console.log(recieved);774 assert(compareClusterInfo(recieved, expected),775 'recieved:' + JSON.stringify(recieved) + '; '776 + 'expected: ' + JSON.stringify(expected));777 };778 // Should cluster 3,4,5:779 var joinAll_ = function(n) { return true ; }780 // Should cluster none:781 var joinNone_ = function(n) { return false ; }782 // Should cluster 4 & 5:783 var joinLevel_ = function(n) { return n.level > 3 ; }784 assertJoinCondition(undefined , [[1,3],[2,4,5]]);785 assertJoinCondition(null , [[1,3],[2,4,5]]);786 assertJoinCondition(joinNone_ , []);787 assertJoinCondition(joinLevel_ , [[2,4,5]]);788 });789 ///////////////////////////////////////////////////////////////790 // Automatic opening of clusters due to dynamic data change791 ///////////////////////////////////////////////////////////////792 /**793 * Helper function, created nested clusters, three deep794 */795 function createNetwork1() {796 var [network, data, numNodes, numEdges] = createSampleNetwork();797 clusterTo(network, 'c1', [3,4]); 798 numNodes += 1; // new cluster node799 numEdges += 1; // 1 cluster edge expected800 assertNumNodes(network, numNodes, numNodes - 2); // 2 clustered nodes801 assertNumEdges(network, numEdges, numEdges - 2); // 2 edges hidden802 clusterTo(network, 'c2', [2,'c1']); 803 numNodes += 1; // new cluster node804 numEdges += 1; // 2 cluster edges expected805 assertNumNodes(network, numNodes, numNodes - 4); // 4 clustered nodes, including c1806 assertNumEdges(network, numEdges, numEdges - 4); // 4 edges hidden, including edge for c1807 clusterTo(network, 'c3', [1,'c2']); 808 // Attempt at visualization: parentheses belong to the cluster one level above809 // c3810 // ( -c2 )811 // ( -c1 )812 // 14-13-12-11 1 -2 (-3-4)813 numNodes += 1; // new cluster node814 numEdges += 0; // No new cluster edge expected815 assertNumNodes(network, numNodes, numNodes - 6); // 6 clustered nodes, including c1 and c2816 assertNumEdges(network, numEdges, numEdges - 5); // 5 edges hidden, including edges for c1 and c2817 return [network, data, numNodes, numEdges];818 }819 it('opens clusters automatically when nodes deleted', function () {820 var [network, data, numNodes, numEdges] = createSampleNetwork();821 // Simple case: cluster of two nodes, delete one node822 clusterTo(network, 'c1', [3,4]); 823 numNodes += 1; // new cluster node824 numEdges += 1; // 1 cluster edge expected825 assertNumNodes(network, numNodes, numNodes - 2); // 2 clustered nodes826 assertNumEdges(network, numEdges, numEdges - 2); // 2 edges hidden827 data.nodes.remove(4);828 numNodes -= 2; // deleting clustered node also removes cluster node829 numEdges -= 2; // cluster edge should also be removed830 assertNumNodes(network, numNodes, numNodes);831 assertNumEdges(network, numEdges, numEdges);832 // Extended case: nested nodes, three deep833 [network, data, numNodes, numEdges] = createNetwork1();834 data.nodes.remove(4);835 // c3836 // ( -c2 )837 // 14-13-12-11 1 (-2 -3)838 numNodes -= 2; // node removed, c1 also gone839 numEdges -= 2;840 assertNumNodes(network, numNodes, numNodes - 4);841 assertNumEdges(network, numEdges, numEdges - 3);842 data.nodes.remove(1);843 // c2844 // 14-13-12-11 (2 -3)845 numNodes -= 2; // node removed, c3 also gone846 numEdges -= 2;847 assertNumNodes(network, numNodes, numNodes - 2);848 assertNumEdges(network, numEdges, numEdges - 1);849 data.nodes.remove(2);850 // 14-13-12-11 3851 numNodes -= 2; // node removed, c2 also gone852 numEdges -= 1;853 assertNumNodes(network, numNodes); // All visible again854 assertNumEdges(network, numEdges);855 // Same as previous step, but remove all the given nodes in one go856 // The result should be the same.857 [network, data, numNodes, numEdges] = createNetwork1(); // nested nodes, three deep858 data.nodes.remove([1,2,4]);859 // 14-13-12-11 3860 assertNumNodes(network, 5);861 assertNumEdges(network, 3);862 });863 ///////////////////////////////////////////////////////////////864 // Opening of clusters at various clustering depths865 ///////////////////////////////////////////////////////////////866 /**867 * Check correct opening of a single cluster.868 * This is the 'simple' case.869 */870 it('properly opens 1-level clusters', function () {871 var [network, data, numNodes, numEdges] = createSampleNetwork();872 // Pedantic: make a cluster of everything873 clusterTo(network, 'c1', [1,2,3,4,11, 12, 13, 14]);874 // c1(14-13-12-11 1-2-3-4)875 numNodes += 1;876 assertNumNodes(network, numNodes, 1); // Just the clustering node visible877 assertNumEdges(network, numEdges, 0); // No extra edges!878 network.clustering.openCluster('c1', {});879 numNodes -= 1;880 assertNumNodes(network, numNodes, numNodes); // Expecting same as original881 assertNumEdges(network, numEdges, numEdges);882 // One external connection883 [network, data, numNodes, numEdges] = createSampleNetwork();884 // 14-13-12-11 1-2-3-4885 clusterTo(network, 'c1', [3,4]); 886 network.clustering.openCluster('c1', {});887 assertNumNodes(network, numNodes, numNodes); // Expecting same as original888 assertNumEdges(network, numEdges, numEdges);889 // Two external connections890 clusterTo(network, 'c1', [2,3]); 891 network.clustering.openCluster('c1', {});892 assertNumNodes(network, numNodes, numNodes); // Expecting same as original893 assertNumEdges(network, numEdges, numEdges);894 // One external connection to cluster895 clusterTo(network, 'c1', [1,2]); 896 clusterTo(network, 'c2', [3,4]); 897 // 14-13-12-11 c1(1-2-)-c2(-3-4)898 network.clustering.openCluster('c1', {});899 // 14-13-12-11 1-2-c2(-3-4)900 numNodes += 1;901 numEdges += 1;902 assertNumNodes(network, numNodes, numNodes - 2);903 assertNumEdges(network, numEdges, numEdges - 2);904 // two external connections to clusters905 [network, data, numNodes, numEdges] = createSampleNetwork();906 data.edges.update({907 from: 1,908 to: 11,909 });910 numEdges += 1;911 assertNumNodes(network, numNodes, numNodes);912 assertNumEdges(network, numEdges, numEdges);913 clusterTo(network, 'c1', [1,2]); 914 // 14-13-12-11-c1(-1-2-)-3-4915 numNodes += 1;916 numEdges += 2;917 clusterTo(network, 'c2', [3,4]); 918 // 14-13-12-11-c1(-1-2-)-c2(-3-4)919 // NOTE: clustering edges are hidden by clustering here!920 numNodes += 1;921 numEdges += 1;922 clusterTo(network, 'c3', [11,12]); 923 // 14-13-c3(-12-11-)-c1(-1-2-)-c2(-3-4)924 numNodes += 1;925 numEdges += 2;926 assertNumNodes(network, numNodes, numNodes - 6);927 assertNumEdges(network, numEdges, numEdges - 8); // 6 regular edges hidden; also 2 clustering!!!!!928 network.clustering.openCluster('c1', {});929 numNodes -= 1;930 numEdges -= 2;931 // 14-13-c3(-12-11-)-1-2-c2(-3-4)932 assertNumNodes(network, numNodes, numNodes - 4);933 assertNumEdges(network, numEdges, numEdges - 5);934 });935 /**936 * Check correct opening of nested clusters.937 * The test uses clustering three levels deep and opens the middle one.938 */939 it('properly opens clustered clusters', function () {940 var [network, data, numNodes, numEdges] = createSampleNetwork();941 data.edges.update({from: 1, to: 11,});942 numEdges += 1;943 clusterTo(network, 'c1', [3,4]); 944 clusterTo(network, 'c2', [2,'c1']); 945 clusterTo(network, 'c3', [1,'c2']);946 // Attempt at visualization: parentheses belong to the cluster one level above947 // -c3948 // ( -c2 )949 // ( -c1 )950 // 14-13-12-11 -1 -2 (-3-4)951 numNodes += 3;952 numEdges += 3;953 //console.log("numNodes: " + numNodes + "; numEdges: " + numEdges);954 assertNumNodes(network, numNodes, numNodes - 6);955 assertNumEdges(network, numEdges, numEdges - 6);956 // Open the middle cluster957 network.clustering.openCluster('c2', {});958 // -c3959 // ( -c1 )960 // 14-13-12-11 -1 -2 (-3-4)961 numNodes -= 1;962 numEdges -= 1;963 assertNumNodes(network, numNodes, numNodes - 5);964 assertNumEdges(network, numEdges, numEdges - 5);965 //966 // Same, with one external connection to cluster967 //968 var [network, data, numNodes, numEdges] = createSampleNetwork();969 data.edges.update({from: 1, to: 11,});970 data.edges.update({from: 2, to: 12,});971 numEdges += 2;972 // 14-13-12-11-1-2-3-4973 // |------|974 assertNumNodes(network, numNodes);975 assertNumEdges(network, numEdges);976 clusterTo(network, 'c0', [11,12]); 977 clusterTo(network, 'c1', [3,4]); 978 clusterTo(network, 'c2', [2,'c1']); 979 clusterTo(network, 'c3', [1,'c2']);980 // +----------------+981 // | c3 |982 // | +----------+ |983 // | | c2 | |984 // +-------+ | | +----+ | |985 // | c0 | | | | c1 | | |986 // 14-13-|-12-11-|-|-1-|-2-|-3-4| | |987 // | | | | | | +----+ | |988 // +-------+ | | | | |989 // | | +----------+ |990 // | | | |991 // | +----------------+992 // |------------|993 // (I)994 numNodes += 4;995 numEdges = 15;996 assertNumNodes(network, numNodes, 4);997 assertNumEdges(network, numEdges, 3); // (I) link 2-12 is combined into cluster edge for 11-1998 // Open the middle cluster999 network.clustering.openCluster('c2', {});1000 // +--------------+1001 // | c3 |1002 // | |1003 // +-------+ | +----+ |1004 // | c0 | | | c1 | |1005 // 14-13-|-12-11-|-|-1--2-|-3-4| |1006 // | | | | | +----+ |1007 // +-------+ | | |1008 // | | | |1009 // | +--------------+1010 // |-----------|1011 // (I)1012 numNodes -= 1;1013 numEdges -= 2;1014 assertNumNodes(network, numNodes, 4); // visibility doesn't change, cluster opened within cluster1015 assertNumEdges(network, numEdges, 3); // (I)1016 // Open the top cluster1017 network.clustering.openCluster('c3', {});1018 // 1019 // +-------+ +----+1020 // | c0 | | c1 |1021 // 14-13-|-12-11-|-1-2-|-3-4|1022 // | | | | +----+1023 // +-------+ | 1024 // | | 1025 // |--------|1026 // (II)1027 numNodes -= 1;1028 numEdges = 12;1029 assertNumNodes(network, numNodes, 6); // visibility doesn't change, cluster opened within cluster1030 assertNumEdges(network, numEdges, 6); // (II) link 2-12 visible again1031 });1032}); // Clustering1033describe('on node.js', function () {1034 it('should be running', function () {1035 assert(this.container !== null, 'Container div not found');1036 // The following should now just plain succeed1037 var [network, data] = createSampleNetwork();1038 assert.equal(Object.keys(network.body.nodes).length, 8);1039 assert.equal(Object.keys(network.body.edges).length, 6);1040 });1041describe('runs example ', function () {1042 function loadExample(path, noPhysics) {1043 include(path, this);1044 var container = document.getElementById('mynetwork');1045 // create a network1046 var data = {1047 nodes: new vis.DataSet(nodes),1048 edges: new vis.DataSet(edges)1049 };1050 if (noPhysics) {1051 // Avoid excessive processor time due to load.1052 // We're just interested that the load itself is good1053 options.physics = false;1054 }1055 var network = new vis.Network(container, data, options);1056 return network;1057 };1058 it('basicUsage', function () {1059 var network = loadExample('./test/network/basicUsage.js');1060 //console.log(Object.keys(network.body.edges));1061 // Count in following also contains the helper nodes for dynamic edges1062 assert.equal(Object.keys(network.body.nodes).length, 10);1063 assert.equal(Object.keys(network.body.edges).length, 5);1064 });1065 it('WorlCup2014', function (done) {1066 // This is a huge example (which is why it's tested here!), so it takes a long time to load.1067 this.timeout(15000);1068 var network = loadExample('./examples/network/datasources/WorldCup2014.js', true);1069 // Count in following also contains the helper nodes for dynamic edges1070 assert.equal(Object.keys(network.body.nodes).length, 9964);1071 assert.equal(Object.keys(network.body.edges).length, 9228);1072 done();1073 });1074 // This actually failed to load, added for this reason1075 it('disassemblerExample', function () {1076 var network = loadExample('./examples/network/exampleApplications/disassemblerExample.js');1077 // console.log(Object.keys(network.body.nodes));1078 // console.log(Object.keys(network.body.edges));1079 // Count in following also contains the helper nodes for dynamic edges1080 assert.equal(Object.keys(network.body.nodes).length, 9);1081 assert.equal(Object.keys(network.body.edges).length, 14 - 3); // NB 3 edges in data not displayed1082 });1083}); // runs example1084}); // on node.js...

Full Screen

Full Screen

NodeBase.js

Source:NodeBase.js Github

copy

Full Screen

1/**2 * The Base class for all Nodes.3 */4class NodeBase {5 /**6 * @param {Object} options7 * @param {Object} body8 * @param {Label} labelModule9 */10 constructor(options, body, labelModule) {11 this.body = body;12 this.labelModule = labelModule;13 this.setOptions(options);14 this.top = undefined;15 this.left = undefined;16 this.height = undefined;17 this.width = undefined;18 this.radius = undefined;19 this.margin = undefined;20 this.refreshNeeded = true;21 this.boundingBox = {top: 0, left: 0, right: 0, bottom: 0};22 }23 /**24 *25 * @param {Object} options26 */27 setOptions(options) {28 this.options = options;29 }30 /**31 *32 * @param {Label} labelModule33 * @private34 */35 _setMargins(labelModule) {36 this.margin = {};37 if (this.options.margin) {38 if (typeof this.options.margin == 'object') {39 this.margin.top = this.options.margin.top;40 this.margin.right = this.options.margin.right;41 this.margin.bottom = this.options.margin.bottom;42 this.margin.left = this.options.margin.left;43 } else {44 this.margin.top = this.options.margin;45 this.margin.right = this.options.margin;46 this.margin.bottom = this.options.margin;47 this.margin.left = this.options.margin;48 }49 }50 labelModule.adjustSizes(this.margin)51 }52 /**53 *54 * @param {CanvasRenderingContext2D} ctx55 * @param {number} angle56 * @returns {number}57 * @private58 */59 _distanceToBorder(ctx,angle) {60 var borderWidth = this.options.borderWidth;61 this.resize(ctx);62 return Math.min(63 Math.abs(this.width / 2 / Math.cos(angle)),64 Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;65 }66 /**67 *68 * @param {CanvasRenderingContext2D} ctx69 * @param {{toArrow: boolean, toArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), toArrowType: *, middleArrow: boolean, middleArrowScale: (number|allOptions.edges.arrows.middle.scaleFactor|{number}|Array), middleArrowType: (allOptions.edges.arrows.middle.type|{string}|string|*), fromArrow: boolean, fromArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), fromArrowType: *, arrowStrikethrough: (*|boolean|allOptions.edges.arrowStrikethrough|{boolean}), color: undefined, inheritsColor: (string|string|string|allOptions.edges.color.inherit|{string, boolean}|Array|*), opacity: *, hidden: *, length: *, shadow: *, shadowColor: *, shadowSize: *, shadowX: *, shadowY: *, dashes: (*|boolean|Array|allOptions.edges.dashes|{boolean, array}), width: *}} values70 */71 enableShadow(ctx, values) {72 if (values.shadow) {73 ctx.shadowColor = values.shadowColor;74 ctx.shadowBlur = values.shadowSize;75 ctx.shadowOffsetX = values.shadowX;76 ctx.shadowOffsetY = values.shadowY;77 }78 }79 /**80 *81 * @param {CanvasRenderingContext2D} ctx82 * @param {{toArrow: boolean, toArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), toArrowType: *, middleArrow: boolean, middleArrowScale: (number|allOptions.edges.arrows.middle.scaleFactor|{number}|Array), middleArrowType: (allOptions.edges.arrows.middle.type|{string}|string|*), fromArrow: boolean, fromArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), fromArrowType: *, arrowStrikethrough: (*|boolean|allOptions.edges.arrowStrikethrough|{boolean}), color: undefined, inheritsColor: (string|string|string|allOptions.edges.color.inherit|{string, boolean}|Array|*), opacity: *, hidden: *, length: *, shadow: *, shadowColor: *, shadowSize: *, shadowX: *, shadowY: *, dashes: (*|boolean|Array|allOptions.edges.dashes|{boolean, array}), width: *}} values83 */84 disableShadow(ctx, values) {85 if (values.shadow) {86 ctx.shadowColor = 'rgba(0,0,0,0)';87 ctx.shadowBlur = 0;88 ctx.shadowOffsetX = 0;89 ctx.shadowOffsetY = 0;90 }91 }92 /**93 *94 * @param {CanvasRenderingContext2D} ctx95 * @param {{toArrow: boolean, toArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), toArrowType: *, middleArrow: boolean, middleArrowScale: (number|allOptions.edges.arrows.middle.scaleFactor|{number}|Array), middleArrowType: (allOptions.edges.arrows.middle.type|{string}|string|*), fromArrow: boolean, fromArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), fromArrowType: *, arrowStrikethrough: (*|boolean|allOptions.edges.arrowStrikethrough|{boolean}), color: undefined, inheritsColor: (string|string|string|allOptions.edges.color.inherit|{string, boolean}|Array|*), opacity: *, hidden: *, length: *, shadow: *, shadowColor: *, shadowSize: *, shadowX: *, shadowY: *, dashes: (*|boolean|Array|allOptions.edges.dashes|{boolean, array}), width: *}} values96 */97 enableBorderDashes(ctx, values) {98 if (values.borderDashes !== false) {99 if (ctx.setLineDash !== undefined) {100 let dashes = values.borderDashes;101 if (dashes === true) {102 dashes = [5,15]103 }104 ctx.setLineDash(dashes);105 }106 else {107 console.warn("setLineDash is not supported in this browser. The dashed borders cannot be used.");108 this.options.shapeProperties.borderDashes = false;109 values.borderDashes = false;110 }111 }112 }113 /**114 *115 * @param {CanvasRenderingContext2D} ctx116 * @param {{toArrow: boolean, toArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), toArrowType: *, middleArrow: boolean, middleArrowScale: (number|allOptions.edges.arrows.middle.scaleFactor|{number}|Array), middleArrowType: (allOptions.edges.arrows.middle.type|{string}|string|*), fromArrow: boolean, fromArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), fromArrowType: *, arrowStrikethrough: (*|boolean|allOptions.edges.arrowStrikethrough|{boolean}), color: undefined, inheritsColor: (string|string|string|allOptions.edges.color.inherit|{string, boolean}|Array|*), opacity: *, hidden: *, length: *, shadow: *, shadowColor: *, shadowSize: *, shadowX: *, shadowY: *, dashes: (*|boolean|Array|allOptions.edges.dashes|{boolean, array}), width: *}} values117 */118 disableBorderDashes(ctx, values) {119 if (values.borderDashes !== false) {120 if (ctx.setLineDash !== undefined) {121 ctx.setLineDash([0]);122 }123 else {124 console.warn("setLineDash is not supported in this browser. The dashed borders cannot be used.");125 this.options.shapeProperties.borderDashes = false;126 values.borderDashes = false;127 }128 }129 }130 /**131 * Determine if the shape of a node needs to be recalculated.132 *133 * @param {boolean} selected134 * @param {boolean} hover135 * @returns {boolean}136 * @protected137 */138 needsRefresh(selected, hover) {139 if (this.refreshNeeded === true) {140 // This is probably not the best location to reset this member.141 // However, in the current logic, it is the most convenient one.142 this.refreshNeeded = false;143 return true;144 }145 return (this.width === undefined) || (this.labelModule.differentState(selected, hover));146 }147 /**148 *149 * @param {CanvasRenderingContext2D} ctx150 * @param {{toArrow: boolean, toArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), toArrowType: *, middleArrow: boolean, middleArrowScale: (number|allOptions.edges.arrows.middle.scaleFactor|{number}|Array), middleArrowType: (allOptions.edges.arrows.middle.type|{string}|string|*), fromArrow: boolean, fromArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), fromArrowType: *, arrowStrikethrough: (*|boolean|allOptions.edges.arrowStrikethrough|{boolean}), color: undefined, inheritsColor: (string|string|string|allOptions.edges.color.inherit|{string, boolean}|Array|*), opacity: *, hidden: *, length: *, shadow: *, shadowColor: *, shadowSize: *, shadowX: *, shadowY: *, dashes: (*|boolean|Array|allOptions.edges.dashes|{boolean, array}), width: *}} values151 */152 initContextForDraw(ctx, values) {153 var borderWidth = values.borderWidth / this.body.view.scale;154 ctx.lineWidth = Math.min(this.width, borderWidth);155 ctx.strokeStyle = values.borderColor;156 ctx.fillStyle = values.color;157 }158 /**159 *160 * @param {CanvasRenderingContext2D} ctx161 * @param {{toArrow: boolean, toArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), toArrowType: *, middleArrow: boolean, middleArrowScale: (number|allOptions.edges.arrows.middle.scaleFactor|{number}|Array), middleArrowType: (allOptions.edges.arrows.middle.type|{string}|string|*), fromArrow: boolean, fromArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), fromArrowType: *, arrowStrikethrough: (*|boolean|allOptions.edges.arrowStrikethrough|{boolean}), color: undefined, inheritsColor: (string|string|string|allOptions.edges.color.inherit|{string, boolean}|Array|*), opacity: *, hidden: *, length: *, shadow: *, shadowColor: *, shadowSize: *, shadowX: *, shadowY: *, dashes: (*|boolean|Array|allOptions.edges.dashes|{boolean, array}), width: *}} values162 */163 performStroke(ctx, values) {164 var borderWidth = values.borderWidth / this.body.view.scale;165 //draw dashed border if enabled, save and restore is required for firefox not to crash on unix.166 ctx.save();167 // if borders are zero width, they will be drawn with width 1 by default. This prevents that168 if (borderWidth > 0) {169 this.enableBorderDashes(ctx, values);170 //draw the border171 ctx.stroke();172 //disable dashed border for other elements173 this.disableBorderDashes(ctx, values);174 }175 ctx.restore();176 }177 /**178 *179 * @param {CanvasRenderingContext2D} ctx180 * @param {{toArrow: boolean, toArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), toArrowType: *, middleArrow: boolean, middleArrowScale: (number|allOptions.edges.arrows.middle.scaleFactor|{number}|Array), middleArrowType: (allOptions.edges.arrows.middle.type|{string}|string|*), fromArrow: boolean, fromArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), fromArrowType: *, arrowStrikethrough: (*|boolean|allOptions.edges.arrowStrikethrough|{boolean}), color: undefined, inheritsColor: (string|string|string|allOptions.edges.color.inherit|{string, boolean}|Array|*), opacity: *, hidden: *, length: *, shadow: *, shadowColor: *, shadowSize: *, shadowX: *, shadowY: *, dashes: (*|boolean|Array|allOptions.edges.dashes|{boolean, array}), width: *}} values181 */182 performFill(ctx, values) {183 // draw shadow if enabled184 this.enableShadow(ctx, values);185 // draw the background186 ctx.fill();187 // disable shadows for other elements.188 this.disableShadow(ctx, values);189 this.performStroke(ctx, values);190 }191 /**192 *193 * @param {number} margin194 * @private195 */196 _addBoundingBoxMargin(margin) {197 this.boundingBox.left -= margin;198 this.boundingBox.top -= margin;199 this.boundingBox.bottom += margin;200 this.boundingBox.right += margin;201 }202 /**203 * Actual implementation of this method call.204 *205 * Doing it like this makes it easier to override206 * in the child classes.207 *208 * @param {number} x width209 * @param {number} y height210 * @param {CanvasRenderingContext2D} ctx211 * @param {boolean} selected212 * @param {boolean} hover213 * @private214 */215 _updateBoundingBox(x, y, ctx, selected, hover) {216 if (ctx !== undefined) {217 this.resize(ctx, selected, hover);218 }219 this.left = x - this.width / 2;220 this.top = y - this.height/ 2;221 this.boundingBox.left = this.left;222 this.boundingBox.top = this.top;223 this.boundingBox.bottom = this.top + this.height;224 this.boundingBox.right = this.left + this.width;225 }226 /**227 * Default implementation of this method call.228 * This acts as a stub which can be overridden.229 *230 * @param {number} x width231 * @param {number} y height232 * @param {CanvasRenderingContext2D} ctx233 * @param {boolean} selected234 * @param {boolean} hover235 */236 updateBoundingBox(x, y, ctx, selected, hover) {237 this._updateBoundingBox(x, y, ctx, selected, hover);238 }239 /**240 * Determine the dimensions to use for nodes with an internal label241 *242 * Currently, these are: Circle, Ellipse, Database, Box243 * The other nodes have external labels, and will not call this method244 *245 * If there is no label, decent default values are supplied.246 *247 * @param {CanvasRenderingContext2D} ctx248 * @param {boolean} [selected]249 * @param {boolean} [hover]250 * @returns {{width:number, height:number}}251 */252 getDimensionsFromLabel(ctx, selected, hover) {253 // NOTE: previously 'textSize' was not put in 'this' for Ellipse254 // TODO: examine the consequences.255 this.textSize = this.labelModule.getTextSize(ctx, selected, hover);256 var width = this.textSize.width;257 var height = this.textSize.height;258 const DEFAULT_SIZE = 14;259 if (width === 0) {260 // This happens when there is no label text set261 width = DEFAULT_SIZE; // use a decent default262 height = DEFAULT_SIZE; // if width zero, then height also always zero263 }264 return {width:width, height:height};265 }266}...

Full Screen

Full Screen

EdgesHandler.js

Source:EdgesHandler.js Github

copy

Full Screen

1var util = require("../../util");2var DataSet = require('../../DataSet');3var DataView = require('../../DataView');4var Edge = require("./components/Edge").default;5/**6 * Handler for Edges7 */8class EdgesHandler {9 /**10 * @param {Object} body11 * @param {Array.<Image>} images12 * @param {Array.<Group>} groups13 */14 constructor(body, images, groups) {15 this.body = body;16 this.images = images;17 this.groups = groups;18 // create the edge API in the body container19 this.body.functions.createEdge = this.create.bind(this);20 this.edgesListeners = {21 add: (event, params) => {this.add(params.items);},22 update: (event, params) => {this.update(params.items);},23 remove: (event, params) => {this.remove(params.items);}24 };25 this.options = {};26 this.defaultOptions = {27 arrows: {28 to: {enabled: false, scaleFactor:1, type: 'arrow'},// boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1}29 middle: {enabled: false, scaleFactor:1, type: 'arrow'},30 from: {enabled: false, scaleFactor:1, type: 'arrow'}31 },32 arrowStrikethrough: true,33 color: {34 color:'#848484',35 highlight:'#848484',36 hover: '#848484',37 inherit: 'from',38 opacity:1.039 },40 dashes: false,41 font: {42 color: '#343434',43 size: 14, // px44 face: 'arial',45 background: 'none',46 strokeWidth: 2, // px47 strokeColor: '#ffffff',48 align:'horizontal',49 multi: false,50 vadjust: 0,51 bold: {52 mod: 'bold'53 },54 boldital: {55 mod: 'bold italic'56 },57 ital: {58 mod: 'italic'59 },60 mono: {61 mod: '',62 size: 15, // px63 face: 'courier new',64 vadjust: 265 }66 },67 hidden: false,68 hoverWidth: 1.5,69 label: undefined,70 labelHighlightBold: true,71 length: undefined,72 physics: true,73 scaling:{74 min: 1,75 max: 15,76 label: {77 enabled: true,78 min: 14,79 max: 30,80 maxVisible: 30,81 drawThreshold: 582 },83 customScalingFunction: function (min,max,total,value) {84 if (max === min) {85 return 0.5;86 }87 else {88 var scale = 1 / (max - min);89 return Math.max(0,(value - min)*scale);90 }91 }92 },93 selectionWidth: 1.5,94 selfReferenceSize:20,95 shadow:{96 enabled: false,97 color: 'rgba(0,0,0,0.5)',98 size:10,99 x:5,100 y:5101 },102 smooth: {103 enabled: true,104 type: "dynamic",105 forceDirection:'none',106 roundness: 0.5107 },108 title:undefined,109 width: 1,110 value: undefined111 };112 util.deepExtend(this.options, this.defaultOptions);113 this.bindEventListeners();114 }115 /**116 * Binds event listeners117 */118 bindEventListeners() {119 // this allows external modules to force all dynamic curves to turn static.120 this.body.emitter.on("_forceDisableDynamicCurves", (type, emit = true) => {121 if (type === 'dynamic') {122 type = 'continuous';123 }124 let dataChanged = false;125 for (let edgeId in this.body.edges) {126 if (this.body.edges.hasOwnProperty(edgeId)) {127 let edge = this.body.edges[edgeId];128 let edgeData = this.body.data.edges._data[edgeId];129 // only forcibly remove the smooth curve if the data has been set of the edge has the smooth curves defined.130 // this is because a change in the global would not affect these curves.131 if (edgeData !== undefined) {132 let smoothOptions = edgeData.smooth;133 if (smoothOptions !== undefined) {134 if (smoothOptions.enabled === true && smoothOptions.type === 'dynamic') {135 if (type === undefined) {136 edge.setOptions({smooth: false});137 }138 else {139 edge.setOptions({smooth: {type: type}});140 }141 dataChanged = true;142 }143 }144 }145 }146 }147 if (emit === true && dataChanged === true) {148 this.body.emitter.emit("_dataChanged");149 }150 });151 // this is called when options of EXISTING nodes or edges have changed.152 //153 // NOTE: Not true, called when options have NOT changed, for both existing as well as new nodes.154 // See update() for logic.155 // TODO: Verify and examine the consequences of this. It might still trigger when156 // non-option fields have changed, but then reconnecting edges is still useless.157 // Alternatively, it might also be called when edges are removed.158 //159 this.body.emitter.on("_dataUpdated", () => {160 this.reconnectEdges();161 });162 // refresh the edges. Used when reverting from hierarchical layout163 this.body.emitter.on("refreshEdges", this.refresh.bind(this));164 this.body.emitter.on("refresh", this.refresh.bind(this));165 this.body.emitter.on("destroy", () => {166 util.forEach(this.edgesListeners, (callback, event) => {167 if (this.body.data.edges)168 this.body.data.edges.off(event, callback);169 });170 delete this.body.functions.createEdge;171 delete this.edgesListeners.add;172 delete this.edgesListeners.update;173 delete this.edgesListeners.remove;174 delete this.edgesListeners;175 });176 }177 /**178 *179 * @param {Object} options180 */181 setOptions(options) {182 if (options !== undefined) {183 // use the parser from the Edge class to fill in all shorthand notations184 Edge.parseOptions(this.options, options, true, this.defaultOptions, true);185 // update smooth settings in all edges186 let dataChanged = false;187 if (options.smooth !== undefined) {188 for (let edgeId in this.body.edges) {189 if (this.body.edges.hasOwnProperty(edgeId)) {190 dataChanged = this.body.edges[edgeId].updateEdgeType() || dataChanged;191 }192 }193 }194 // update fonts in all edges195 if (options.font !== undefined) {196 for (let edgeId in this.body.edges) {197 if (this.body.edges.hasOwnProperty(edgeId)) {198 this.body.edges[edgeId].updateLabelModule();199 }200 }201 }202 // update the state of the variables if needed203 if (options.hidden !== undefined || options.physics !== undefined || dataChanged === true) {204 this.body.emitter.emit('_dataChanged');205 }206 }207 }208 /**209 * Load edges by reading the data table210 * @param {Array | DataSet | DataView} edges The data containing the edges.211 * @param {boolean} [doNotEmit=false]212 * @private213 */214 setData(edges, doNotEmit = false) {215 var oldEdgesData = this.body.data.edges;216 if (edges instanceof DataSet || edges instanceof DataView) {217 this.body.data.edges = edges;218 }219 else if (Array.isArray(edges)) {220 this.body.data.edges = new DataSet();221 this.body.data.edges.add(edges);222 }223 else if (!edges) {224 this.body.data.edges = new DataSet();225 }226 else {227 throw new TypeError('Array or DataSet expected');228 }229 // TODO: is this null or undefined or false?230 if (oldEdgesData) {231 // unsubscribe from old dataset232 util.forEach(this.edgesListeners, (callback, event) => {oldEdgesData.off(event, callback);});233 }234 // remove drawn edges235 this.body.edges = {};236 // TODO: is this null or undefined or false?237 if (this.body.data.edges) {238 // subscribe to new dataset239 util.forEach(this.edgesListeners, (callback, event) => {this.body.data.edges.on(event, callback);});240 // draw all new nodes241 var ids = this.body.data.edges.getIds();242 this.add(ids, true);243 }244 this.body.emitter.emit('_adjustEdgesForHierarchicalLayout');245 if (doNotEmit === false) {246 this.body.emitter.emit("_dataChanged");247 }248 }249 /**250 * Add edges251 * @param {number[] | string[]} ids252 * @param {boolean} [doNotEmit=false]253 * @private254 */255 add(ids, doNotEmit = false) {256 var edges = this.body.edges;257 var edgesData = this.body.data.edges;258 for (let i = 0; i < ids.length; i++) {259 var id = ids[i];260 var oldEdge = edges[id];261 if (oldEdge) {262 oldEdge.disconnect();263 }264 var data = edgesData.get(id, {"showInternalIds" : true});265 edges[id] = this.create(data);266 }267 this.body.emitter.emit('_adjustEdgesForHierarchicalLayout');268 if (doNotEmit === false) {269 this.body.emitter.emit("_dataChanged");270 }271 }272 /**273 * Update existing edges, or create them when not yet existing274 * @param {number[] | string[]} ids275 * @private276 */277 update(ids) {278 var edges = this.body.edges;279 var edgesData = this.body.data.edges;280 var dataChanged = false;281 for (var i = 0; i < ids.length; i++) {282 var id = ids[i];283 var data = edgesData.get(id);284 var edge = edges[id];285 if (edge !== undefined) {286 // update edge287 edge.disconnect();288 dataChanged = edge.setOptions(data) || dataChanged; // if a support node is added, data can be changed.289 edge.connect();290 }291 else {292 // create edge293 this.body.edges[id] = this.create(data);294 dataChanged = true;295 }296 }297 if (dataChanged === true) {298 this.body.emitter.emit('_adjustEdgesForHierarchicalLayout');299 this.body.emitter.emit("_dataChanged");300 }301 else {302 this.body.emitter.emit("_dataUpdated");303 }304 }305 /**306 * Remove existing edges. Non existing ids will be ignored307 * @param {number[] | string[]} ids308 * @param {boolean} [emit=true]309 * @private310 */311 remove(ids, emit = true) {312 if (ids.length === 0) return; // early out313 var edges = this.body.edges;314 util.forEach(ids, (id) => {315 var edge = edges[id];316 if (edge !== undefined) {317 edge.remove();318 }319 });320 if (emit) {321 this.body.emitter.emit("_dataChanged");322 }323 }324 /**325 * Refreshes Edge Handler326 */327 refresh() {328 util.forEach(this.body.edges, (edge, edgeId) => {329 let data = this.body.data.edges._data[edgeId];330 if (data !== undefined) {331 edge.setOptions(data);332 }333 });334 }335 /**336 *337 * @param {Object} properties338 * @returns {Edge}339 */340 create(properties) {341 return new Edge(properties, this.body, this.options, this.defaultOptions)342 }343 /**344 * Reconnect all edges345 * @private346 */347 reconnectEdges() {348 var id;349 var nodes = this.body.nodes;350 var edges = this.body.edges;351 for (id in nodes) {352 if (nodes.hasOwnProperty(id)) {353 nodes[id].edges = [];354 }355 }356 for (id in edges) {357 if (edges.hasOwnProperty(id)) {358 var edge = edges[id];359 edge.from = null;360 edge.to = null;361 edge.connect();362 }363 }364 }365 /**366 *367 * @param {Edge.id} edgeId368 * @returns {Array}369 */370 getConnectedNodes(edgeId) {371 let nodeList = [];372 if (this.body.edges[edgeId] !== undefined) {373 let edge = this.body.edges[edgeId];374 if (edge.fromId !== undefined) {nodeList.push(edge.fromId);}375 if (edge.toId !== undefined) {nodeList.push(edge.toId);}376 }377 return nodeList;378 }379 /**380 * There is no direct relation between the nodes and the edges DataSet,381 * so the right place to do call this is in the handler for event `_dataUpdated`.382 */383 _updateState() {384 this._addMissingEdges();385 this._removeInvalidEdges();386 }387 /**388 * Scan for missing nodes and remove corresponding edges, if any.389 * @private390 */391 _removeInvalidEdges() {392 393 let edgesToDelete = [];394 util.forEach(this.body.edges, (edge, id) => {395 let toNode = this.body.nodes[edge.toId];396 let fromNode = this.body.nodes[edge.fromId];397 // Skip clustering edges here, let the Clustering module handle those398 if ((toNode !== undefined && toNode.isCluster === true)399 || (fromNode !== undefined && fromNode.isCluster === true)) {400 return;401 }402 if (toNode === undefined || fromNode === undefined) {403 edgesToDelete.push(id);404 }405 });406 this.remove(edgesToDelete, false);407 }408 /**409 * add all edges from dataset that are not in the cached state410 * @private411 */ 412 _addMissingEdges() {413 let edges = this.body.edges;414 let edgesData = this.body.data.edges;415 let addIds = [];416 edgesData.forEach((edgeData, edgeId) => {417 let edge = edges[edgeId];418 if(edge===undefined) {419 addIds.push(edgeId);420 }421 });422 423 this.add(addIds,true);424 } 425}...

Full Screen

Full Screen

Using AI Code Generation

copy

Full Screen

1var tracetest = require('tracetest');2var edges = tracetest.edges;3console.log(edges);4var tracetest = require('tracetest');5var edges = tracetest.edges;6console.log(edges);7var tracetest = require('tracetest');8var edges = tracetest.edges;9console.log(edges);10var tracetest = require('tracetest');11var edges = tracetest.edges;12console.log(edges);13var tracetest = require('tracetest');14var edges = tracetest.edges;15console.log(edges);16var tracetest = require('tracetest');17var edges = tracetest.edges;18console.log(edges);19var tracetest = require('tracetest');20var edges = tracetest.edges;21console.log(edges);22var tracetest = require('tracetest');23var edges = tracetest.edges;24console.log(edges);25var tracetest = require('tracetest');26var edges = tracetest.edges;27console.log(edges);28var tracetest = require('tracetest');29var edges = tracetest.edges;30console.log(edges);31var tracetest = require('tracetest');32var edges = tracetest.edges;33console.log(edges);34var tracetest = require('tracetest');35var edges = tracetest.edges;36console.log(edges);

Full Screen

Using AI Code Generation

copy

Full Screen

1var tracetest = require('tracetest');2tracetest.edges();3var trace = require('trace');4var edges = function() {5 trace.edges();6}7exports.edges = edges;8var edges = function() {9 console.log('edges');10}11exports.edges = edges;

Full Screen

Using AI Code Generation

copy

Full Screen

1var tracer = require('./tracetest.js');2var edges = tracer.edges;3];4console.log(edges(graph));5var edges = function(graph) {6 var result = [];7 for (var i = 0; i < graph.length; i++) {8 for (var j = 0; j < graph[i].length; j++) {9 if (graph[i][j] === 1) {10 result.push([i, j]);11 }12 }13 }14 return result;15};16module.exports.edges = edges;17var edges = function(graph) {18 var result = [];19 for (var i = 0; i < graph.length; i++) {20 for (var j = 0; j < graph[i].length; j++) {21 if (graph[i][j] ===

Full Screen

Using AI Code Generation

copy

Full Screen

1var tracetest = require("tracetest");2var edges = tracetest.edges();3var edge = edges.next();4while(edge) {5 console.log(edge);6 edge = edges.next();7}8{ from: '0', to: '1', label: 'a' }9{ from: '1', to: '2', label: 'b' }10{ from: '2', to: '3', label: 'c' }11{ from: '3', to: '4', label: 'd' }12{ from: '4', to: '5', label: 'e' }13{ from: '5', to: '6', label: 'f' }14{ from: '6', to: '7', label: 'g' }15{ from: '7', to: '8', label: 'h' }16{ from: '8', to: '9', label: 'i' }17{ from: '9', to: '10', label: 'j' }18{ from: '10', to: '11', label: 'k' }19{ from: '11', to: '12', label: 'l' }20{ from: '12', to: '13', label: 'm' }21{ from: '13', to: '14', label: 'n' }22{ from: '14', to: '15', label: 'o' }23{ from: '15', to: '16', label: 'p' }24{ from: '16', to: '17', label: 'q' }25{ from: '17', to: '18', label: 'r' }26{ from: '18', to: '19', label: 's' }27{ from: '19', to: '20', label: 't' }28{ from: '20', to: '21', label: 'u' }29{ from: '21', to: '22', label: 'v' }30{ from: '22', to: '23', label: 'w' }31{ from: '23', to: '24', label: 'x' }32{ from: '24', to: '25', label: 'y' }33{ from: '25', to: '26', label: 'z' }34{ from: '26', to: '27', label:

Full Screen

Using AI Code Generation

copy

Full Screen

1var edges = tracetest.edges();2var graph = new Graph();3for (var i = 0; i < edges.length; i++) {4 graph.addEdges(edges[i].from, edges[i].to, edges[i].cost);5}6var path = graph.shortestPath(1, 2);7for (var i = 0; i < path.length; i++) {8 tracetest.trace(path[i]);9}10var distance = graph.distance;11console.log("distance: " + distance);12console.log("path: " + path);13for (var i = 0; i < path.length; i++) {14 tracetest.trace(path[i]);15}16var distance = graph.distance;17console.log("distance: " + distance);18console.log("path: " + path);19for (var i = 0; i < path.length; i++) {20 tracetest.trace(path[i]);21}22var distance = graph.distance;23console.log("distance: " + distance);24console.log("path: " + path);25for (var i = 0; i < path.length; i++) {26 tracetest.trace(path[i]);27}28var distance = graph.distance;29console.log("distance: " + distance);30console.log("path: " + path);

Full Screen

Automation Testing Tutorials

Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.

LambdaTest Learning Hubs:

YouTube

You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.

Run tracetest automation tests on LambdaTest cloud grid

Perform automation testing on 3000+ real desktop and mobile devices online.

Try LambdaTest Now !!

Get 100 minutes of automation test minutes FREE!!

Next-Gen App & Browser Testing Cloud

Was this article helpful?

Helpful

NotHelpful