Skip to content

Commit bb6984f

Browse files
committed
canvas cluster analysis and related tests
1 parent 521e8fe commit bb6984f

File tree

3 files changed

+277
-2
lines changed

3 files changed

+277
-2
lines changed

e2e/logic/POM/codeGraph.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Locator, Page } from "playwright";
22
import BasePage from "../../infra/ui/basePage";
3-
import { waitToBeEnabled } from "../utils";
3+
import { delay, waitToBeEnabled } from "../utils";
4+
import { analyzeCanvasWithLocator, CanvasAnalysisResult } from "../canvasAnalysis";
45

56
export default class CodeGraph extends BasePage {
67
/* NavBar Locators*/
@@ -113,6 +114,28 @@ export default class CodeGraph extends BasePage {
113114
private get notificationError(): Locator {
114115
return this.page.locator("//div[@role='region']//ol//li");
115116
}
117+
118+
/* Canvas Locators*/
119+
120+
private get canvasElement(): Locator {
121+
return this.page.locator("//canvas[position()=3]");
122+
}
123+
124+
private get zoomInBtn(): Locator {
125+
return this.page.locator("//button[@title='Zoom In']");
126+
}
127+
128+
private get zoomOutBtn(): Locator {
129+
return this.page.locator("//button[@title='Zoom Out']");
130+
}
131+
132+
private get centerBtn(): Locator {
133+
return this.page.locator("//button[@title='Center']");
134+
}
135+
136+
private get removeNodeViaElementMenu(): Locator {
137+
return this.page.locator("//button[@title='Remove']");
138+
}
116139

117140
/* NavBar functionality */
118141
async clickOnFalkorDbLogo(): Promise<Page> {
@@ -253,6 +276,36 @@ export default class CodeGraph extends BasePage {
253276
return await this.searchBarList.evaluate((element) => {
254277
return element.scrollTop + element.clientHeight >= element.scrollHeight;
255278
});
256-
}
279+
}
280+
281+
/* Canvas functionality */
282+
283+
async getCanvasAnalysis(): Promise<CanvasAnalysisResult> {
284+
await delay(2000);
285+
return await analyzeCanvasWithLocator(this.canvasElement);
286+
}
287+
288+
async clickZoomIn(): Promise<void> {
289+
await this.zoomInBtn.click();
290+
}
291+
292+
async clickZoomOut(): Promise<void> {
293+
await this.zoomOutBtn.click();
294+
}
295+
296+
async clickCenter(): Promise<void> {
297+
await this.centerBtn.click();
298+
}
299+
300+
async clickOnRemoveNodeViaElementMenu(): Promise<void> {
301+
await this.removeNodeViaElementMenu.click();
302+
}
303+
304+
async rightClickOnNode(x : number, y: number): Promise<void> {
305+
const boundingBox = (await this.canvasElement.boundingBox())!;
306+
const adjustedX = boundingBox.x + Math.round(x);
307+
const adjustedY = boundingBox.y + Math.round(y);
308+
await this.page.mouse.click(adjustedX, adjustedY, { button: 'right' });
309+
}
257310

258311
}

e2e/logic/canvasAnalysis.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { Locator } from "@playwright/test";
2+
3+
export type Pixel = { x: number; y: number };
4+
5+
export interface CanvasAnalysisResult {
6+
red: Array<{ x: number; y: number; radius: number }>;
7+
yellow: Array<{ x: number; y: number; radius: number }>;
8+
green: Array<{ x: number; y: number; radius: number }>;
9+
}
10+
11+
export async function analyzeCanvasWithLocator(locator: Locator) {
12+
const canvasHandle = await locator.evaluateHandle((canvas) => canvas as HTMLCanvasElement);
13+
const canvasElement = await canvasHandle.asElement();
14+
if (!canvasElement) {
15+
throw new Error("Failed to retrieve canvas element");
16+
}
17+
18+
// Retrieve the original canvas width
19+
const originalCanvasWidth = await canvasElement.evaluate((canvas) => canvas.width);
20+
21+
const result = await canvasElement.evaluate(
22+
(canvas, originalWidth) => {
23+
const ctx = canvas?.getContext("2d");
24+
if (!ctx) {
25+
throw new Error("Failed to get 2D context");
26+
}
27+
28+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
29+
const { data, width, height } = imageData;
30+
31+
const scaleFactor = canvas.width / originalWidth;
32+
const adjustedRadius = 3 / scaleFactor;
33+
const adjustedMergeRadius = 10 / scaleFactor;
34+
35+
type Pixel = { x: number; y: number };
36+
37+
const redPixels: Pixel[] = [];
38+
const yellowPixels: Pixel[] = [];
39+
const greenPixels: Pixel[] = [];
40+
41+
const isRedPixel = (r: number, g: number, b: number) => r > 170 && g < 120 && b < 120;
42+
const isYellowPixel = (r: number, g: number, b: number) => r > 170 && g > 170 && b < 130;
43+
const isGreenPixel = (r: number, g: number, b: number) => g > 120 && g > r && g > b && r < 50 && b < 160;
44+
45+
for (let y = 0; y < height; y++) {
46+
for (let x = 0; x < width; x++) {
47+
const i = (y * width + x) * 4;
48+
const r = data[i];
49+
const g = data[i + 1];
50+
const b = data[i + 2];
51+
52+
if (isRedPixel(r, g, b)) redPixels.push({ x, y });
53+
if (isYellowPixel(r, g, b)) yellowPixels.push({ x, y });
54+
if (isGreenPixel(r, g, b)) greenPixels.push({ x, y });
55+
}
56+
}
57+
58+
const clusterNodes = (pixels: Pixel[], radius: number): Pixel[][] => {
59+
const visited = new Set<string>();
60+
const clusters: Pixel[][] = [];
61+
62+
pixels.forEach((pixel) => {
63+
const key = `${pixel.x},${pixel.y}`;
64+
if (visited.has(key)) return;
65+
66+
const cluster: Pixel[] = [];
67+
const stack: Pixel[] = [pixel];
68+
69+
while (stack.length > 0) {
70+
const current = stack.pop()!;
71+
const currentKey = `${current.x},${current.y}`;
72+
if (visited.has(currentKey)) continue;
73+
74+
visited.add(currentKey);
75+
cluster.push(current);
76+
77+
pixels.forEach((neighbor) => {
78+
const dist = Math.sqrt(
79+
(current.x - neighbor.x) ** 2 + (current.y - neighbor.y) ** 2
80+
);
81+
if (dist <= radius) stack.push(neighbor);
82+
});
83+
}
84+
85+
clusters.push(cluster);
86+
});
87+
88+
return clusters;
89+
};
90+
91+
const mergeCloseClusters = (clusters: Pixel[][], mergeRadius: number): Pixel[][] => {
92+
const mergedClusters: Pixel[][] = [];
93+
const used = new Set<number>();
94+
95+
for (let i = 0; i < clusters.length; i++) {
96+
if (used.has(i)) continue;
97+
98+
let merged = [...clusters[i]];
99+
for (let j = i + 1; j < clusters.length; j++) {
100+
if (used.has(j)) continue;
101+
102+
const dist = Math.sqrt(
103+
(merged[0].x - clusters[j][0].x) ** 2 +
104+
(merged[0].y - clusters[j][0].y) ** 2
105+
);
106+
107+
if (dist <= mergeRadius) {
108+
merged = merged.concat(clusters[j]);
109+
used.add(j);
110+
}
111+
}
112+
113+
mergedClusters.push(merged);
114+
used.add(i);
115+
}
116+
117+
return mergedClusters;
118+
};
119+
120+
const redClusters = clusterNodes(redPixels, adjustedRadius);
121+
const yellowClusters = clusterNodes(yellowPixels, adjustedRadius);
122+
const greenClusters = clusterNodes(greenPixels, adjustedRadius);
123+
124+
const mergedGreenClusters = mergeCloseClusters(greenClusters, adjustedMergeRadius);
125+
126+
const filteredRedClusters = redClusters.filter((cluster) => cluster.length >= 5);
127+
const filteredYellowClusters = yellowClusters.filter((cluster) => cluster.length >= 5);
128+
const filteredGreenClusters = mergedGreenClusters.filter((cluster) => cluster.length >= 5);
129+
130+
const calculateRadius = (cluster: Pixel[], scaleFactor: number) => {
131+
const rawRadius = Math.sqrt(cluster.length / Math.PI) / scaleFactor;
132+
return Math.round(rawRadius * 1000) / 1000;
133+
};
134+
135+
return {
136+
red: filteredRedClusters.map(cluster => ({
137+
x: cluster.reduce((sum, p) => sum + p.x, 0) / cluster.length,
138+
y: cluster.reduce((sum, p) => sum + p.y, 0) / cluster.length,
139+
radius: calculateRadius(cluster, scaleFactor)
140+
})),
141+
yellow: filteredYellowClusters.map(cluster => ({
142+
x: cluster.reduce((sum, p) => sum + p.x, 0) / cluster.length,
143+
y: cluster.reduce((sum, p) => sum + p.y, 0) / cluster.length,
144+
radius: calculateRadius(cluster, scaleFactor)
145+
})),
146+
green: filteredGreenClusters.map(cluster => ({
147+
x: cluster.reduce((sum, p) => sum + p.x, 0) / cluster.length,
148+
y: cluster.reduce((sum, p) => sum + p.y, 0) / cluster.length,
149+
radius: calculateRadius(cluster, scaleFactor)
150+
}))
151+
};
152+
},
153+
originalCanvasWidth
154+
);
155+
156+
await canvasHandle.dispose();
157+
return result;
158+
}

e2e/tests/codeGraph.spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import urls from "../config/urls.json";
55
import { GRAPH_ID } from "../config/constants";
66
import { delay } from "../logic/utils";
77
import { searchData, specialCharacters } from "../config/testData";
8+
import { CanvasAnalysisResult } from "../logic/canvasAnalysis";
9+
10+
const colors: (keyof CanvasAnalysisResult)[] = ["red", "yellow", "green"];
811

912
test.describe("Code graph tests", () => {
1013
let browser: BrowserWrapper;
@@ -62,4 +65,65 @@ test.describe("Code graph tests", () => {
6265
expect(await codeGraph.isNotificationError()).toBe(expectedRes);
6366
});
6467
});
68+
69+
test(`Verify zoom in functionality on canvas`, async () => {
70+
const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl);
71+
await codeGraph.selectGraph(GRAPH_ID);
72+
const initialNodeAnalysis = await codeGraph.getCanvasAnalysis();
73+
await codeGraph.clickZoomIn();
74+
await codeGraph.clickZoomIn();
75+
const updatedNodeAnalysis = await codeGraph.getCanvasAnalysis();
76+
for (const color of colors) {
77+
const initialRadius = initialNodeAnalysis[color][0].radius;
78+
const updatedRadius = updatedNodeAnalysis[color][0].radius;
79+
expect(initialRadius).toBeDefined();
80+
expect(updatedRadius).toBeDefined();
81+
expect(updatedRadius).toBeGreaterThan(initialRadius);
82+
}
83+
})
84+
85+
test(`Verify zoom out functionality on canvas`, async () => {
86+
const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl);
87+
await codeGraph.selectGraph(GRAPH_ID);
88+
const initialNodeAnalysis = await codeGraph.getCanvasAnalysis();
89+
for (let i = 0; i < 5; i++) {
90+
await codeGraph.clickZoomOut();
91+
}
92+
const updatedNodeAnalysis = await codeGraph.getCanvasAnalysis();
93+
for (const color of colors) {
94+
const initialRadius = initialNodeAnalysis[color][0].radius;
95+
const updatedRadius = updatedNodeAnalysis[color][0].radius;
96+
expect(initialRadius).toBeDefined();
97+
expect(updatedRadius).toBeDefined();
98+
expect(updatedRadius).toBeLessThan(initialRadius);
99+
}
100+
})
101+
102+
test(`Verify center graph button centers nodes in canvas`, async () => {
103+
const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl);
104+
await codeGraph.selectGraph(GRAPH_ID);
105+
const initialNodeAnalysis = await codeGraph.getCanvasAnalysis();
106+
await codeGraph.clickZoomIn();
107+
await codeGraph.clickZoomIn();
108+
await codeGraph.clickCenter();
109+
const updatedNodeAnalysis = await codeGraph.getCanvasAnalysis();
110+
for (const color of colors) {
111+
const initialRadius = Math.round(initialNodeAnalysis[color][0].radius);
112+
const updatedRadius = Math.round(updatedNodeAnalysis[color][0].radius);
113+
expect(initialRadius).toBeDefined();
114+
expect(updatedRadius).toBeDefined();
115+
expect(updatedRadius).toBeCloseTo(initialRadius);
116+
}
117+
})
118+
119+
test(`Validate node removal functionality via element menu in canvas`, async () => {
120+
const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl);
121+
await codeGraph.selectGraph(GRAPH_ID);
122+
const initialNodeAnalysis = await codeGraph.getCanvasAnalysis();
123+
await codeGraph.rightClickOnNode(initialNodeAnalysis.red[0].x, initialNodeAnalysis.red[0].y);
124+
await codeGraph.clickOnRemoveNodeViaElementMenu();
125+
const updatedNodeAnalysis = await codeGraph.getCanvasAnalysis();
126+
expect(initialNodeAnalysis.red.length).toBeGreaterThan(updatedNodeAnalysis.red.length);
127+
});
128+
65129
});

0 commit comments

Comments
 (0)