renderer.spec.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. import { test, expect } from '@playwright/test';
  2. const copy = obj => JSON.parse(JSON.stringify(obj));
  3. const delay = async ms => new Promise(resolve => setTimeout(resolve, ms));
  4. const expectUniquePoints = pts => {
  5. const toStringPt = pt => `(${pt.x},${pt.y})`;
  6. const toStringPts = pts => pts.map(toStringPt).join(' ');
  7. const str = toStringPts(pts);
  8. const strUnique = Array.from(new Set(pts.map(toStringPt))).join(' ');
  9. expect(str).toBe(strUnique);
  10. };
  11. const dist = (pt1, pt2) => Math.sqrt((pt1.x - pt2.x) ** 2 + (pt1.y - pt2.y) ** 2);
  12. test.describe('Renderer', () => {
  13. test.beforeEach(async ({ page }) => {
  14. page.on('console', (msg) => console.log(`[browser] ${msg.text()}`));
  15. await page.setViewportSize({ width: 1280, height: 720 });
  16. await page.goto('http://127.0.0.1:3333/playwright-page/index.html');
  17. });
  18. test('starts with no nodes', async ({ page }) => {
  19. const numNodes = await page.evaluate(() => {
  20. const cy = window.cy;
  21. return cy.nodes().length;
  22. });
  23. expect(numNodes).toBe(0);
  24. });
  25. test('adds a node', async ({ page }) => {
  26. const numNodes = await page.evaluate(() => {
  27. const cy = window.cy;
  28. cy.add({
  29. data: { id: 'foo' }
  30. });
  31. return cy.nodes().length;
  32. });
  33. expect(numNodes).toBe(1);
  34. });
  35. test.describe('node style', () => {
  36. test.beforeEach(async ({ page }) => {
  37. await page.evaluate(() => {
  38. const cy = window.cy;
  39. cy.add({ data: { id: 'a' } });
  40. });
  41. }); // beforeEach
  42. test('node bounding box extends beyond width and height for bordered triangle', async ({ page }) => {
  43. const bb = await page.evaluate(() => {
  44. const cy = window.cy;
  45. cy.style().fromJson([
  46. {
  47. selector: 'node',
  48. style: {
  49. 'shape': 'triangle',
  50. 'width': 100,
  51. 'height': 100,
  52. 'border-width': 10,
  53. 'border-color': 'black',
  54. }
  55. }
  56. ]).update();
  57. return cy.$('#a').boundingBox();
  58. });
  59. expect(bb.w).toBeGreaterThan(105);
  60. expect(bb.h).toBeGreaterThan(105);
  61. } ); // node bounding box extends beyond width and height for triangle
  62. test('boundingBox() does not throw for polygon node with border and edge', async ({ page }) => {
  63. const bb = await page.evaluate(() => {
  64. const cy = window.cy;
  65. cy.add([
  66. {
  67. data: { id: 'p' },
  68. position: { x: 100, y: 100 }
  69. },
  70. {
  71. data: { id: 'q' },
  72. position: { x: 200, y: 200 }
  73. },
  74. {
  75. data: { id: 'pq', source: 'p', target: 'q' }
  76. }
  77. ]);
  78. cy.style().fromJson([
  79. {
  80. selector: '#p',
  81. style: {
  82. 'shape': 'polygon',
  83. 'shape-polygon-points': '-1 -1 1 -1 1 1 -1 1',
  84. 'width': 50,
  85. 'height': 50,
  86. 'border-width': 5,
  87. 'border-color': 'black',
  88. }
  89. }
  90. ]).update();
  91. return cy.$('#p').boundingBox();
  92. });
  93. expect(bb.w).toBeGreaterThan(0);
  94. expect(bb.h).toBeGreaterThan(0);
  95. expect(bb.x1).toBeDefined();
  96. expect(bb.x2).toBeDefined();
  97. expect(bb.y1).toBeDefined();
  98. expect(bb.y2).toBeDefined();
  99. }); // boundingBox() does not throw for polygon node with border and edge
  100. test('polygon node bounding box extends beyond width and height for border', async ({ page }) => {
  101. const bb = await page.evaluate(() => {
  102. const cy = window.cy;
  103. cy.style().fromJson([
  104. {
  105. selector: 'node',
  106. style: {
  107. 'shape': 'polygon',
  108. 'shape-polygon-points': '-1 -1 1 -1 1 1 -1 1',
  109. 'width': 100,
  110. 'height': 100,
  111. 'border-width': 10,
  112. 'border-color': 'black',
  113. }
  114. }
  115. ]).update();
  116. return cy.$('#a').boundingBox();
  117. });
  118. expect(bb.w).toBeGreaterThan(105);
  119. expect(bb.h).toBeGreaterThan(105);
  120. }); // polygon node bounding box extends beyond width and height for border
  121. });
  122. test.describe('straight edges', () => {
  123. test.beforeEach(async ({ page }) => {
  124. await page.evaluate(() => {
  125. const cy = window.cy;
  126. cy.style().fromJson([
  127. {
  128. selector: 'edge',
  129. style: {
  130. 'curve-style': 'straight'
  131. }
  132. }
  133. ]).update();
  134. cy.add([
  135. {
  136. data: { id: 'a' },
  137. position: { x: 0, y: 0 }
  138. },
  139. {
  140. data: { id: 'b' },
  141. position: { x: 0, y: 0 }
  142. },
  143. {
  144. data: { id: 'ab1', source: 'a', target: 'b' }
  145. },
  146. {
  147. data: { id: 'ab2', source: 'a', target: 'b' }
  148. }
  149. ]);
  150. });
  151. }); // beforeEach
  152. test('initial bounding box is zero', async ({ page }) => {
  153. let bb1 = await page.evaluate(() => cy.$('#ab1').boundingBox());
  154. let bb2 = await page.evaluate(() => cy.$('#ab2').boundingBox());
  155. expect(bb1.w).toEqual(0);
  156. expect(bb1.h).toEqual(0);
  157. expect(bb2.w).toEqual(0);
  158. expect(bb2.h).toEqual(0);
  159. }); // initial bounding box is zero
  160. test('moved bounding box is nonzero', async ({ page }) => {
  161. await delay(500);
  162. await page.evaluate(() => {
  163. cy.layout({ name: 'grid', rows: 1, cols: 2 }).run();
  164. });
  165. await delay(500);
  166. let bb1 = await page.evaluate(() => cy.$('#ab1').boundingBox());
  167. let bb2 = await page.evaluate(() => cy.$('#ab2').boundingBox());
  168. expect(bb1.w).not.toEqual(0);
  169. expect(bb1.h).not.toEqual(0);
  170. expect(bb2.w).not.toEqual(0);
  171. expect(bb2.h).not.toEqual(0);
  172. }); // initial bounding box is nonzero
  173. test('manual endpoints correct', async ({ page }) => {
  174. // Set the endpoints manually
  175. const {
  176. ab1Source, ab1Target, ab2Source, ab2Target,
  177. ab1Midpoint, ab2Midpoint
  178. } = await page.evaluate(() => {
  179. const cy = window.cy;
  180. cy.$('#1').position({ x: 0, y: 0 });
  181. cy.$('#b').position({ x: 100, y: 0 });
  182. // Set the endpoints for the fist edge via style
  183. cy.$('#ab1').style('source-endpoint', '10px 10px');
  184. cy.$('#ab1').style('target-endpoint', '20px 10px');
  185. // Set the endpoints for the second edge via style
  186. cy.$('#ab2').style('source-endpoint', '30px 20px');
  187. cy.$('#ab2').style('target-endpoint', '40px 20px');
  188. // Return the endpoints for verification
  189. return {
  190. ab1Source: cy.$('#ab1').sourceEndpoint(),
  191. ab1Target: cy.$('#ab1').targetEndpoint(),
  192. ab2Source: cy.$('#ab2').sourceEndpoint(),
  193. ab2Target: cy.$('#ab2').targetEndpoint(),
  194. ab1Midpoint: cy.$('#ab1').midpoint(),
  195. ab2Midpoint: cy.$('#ab2').midpoint()
  196. };
  197. });
  198. // Verify the endpoints
  199. expect(ab1Source.x).toBe(10);
  200. expect(ab1Source.y).toBe(10);
  201. expect(ab1Target.x).toBe(20 + 100);
  202. expect(ab1Target.y).toBe(10);
  203. expect(ab2Source.x).toBe(30);
  204. expect(ab2Source.y).toBe(20);
  205. expect(ab2Target.x).toBe(40 + 100);
  206. expect(ab2Target.y).toBe(20);
  207. // Verify the midpoint y values only
  208. expect(ab1Midpoint.y).toEqual(10);
  209. expect(ab2Midpoint.y).toEqual(20);
  210. }); // manual endpoints correct
  211. });
  212. test.describe('bundled beziers', () => {
  213. const stepSize = 40;
  214. let ctrlpts1;
  215. let pt_ab1_1, pt_ab2_1;
  216. test.beforeEach(async ({ page }) => {
  217. ctrlpts1 = await page.evaluate(() => {
  218. const cy = window.cy;
  219. cy.style().fromJson([
  220. {
  221. selector: 'edge',
  222. style: {
  223. 'curve-style': 'bezier',
  224. 'control-point-step-size': 40
  225. }
  226. }
  227. ]).update();
  228. cy.add([
  229. {
  230. data: { id: 'a' }
  231. },
  232. {
  233. data: { id: 'b' }
  234. },
  235. {
  236. data: { id: 'ab1', source: 'a', target: 'b' }
  237. },
  238. {
  239. data: { id: 'ab2', source: 'a', target: 'b' }
  240. }
  241. ]);
  242. cy.layout({ name: 'grid', rows: 1, cols: 2 }).run();
  243. return cy.edges().map(edge => edge.controlPoints()[0]);
  244. });
  245. pt_ab1_1 = await page.evaluate(() => {
  246. return window.cy.$('#ab1').controlPoints()[0];
  247. });
  248. pt_ab2_1 = await page.evaluate(() => {
  249. return window.cy.$('#ab2').controlPoints()[0];
  250. });
  251. }); // beforeEach
  252. test('move when adding to the bundle', async ({ page }) => {
  253. expect(ctrlpts1.length).toBe(2);
  254. expectUniquePoints(ctrlpts1);
  255. // distance between the two control points
  256. let d_1_01 = dist(ctrlpts1[0], ctrlpts1[1]);
  257. expect(d_1_01).toBe(stepSize);
  258. let ctrlpts2 = await page.evaluate(() => {
  259. const cy = window.cy;
  260. cy.add([
  261. {
  262. data: { id: 'ab3', source: 'a', target: 'b' }
  263. },
  264. {
  265. data: { id: 'ab4', source: 'a', target: 'b' }
  266. }
  267. ]);
  268. return cy.edges().map(edge => edge.controlPoints()[0]);
  269. });
  270. // console.log(ctrlpts2);
  271. expect(ctrlpts2.length).toBe(4);
  272. expectUniquePoints(ctrlpts2);
  273. let d_2_01 = dist(ctrlpts2[0], ctrlpts2[1]);
  274. let d_2_12 = dist(ctrlpts2[1], ctrlpts2[2]);
  275. let d_2_23 = dist(ctrlpts2[2], ctrlpts2[3]);
  276. expect(d_2_01).toBeCloseTo(stepSize);
  277. expect(d_2_12).toBeCloseTo(stepSize);
  278. expect(d_2_23).toBeCloseTo(stepSize);
  279. let pt_ab1_2 = await page.evaluate(() => {
  280. return window.cy.$('#ab1').controlPoints()[0];
  281. });
  282. let pt_ab2_2 = await page.evaluate(() => {
  283. return window.cy.$('#ab2').controlPoints()[0];
  284. });
  285. // ctrl pts for ab1 and ab2 should have changed
  286. expect(pt_ab1_1).not.toEqual(pt_ab1_2);
  287. expect(pt_ab2_1).not.toEqual(pt_ab2_2);
  288. }); // move when adding to the bundle
  289. test('move when removing from the bundle', async ({ page }) => {
  290. await page.evaluate(() => {
  291. const cy = window.cy;
  292. cy.add([
  293. {
  294. data: { id: 'ab3', source: 'a', target: 'b' }
  295. },
  296. {
  297. data: { id: 'ab4', source: 'a', target: 'b' }
  298. },
  299. {
  300. data: { id: 'ab5', source: 'a', target: 'b' }
  301. }
  302. ]);
  303. });
  304. let pt_ab1_2 = await page.evaluate(() => cy.$('#ab1').controlPoints()[0]);
  305. let pt_ab2_2 = await page.evaluate(() => cy.$('#ab2').controlPoints()[0]);
  306. let ctrlpts3 = await page.evaluate(() => {
  307. const cy = window.cy;
  308. cy.$('#ab3').remove(); // only ab1,2,4,5 left
  309. return cy.edges().map(edge => edge.controlPoints()[0]);
  310. });
  311. let pt_ab1_3 = await page.evaluate(() => cy.$('#ab1').controlPoints()[0]);
  312. let pt_ab2_3 = await page.evaluate(() => cy.$('#ab2').controlPoints()[0]);
  313. expectUniquePoints(ctrlpts3);
  314. expect(pt_ab1_2).not.toEqual(pt_ab1_3);
  315. expect(pt_ab2_2).not.toEqual(pt_ab2_3);
  316. let d_3_01 = dist(ctrlpts3[0], ctrlpts3[1]);
  317. let d_3_12 = dist(ctrlpts3[1], ctrlpts3[2]);
  318. let d_3_23 = dist(ctrlpts3[2], ctrlpts3[3]);
  319. expect(d_3_01).toBeCloseTo(stepSize);
  320. expect(d_3_12).toBeCloseTo(stepSize);
  321. expect(d_3_23).toBeCloseTo(stepSize);
  322. }); // move when removing from the bundle
  323. test('do not move when setting one edge to visibility:hidden', async ({ page }) => {
  324. await page.evaluate(() => {
  325. window.cy.$('#ab1').style('visibility', 'hidden');
  326. });
  327. let pt_ab1_2 = await page.evaluate(() => cy.$('#ab1').controlPoints()[0]);
  328. let pt_ab2_2 = await page.evaluate(() => cy.$('#ab2').controlPoints()[0]);
  329. expect(pt_ab1_1).toEqual(pt_ab1_2);
  330. expect(pt_ab2_1).toEqual(pt_ab2_2);
  331. }); // do not move when setting one edge to visibility:hidden
  332. test('move when setting one edge to display:none', async ({ page }) => {
  333. await page.evaluate(() => {
  334. window.cy.$('#ab1').style('display', 'none');
  335. });
  336. let pt_ab1_2 = await page.evaluate(() => cy.$('#ab1').controlPoints());
  337. let pt_ab2_2 = await page.evaluate(() => cy.$('#ab2').controlPoints());
  338. expect(pt_ab1_2).toBeUndefined(); // because display: none
  339. expect(pt_ab2_2).toBeUndefined(); // because only one edge left => straight
  340. }); // move when setting one edge to display:none
  341. test('move when setting one edge to display:none (bigger bundle)', async ({ page }) => {
  342. await page.evaluate(() => {
  343. window.cy.add([
  344. {
  345. data: { id: 'ab3', source: 'a', target: 'b' }
  346. },
  347. {
  348. data: { id: 'ab4', source: 'a', target: 'b' }
  349. },
  350. {
  351. data: { id: 'ab5', source: 'a', target: 'b' }
  352. }
  353. ]);
  354. cy.$('#ab1').style('display', 'none');
  355. });
  356. let pt_ab1_2 = await page.evaluate(() => cy.$('#ab1').controlPoints());
  357. let pt_ab2_2 = await page.evaluate(() => cy.$('#ab2').controlPoints()[0]);
  358. expect(pt_ab1_2).toBeUndefined(); // because display: none
  359. expect(pt_ab2_2).toBeDefined();
  360. }); // move when setting one edge to display:none
  361. test('move when setting one edge to curve-style:straight', async ({ page }) => {
  362. await page.evaluate(() => {
  363. window.cy.add([
  364. {
  365. data: { id: 'ab3', source: 'a', target: 'b' }
  366. },
  367. {
  368. data: { id: 'ab4', source: 'a', target: 'b' }
  369. },
  370. {
  371. data: { id: 'ab5', source: 'a', target: 'b' }
  372. }
  373. ]);
  374. cy.$('#ab1').style('curve-style', 'straight');
  375. });
  376. let pt_ab1_2 = await page.evaluate(() => window.cy.$('#ab1').controlPoints());
  377. let pt_ab2_2 = await page.evaluate(() => window.cy.$('#ab2').controlPoints()[0]);
  378. expect(pt_ab1_2).toBeUndefined(); // because curve-style:straight
  379. expect(pt_ab2_2).toBeDefined();
  380. }); // move when setting one edge to curve-style:straight
  381. test('diagonal bottom left to top right', async ({page}) => {
  382. let controlPoints = await page.evaluate(() => {
  383. const cy = window.cy;
  384. cy.style().fromJson([{
  385. selector: 'edge',
  386. style: {
  387. 'curve-style': 'bezier',
  388. 'control-point-step-size': Math.sqrt(200)
  389. }
  390. }]).update();
  391. cy.add([{
  392. data: {id: 'a'}
  393. }, {
  394. data: {id: 'b'}
  395. }, {
  396. data: {id: 'ab1', source: 'a', target: 'b'}
  397. }, {
  398. data: {id: 'ab2', source: 'a', target: 'b'}
  399. }]);
  400. var presetOptions = {
  401. name: 'preset',
  402. positions: {
  403. a: {x: 0, y: 100},
  404. b: {x: 100, y: 0}
  405. }
  406. };
  407. cy.layout(presetOptions).run();
  408. let pt_ab1_1 = cy.$('#ab1').controlPoints()[0];
  409. let pt_ab2_1 = cy.$('#ab2').controlPoints()[0];
  410. return [pt_ab1_1, pt_ab2_1]
  411. });
  412. // Step size 14.14, sqrt(10*10+10*10).
  413. // Gives a 5 pixel translation away from the mid-point,
  414. // if the vector between the nodes are at a 45 deg angle.
  415. // Mid-point is at (50,50)
  416. expect(controlPoints[0].x).toBeCloseTo(45, 5);
  417. expect(controlPoints[0].y).toBeCloseTo(45, 5);
  418. expect(controlPoints[1].x).toBeCloseTo(55, 5);
  419. expect(controlPoints[1].y).toBeCloseTo(55, 5);
  420. });
  421. // test('do not move when straight edge added', async ({ page }) => {
  422. // await page.evaluate(() => {
  423. // window.cy.add([
  424. // {
  425. // data: { id: 'ab3', source: 'a', target: 'b' },
  426. // style: {
  427. // 'curve-style': 'straight'
  428. // }
  429. // }
  430. // ]);
  431. // });
  432. // // TODO...
  433. // }); // move when setting one edge to curve-style:straight
  434. }); // bundled beziers
  435. test.describe('rounded-edges', () => {
  436. let singleBentEdge;
  437. let midCollinearEdge;
  438. test.beforeEach(async ({ page }) => {
  439. await page.evaluate(() => {
  440. const cy = window.cy;
  441. cy.style().fromJson([
  442. {
  443. selector: 'edge',
  444. style: {
  445. 'curve-style': 'round-segments',
  446. 'segment-radii': 50,
  447. 'radius-type': 'influence-radius'
  448. }
  449. },
  450. {
  451. selector: '#ab1',
  452. style: {
  453. 'segment-weights': [0.5],
  454. 'segment-distances': [50]
  455. }
  456. },
  457. {
  458. selector: '#ab2',
  459. style: {
  460. 'segment-weights': [0.25 , 0.5, 0.75],
  461. 'segment-distances': [50, 50, 50]
  462. }
  463. }
  464. ]).update();
  465. cy.add([
  466. {
  467. data: { id: 'a' }
  468. },
  469. {
  470. data: { id: 'b' }
  471. },
  472. {
  473. data: { id: 'ab1', source: 'a', target: 'b' }
  474. },
  475. {
  476. data: { id: 'ab2', source: 'a', target: 'b' }
  477. },
  478. ]);
  479. cy.layout({ name: 'grid', rows: 1, cols: 2 }).run();
  480. singleBentEdge = cy.$('#ab1');
  481. midCollinearEdge = cy.$('#ab2');
  482. });
  483. }) // Before Each
  484. test('collinear mid point correctly defined', async ({page}) => {
  485. const midpoint = await page.evaluate(() => midCollinearEdge.midpoint());
  486. const points = await page.evaluate(() => midCollinearEdge.segmentPoints());
  487. expect(midpoint.x).not.toBeNaN();
  488. expect(midpoint.y).not.toBeNaN();
  489. expect(midpoint).toMatchObject(points[1])
  490. });
  491. test('mid point correctly defined', async ({page}) => {
  492. const {midpoint, points, a,b} = await page.evaluate(() => ({
  493. midpoint: singleBentEdge.midpoint(),
  494. points: singleBentEdge.segmentPoints(),
  495. a: singleBentEdge.source().position(),
  496. b: singleBentEdge.target().position(),
  497. }));
  498. expect(midpoint.x).not.toBeNaN();
  499. expect(midpoint.y).not.toBeNaN();
  500. const controlPoint = points[0];
  501. expect(midpoint).not.toMatchObject(controlPoint); // The mid point is supposed to be on the curve and not on the control point
  502. const isInsideTriangle = (point, {a,b,c}) => {
  503. const dX = point.x - a.x, dY = point.y - a.y;
  504. const dX21 = b.x - a.x, dY21 = b.y - a.y;
  505. const dX31 = c.x - a.x, dY31 = c.y - a.y;
  506. const denom = dY21 * dX31 - dX21 * dY31;
  507. const alpha = (dY21 * dX - dX21 * dY) / denom;
  508. const beta = (dX31 * dY - dY31 * dX) / denom;
  509. const gamma = 1 - alpha - beta;
  510. return alpha >= 0 && beta >= 0 && gamma >= 0;
  511. };
  512. // The actual midpoint should be within a triangle between the source, the target and the defined control point
  513. expect(isInsideTriangle(midpoint, {a,b,c: controlPoint})).toBeTruthy()
  514. })
  515. }); // Rounded edges
  516. test.describe('with layout', () => {
  517. test('single node cose layout with bounding box', async ({ page }) => {
  518. const pos = await page.evaluate(async () => {
  519. const cy = window.cy;
  520. // remove all eles
  521. cy.elements().remove();
  522. // add one node
  523. let node = cy.add({ data: { id: 'a' } });
  524. // run layout
  525. let layout = cy.layout({
  526. name: 'cose',
  527. boundingBox: {
  528. x1: 0,
  529. y1: 0,
  530. x2: 100,
  531. y2: 100
  532. },
  533. });
  534. let layoutstop = layout.promiseOn('layoutstop');
  535. layout.run();
  536. await layoutstop;
  537. return node.position();
  538. });
  539. expect(pos.x).not.toBeNaN();
  540. expect(pos.y).not.toBeNaN();
  541. }); // single node cose layout
  542. }); // with layout
  543. }); // renderer