cytoscape
Version:
Graph theory (a.k.a. network) library for analysis and visualisation
610 lines (484 loc) • 17.8 kB
JavaScript
import { test, expect } from '@playwright/test';
const copy = obj => JSON.parse(JSON.stringify(obj));
const delay = async ms => new Promise(resolve => setTimeout(resolve, ms));
const expectUniquePoints = pts => {
const toStringPt = pt => `(${pt.x},${pt.y})`;
const toStringPts = pts => pts.map(toStringPt).join(' ');
const str = toStringPts(pts);
const strUnique = Array.from(new Set(pts.map(toStringPt))).join(' ');
expect(str).toBe(strUnique);
};
const dist = (pt1, pt2) => Math.sqrt((pt1.x - pt2.x) ** 2 + (pt1.y - pt2.y) ** 2);
test.describe('Renderer', () => {
test.beforeEach(async ({ page }) => {
page.on('console', (msg) => console.log(`[browser] ${msg.text()}`));
await page.setViewportSize({ width: 1280, height: 720 });
await page.goto('http://127.0.0.1:3333/playwright-page/index.html');
});
test('starts with no nodes', async ({ page }) => {
const numNodes = await page.evaluate(() => {
const cy = window.cy;
return cy.nodes().length;
});
expect(numNodes).toBe(0);
});
test('adds a node', async ({ page }) => {
const numNodes = await page.evaluate(() => {
const cy = window.cy;
cy.add({
data: { id: 'foo' }
});
return cy.nodes().length;
});
expect(numNodes).toBe(1);
});
test.describe('node style', () => {
test.beforeEach(async ({ page }) => {
await page.evaluate(() => {
const cy = window.cy;
cy.add({ data: { id: 'a' } });
});
}); // beforeEach
test('node bounding box extends beyond width and height for bordered triangle', async ({ page }) => {
const bb = await page.evaluate(() => {
const cy = window.cy;
cy.style().fromJson([
{
selector: 'node',
style: {
'shape': 'triangle',
'width': 100,
'height': 100,
'border-width': 10,
'border-color': 'black',
}
}
]).update();
return cy.$('#a').boundingBox();
});
expect(bb.w).toBeGreaterThan(105);
expect(bb.h).toBeGreaterThan(105);
} ); // node bounding box extends beyond width and height for triangle
});
test.describe('straight edges', () => {
test.beforeEach(async ({ page }) => {
await page.evaluate(() => {
const cy = window.cy;
cy.style().fromJson([
{
selector: 'edge',
style: {
'curve-style': 'straight'
}
}
]).update();
cy.add([
{
data: { id: 'a' },
position: { x: 0, y: 0 }
},
{
data: { id: 'b' },
position: { x: 0, y: 0 }
},
{
data: { id: 'ab1', source: 'a', target: 'b' }
},
{
data: { id: 'ab2', source: 'a', target: 'b' }
}
]);
});
}); // beforeEach
test('initial bounding box is zero', async ({ page }) => {
let bb1 = await page.evaluate(() => cy.$('#ab1').boundingBox());
let bb2 = await page.evaluate(() => cy.$('#ab2').boundingBox());
expect(bb1.w).toEqual(0);
expect(bb1.h).toEqual(0);
expect(bb2.w).toEqual(0);
expect(bb2.h).toEqual(0);
}); // initial bounding box is zero
test('moved bounding box is nonzero', async ({ page }) => {
await delay(500);
await page.evaluate(() => {
cy.layout({ name: 'grid', rows: 1, cols: 2 }).run();
});
await delay(500);
let bb1 = await page.evaluate(() => cy.$('#ab1').boundingBox());
let bb2 = await page.evaluate(() => cy.$('#ab2').boundingBox());
expect(bb1.w).not.toEqual(0);
expect(bb1.h).not.toEqual(0);
expect(bb2.w).not.toEqual(0);
expect(bb2.h).not.toEqual(0);
}); // initial bounding box is nonzero
test('manual endpoints correct', async ({ page }) => {
// Set the endpoints manually
const {
ab1Source, ab1Target, ab2Source, ab2Target,
ab1Midpoint, ab2Midpoint
} = await page.evaluate(() => {
const cy = window.cy;
cy.$('#1').position({ x: 0, y: 0 });
cy.$('#b').position({ x: 100, y: 0 });
// Set the endpoints for the fist edge via style
cy.$('#ab1').style('source-endpoint', '10px 10px');
cy.$('#ab1').style('target-endpoint', '20px 10px');
// Set the endpoints for the second edge via style
cy.$('#ab2').style('source-endpoint', '30px 20px');
cy.$('#ab2').style('target-endpoint', '40px 20px');
// Return the endpoints for verification
return {
ab1Source: cy.$('#ab1').sourceEndpoint(),
ab1Target: cy.$('#ab1').targetEndpoint(),
ab2Source: cy.$('#ab2').sourceEndpoint(),
ab2Target: cy.$('#ab2').targetEndpoint(),
ab1Midpoint: cy.$('#ab1').midpoint(),
ab2Midpoint: cy.$('#ab2').midpoint()
};
});
// Verify the endpoints
expect(ab1Source.x).toBe(10);
expect(ab1Source.y).toBe(10);
expect(ab1Target.x).toBe(20 + 100);
expect(ab1Target.y).toBe(10);
expect(ab2Source.x).toBe(30);
expect(ab2Source.y).toBe(20);
expect(ab2Target.x).toBe(40 + 100);
expect(ab2Target.y).toBe(20);
// Verify the midpoint y values only
expect(ab1Midpoint.y).toEqual(10);
expect(ab2Midpoint.y).toEqual(20);
}); // manual endpoints correct
});
test.describe('bundled beziers', () => {
const stepSize = 40;
let ctrlpts1;
let pt_ab1_1, pt_ab2_1;
test.beforeEach(async ({ page }) => {
ctrlpts1 = await page.evaluate(() => {
const cy = window.cy;
cy.style().fromJson([
{
selector: 'edge',
style: {
'curve-style': 'bezier',
'control-point-step-size': 40
}
}
]).update();
cy.add([
{
data: { id: 'a' }
},
{
data: { id: 'b' }
},
{
data: { id: 'ab1', source: 'a', target: 'b' }
},
{
data: { id: 'ab2', source: 'a', target: 'b' }
}
]);
cy.layout({ name: 'grid', rows: 1, cols: 2 }).run();
return cy.edges().map(edge => edge.controlPoints()[0]);
});
pt_ab1_1 = await page.evaluate(() => {
return window.cy.$('#ab1').controlPoints()[0];
});
pt_ab2_1 = await page.evaluate(() => {
return window.cy.$('#ab2').controlPoints()[0];
});
}); // beforeEach
test('move when adding to the bundle', async ({ page }) => {
expect(ctrlpts1.length).toBe(2);
expectUniquePoints(ctrlpts1);
// distance between the two control points
let d_1_01 = dist(ctrlpts1[0], ctrlpts1[1]);
expect(d_1_01).toBe(stepSize);
let ctrlpts2 = await page.evaluate(() => {
const cy = window.cy;
cy.add([
{
data: { id: 'ab3', source: 'a', target: 'b' }
},
{
data: { id: 'ab4', source: 'a', target: 'b' }
}
]);
return cy.edges().map(edge => edge.controlPoints()[0]);
});
// console.log(ctrlpts2);
expect(ctrlpts2.length).toBe(4);
expectUniquePoints(ctrlpts2);
let d_2_01 = dist(ctrlpts2[0], ctrlpts2[1]);
let d_2_12 = dist(ctrlpts2[1], ctrlpts2[2]);
let d_2_23 = dist(ctrlpts2[2], ctrlpts2[3]);
expect(d_2_01).toBeCloseTo(stepSize);
expect(d_2_12).toBeCloseTo(stepSize);
expect(d_2_23).toBeCloseTo(stepSize);
let pt_ab1_2 = await page.evaluate(() => {
return window.cy.$('#ab1').controlPoints()[0];
});
let pt_ab2_2 = await page.evaluate(() => {
return window.cy.$('#ab2').controlPoints()[0];
});
// ctrl pts for ab1 and ab2 should have changed
expect(pt_ab1_1).not.toEqual(pt_ab1_2);
expect(pt_ab2_1).not.toEqual(pt_ab2_2);
}); // move when adding to the bundle
test('move when removing from the bundle', async ({ page }) => {
await page.evaluate(() => {
const cy = window.cy;
cy.add([
{
data: { id: 'ab3', source: 'a', target: 'b' }
},
{
data: { id: 'ab4', source: 'a', target: 'b' }
},
{
data: { id: 'ab5', source: 'a', target: 'b' }
}
]);
});
let pt_ab1_2 = await page.evaluate(() => cy.$('#ab1').controlPoints()[0]);
let pt_ab2_2 = await page.evaluate(() => cy.$('#ab2').controlPoints()[0]);
let ctrlpts3 = await page.evaluate(() => {
const cy = window.cy;
cy.$('#ab3').remove(); // only ab1,2,4,5 left
return cy.edges().map(edge => edge.controlPoints()[0]);
});
let pt_ab1_3 = await page.evaluate(() => cy.$('#ab1').controlPoints()[0]);
let pt_ab2_3 = await page.evaluate(() => cy.$('#ab2').controlPoints()[0]);
expectUniquePoints(ctrlpts3);
expect(pt_ab1_2).not.toEqual(pt_ab1_3);
expect(pt_ab2_2).not.toEqual(pt_ab2_3);
let d_3_01 = dist(ctrlpts3[0], ctrlpts3[1]);
let d_3_12 = dist(ctrlpts3[1], ctrlpts3[2]);
let d_3_23 = dist(ctrlpts3[2], ctrlpts3[3]);
expect(d_3_01).toBeCloseTo(stepSize);
expect(d_3_12).toBeCloseTo(stepSize);
expect(d_3_23).toBeCloseTo(stepSize);
}); // move when removing from the bundle
test('do not move when setting one edge to visibility:hidden', async ({ page }) => {
await page.evaluate(() => {
window.cy.$('#ab1').style('visibility', 'hidden');
});
let pt_ab1_2 = await page.evaluate(() => cy.$('#ab1').controlPoints()[0]);
let pt_ab2_2 = await page.evaluate(() => cy.$('#ab2').controlPoints()[0]);
expect(pt_ab1_1).toEqual(pt_ab1_2);
expect(pt_ab2_1).toEqual(pt_ab2_2);
}); // do not move when setting one edge to visibility:hidden
test('move when setting one edge to display:none', async ({ page }) => {
await page.evaluate(() => {
window.cy.$('#ab1').style('display', 'none');
});
let pt_ab1_2 = await page.evaluate(() => cy.$('#ab1').controlPoints());
let pt_ab2_2 = await page.evaluate(() => cy.$('#ab2').controlPoints());
expect(pt_ab1_2).toBeUndefined(); // because display: none
expect(pt_ab2_2).toBeUndefined(); // because only one edge left => straight
}); // move when setting one edge to display:none
test('move when setting one edge to display:none (bigger bundle)', async ({ page }) => {
await page.evaluate(() => {
window.cy.add([
{
data: { id: 'ab3', source: 'a', target: 'b' }
},
{
data: { id: 'ab4', source: 'a', target: 'b' }
},
{
data: { id: 'ab5', source: 'a', target: 'b' }
}
]);
cy.$('#ab1').style('display', 'none');
});
let pt_ab1_2 = await page.evaluate(() => cy.$('#ab1').controlPoints());
let pt_ab2_2 = await page.evaluate(() => cy.$('#ab2').controlPoints()[0]);
expect(pt_ab1_2).toBeUndefined(); // because display: none
expect(pt_ab2_2).toBeDefined();
}); // move when setting one edge to display:none
test('move when setting one edge to curve-style:straight', async ({ page }) => {
await page.evaluate(() => {
window.cy.add([
{
data: { id: 'ab3', source: 'a', target: 'b' }
},
{
data: { id: 'ab4', source: 'a', target: 'b' }
},
{
data: { id: 'ab5', source: 'a', target: 'b' }
}
]);
cy.$('#ab1').style('curve-style', 'straight');
});
let pt_ab1_2 = await page.evaluate(() => window.cy.$('#ab1').controlPoints());
let pt_ab2_2 = await page.evaluate(() => window.cy.$('#ab2').controlPoints()[0]);
expect(pt_ab1_2).toBeUndefined(); // because curve-style:straight
expect(pt_ab2_2).toBeDefined();
}); // move when setting one edge to curve-style:straight
test('diagonal bottom left to top right', async ({page}) => {
let controlPoints = await page.evaluate(() => {
const cy = window.cy;
cy.style().fromJson([{
selector: 'edge',
style: {
'curve-style': 'bezier',
'control-point-step-size': Math.sqrt(200)
}
}]).update();
cy.add([{
data: {id: 'a'}
}, {
data: {id: 'b'}
}, {
data: {id: 'ab1', source: 'a', target: 'b'}
}, {
data: {id: 'ab2', source: 'a', target: 'b'}
}]);
var presetOptions = {
name: 'preset',
positions: {
a: {x: 0, y: 100},
b: {x: 100, y: 0}
}
};
cy.layout(presetOptions).run();
let pt_ab1_1 = cy.$('#ab1').controlPoints()[0];
let pt_ab2_1 = cy.$('#ab2').controlPoints()[0];
return [pt_ab1_1, pt_ab2_1]
});
// Step size 14.14, sqrt(10*10+10*10).
// Gives a 5 pixel translation away from the mid-point,
// if the vector between the nodes are at a 45 deg angle.
// Mid-point is at (50,50)
expect(controlPoints[0].x).toBeCloseTo(45, 5);
expect(controlPoints[0].y).toBeCloseTo(45, 5);
expect(controlPoints[1].x).toBeCloseTo(55, 5);
expect(controlPoints[1].y).toBeCloseTo(55, 5);
});
// test('do not move when straight edge added', async ({ page }) => {
// await page.evaluate(() => {
// window.cy.add([
// {
// data: { id: 'ab3', source: 'a', target: 'b' },
// style: {
// 'curve-style': 'straight'
// }
// }
// ]);
// });
// // TODO...
// }); // move when setting one edge to curve-style:straight
}); // bundled beziers
test.describe('rounded-edges', () => {
let singleBentEdge;
let midCollinearEdge;
test.beforeEach(async ({ page }) => {
await page.evaluate(() => {
const cy = window.cy;
cy.style().fromJson([
{
selector: 'edge',
style: {
'curve-style': 'round-segments',
'segment-radii': 50,
'radius-type': 'influence-radius'
}
},
{
selector: '#ab1',
style: {
'segment-weights': [0.5],
'segment-distances': [50]
}
},
{
selector: '#ab2',
style: {
'segment-weights': [0.25 , 0.5, 0.75],
'segment-distances': [50, 50, 50]
}
}
]).update();
cy.add([
{
data: { id: 'a' }
},
{
data: { id: 'b' }
},
{
data: { id: 'ab1', source: 'a', target: 'b' }
},
{
data: { id: 'ab2', source: 'a', target: 'b' }
},
]);
cy.layout({ name: 'grid', rows: 1, cols: 2 }).run();
singleBentEdge = cy.$('#ab1');
midCollinearEdge = cy.$('#ab2');
});
}) // Before Each
test('collinear mid point correctly defined', async ({page}) => {
const midpoint = await page.evaluate(() => midCollinearEdge.midpoint());
const points = await page.evaluate(() => midCollinearEdge.segmentPoints());
expect(midpoint.x).not.toBeNaN();
expect(midpoint.y).not.toBeNaN();
expect(midpoint).toMatchObject(points[1])
});
test('mid point correctly defined', async ({page}) => {
const {midpoint, points, a,b} = await page.evaluate(() => ({
midpoint: singleBentEdge.midpoint(),
points: singleBentEdge.segmentPoints(),
a: singleBentEdge.source().position(),
b: singleBentEdge.target().position(),
}));
expect(midpoint.x).not.toBeNaN();
expect(midpoint.y).not.toBeNaN();
const controlPoint = points[0];
expect(midpoint).not.toMatchObject(controlPoint); // The mid point is supposed to be on the curve and not on the control point
const isInsideTriangle = (point, {a,b,c}) => {
const dX = point.x - a.x, dY = point.y - a.y;
const dX21 = b.x - a.x, dY21 = b.y - a.y;
const dX31 = c.x - a.x, dY31 = c.y - a.y;
const denom = dY21 * dX31 - dX21 * dY31;
const alpha = (dY21 * dX - dX21 * dY) / denom;
const beta = (dX31 * dY - dY31 * dX) / denom;
const gamma = 1 - alpha - beta;
return alpha >= 0 && beta >= 0 && gamma >= 0;
};
// The actual midpoint should be within a triangle between the source, the target and the defined control point
expect(isInsideTriangle(midpoint, {a,b,c: controlPoint})).toBeTruthy()
})
}); // Rounded edges
test.describe('with layout', () => {
test('single node cose layout with bounding box', async ({ page }) => {
const pos = await page.evaluate(async () => {
const cy = window.cy;
// remove all eles
cy.elements().remove();
// add one node
let node = cy.add({ data: { id: 'a' } });
// run layout
let layout = cy.layout({
name: 'cose',
boundingBox: {
x1: 0,
y1: 0,
x2: 100,
y2: 100
},
});
let layoutstop = layout.promiseOn('layoutstop');
layout.run();
await layoutstop;
return node.position();
});
expect(pos.x).not.toBeNaN();
expect(pos.y).not.toBeNaN();
}); // single node cose layout
}); // with layout
}); // renderer