Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/components/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,14 @@ export default function Input({ value, onValueChange, handleSubmit, graph, icon,
value={value || ""}
onChange={(e) => {
const newVal = e.target.value
const invalidChars = /[%*()\-\[\]{};:"|~]/;

if (invalidChars.test(newVal)) {
e.target.setCustomValidity(`The character "${newVal.match(invalidChars)?.[0]}" is not allowed in this field.`);
e.target.reportValidity();
return;
}
e.target.setCustomValidity('');
onValueChange({ name: newVal })
}}
{...props}
Expand Down
4 changes: 2 additions & 2 deletions e2e/config/testData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ const categorizeCharacters = (characters: string[], expectedRes: boolean): { cha
};

export const specialCharacters: { character: string; expectedRes: boolean }[] = [
...categorizeCharacters(['%', '*', '(', ')', '-', '[', ']', '{', '}', ';', ':', '"', '|', '~'], true),
...categorizeCharacters(['!', '@', '$', '^', '_', '=', '+', "'", ',', '.', '<', '>', '/', '?', '\\', '`', '&', '#'], false)
...categorizeCharacters(['%', '*', '(', ')', '-', '[', ']', '{', '}', ';', ':', '"', '|', '~'], false),
...categorizeCharacters(['!', '@', '$', '^', '_', '=', '+', "'", ',', '.', '<', '>', '/', '?', '\\', '`', '&', '#'], true)
];
57 changes: 55 additions & 2 deletions e2e/logic/POM/codeGraph.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Locator, Page } from "playwright";
import BasePage from "../../infra/ui/basePage";
import { waitToBeEnabled } from "../utils";
import { delay, waitToBeEnabled } from "../utils";
import { analyzeCanvasWithLocator, CanvasAnalysisResult } from "../canvasAnalysis";

export default class CodeGraph extends BasePage {
/* NavBar Locators*/
Expand Down Expand Up @@ -113,6 +114,28 @@ export default class CodeGraph extends BasePage {
private get notificationError(): Locator {
return this.page.locator("//div[@role='region']//ol//li");
}

/* Canvas Locators*/

private get canvasElement(): Locator {
return this.page.locator("//canvas[position()=3]");
}

private get zoomInBtn(): Locator {
return this.page.locator("//button[@title='Zoom In']");
}

private get zoomOutBtn(): Locator {
return this.page.locator("//button[@title='Zoom Out']");
}

private get centerBtn(): Locator {
return this.page.locator("//button[@title='Center']");
}

private get removeNodeViaElementMenu(): Locator {
return this.page.locator("//button[@title='Remove']");
}

/* NavBar functionality */
async clickOnFalkorDbLogo(): Promise<Page> {
Expand Down Expand Up @@ -253,6 +276,36 @@ export default class CodeGraph extends BasePage {
return await this.searchBarList.evaluate((element) => {
return element.scrollTop + element.clientHeight >= element.scrollHeight;
});
}
}

/* Canvas functionality */

async getCanvasAnalysis(): Promise<CanvasAnalysisResult> {
await delay(2000);
return await analyzeCanvasWithLocator(this.canvasElement);
}

async clickZoomIn(): Promise<void> {
await this.zoomInBtn.click();
}

async clickZoomOut(): Promise<void> {
await this.zoomOutBtn.click();
}

async clickCenter(): Promise<void> {
await this.centerBtn.click();
}

async clickOnRemoveNodeViaElementMenu(): Promise<void> {
await this.removeNodeViaElementMenu.click();
}

async rightClickOnNode(x : number, y: number): Promise<void> {
const boundingBox = (await this.canvasElement.boundingBox())!;
const adjustedX = boundingBox.x + Math.round(x);
const adjustedY = boundingBox.y + Math.round(y);
await this.page.mouse.click(adjustedX, adjustedY, { button: 'right' });
}

}
158 changes: 158 additions & 0 deletions e2e/logic/canvasAnalysis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { Locator } from "@playwright/test";

export type Pixel = { x: number; y: number };

export interface CanvasAnalysisResult {
red: Array<{ x: number; y: number; radius: number }>;
yellow: Array<{ x: number; y: number; radius: number }>;
green: Array<{ x: number; y: number; radius: number }>;
}

export async function analyzeCanvasWithLocator(locator: Locator) {
const canvasHandle = await locator.evaluateHandle((canvas) => canvas as HTMLCanvasElement);
const canvasElement = await canvasHandle.asElement();
if (!canvasElement) {
throw new Error("Failed to retrieve canvas element");
}

// Retrieve the original canvas width
const originalCanvasWidth = await canvasElement.evaluate((canvas) => canvas.width);

const result = await canvasElement.evaluate(
(canvas, originalWidth) => {
const ctx = canvas?.getContext("2d");
if (!ctx) {
throw new Error("Failed to get 2D context");
}

const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const { data, width, height } = imageData;

const scaleFactor = canvas.width / originalWidth;
const adjustedRadius = 3 / scaleFactor;
const adjustedMergeRadius = 10 / scaleFactor;

type Pixel = { x: number; y: number };

const redPixels: Pixel[] = [];
const yellowPixels: Pixel[] = [];
const greenPixels: Pixel[] = [];

const isRedPixel = (r: number, g: number, b: number) => r > 170 && g < 120 && b < 120;
const isYellowPixel = (r: number, g: number, b: number) => r > 170 && g > 170 && b < 130;
const isGreenPixel = (r: number, g: number, b: number) => g > 120 && g > r && g > b && r < 50 && b < 160;

for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];

if (isRedPixel(r, g, b)) redPixels.push({ x, y });
if (isYellowPixel(r, g, b)) yellowPixels.push({ x, y });
if (isGreenPixel(r, g, b)) greenPixels.push({ x, y });
}
}

const clusterNodes = (pixels: Pixel[], radius: number): Pixel[][] => {
const visited = new Set<string>();
const clusters: Pixel[][] = [];

pixels.forEach((pixel) => {
const key = `${pixel.x},${pixel.y}`;
if (visited.has(key)) return;

const cluster: Pixel[] = [];
const stack: Pixel[] = [pixel];

while (stack.length > 0) {
const current = stack.pop()!;
const currentKey = `${current.x},${current.y}`;
if (visited.has(currentKey)) continue;

visited.add(currentKey);
cluster.push(current);

pixels.forEach((neighbor) => {
const dist = Math.sqrt(
(current.x - neighbor.x) ** 2 + (current.y - neighbor.y) ** 2
);
if (dist <= radius) stack.push(neighbor);
});
}

clusters.push(cluster);
});

return clusters;
};

const mergeCloseClusters = (clusters: Pixel[][], mergeRadius: number): Pixel[][] => {
const mergedClusters: Pixel[][] = [];
const used = new Set<number>();

for (let i = 0; i < clusters.length; i++) {
if (used.has(i)) continue;

let merged = [...clusters[i]];
for (let j = i + 1; j < clusters.length; j++) {
if (used.has(j)) continue;

const dist = Math.sqrt(
(merged[0].x - clusters[j][0].x) ** 2 +
(merged[0].y - clusters[j][0].y) ** 2
);

if (dist <= mergeRadius) {
merged = merged.concat(clusters[j]);
used.add(j);
}
}

mergedClusters.push(merged);
used.add(i);
}

return mergedClusters;
};

const redClusters = clusterNodes(redPixels, adjustedRadius);
const yellowClusters = clusterNodes(yellowPixels, adjustedRadius);
const greenClusters = clusterNodes(greenPixels, adjustedRadius);

const mergedGreenClusters = mergeCloseClusters(greenClusters, adjustedMergeRadius);

const filteredRedClusters = redClusters.filter((cluster) => cluster.length >= 5);
const filteredYellowClusters = yellowClusters.filter((cluster) => cluster.length >= 5);
const filteredGreenClusters = mergedGreenClusters.filter((cluster) => cluster.length >= 5);

const calculateRadius = (cluster: Pixel[], scaleFactor: number) => {
const rawRadius = Math.sqrt(cluster.length / Math.PI) / scaleFactor;
return Math.round(rawRadius * 1000) / 1000;
};

return {
red: filteredRedClusters.map(cluster => ({
x: cluster.reduce((sum, p) => sum + p.x, 0) / cluster.length,
y: cluster.reduce((sum, p) => sum + p.y, 0) / cluster.length,
radius: calculateRadius(cluster, scaleFactor)
})),
yellow: filteredYellowClusters.map(cluster => ({
x: cluster.reduce((sum, p) => sum + p.x, 0) / cluster.length,
y: cluster.reduce((sum, p) => sum + p.y, 0) / cluster.length,
radius: calculateRadius(cluster, scaleFactor)
})),
green: filteredGreenClusters.map(cluster => ({
x: cluster.reduce((sum, p) => sum + p.x, 0) / cluster.length,
y: cluster.reduce((sum, p) => sum + p.y, 0) / cluster.length,
radius: calculateRadius(cluster, scaleFactor)
}))
};
},
originalCanvasWidth
);

await canvasHandle.dispose();
return result;
}
66 changes: 65 additions & 1 deletion e2e/tests/codeGraph.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import urls from "../config/urls.json";
import { GRAPH_ID } from "../config/constants";
import { delay } from "../logic/utils";
import { searchData, specialCharacters } from "../config/testData";
import { CanvasAnalysisResult } from "../logic/canvasAnalysis";

const colors: (keyof CanvasAnalysisResult)[] = ["red", "yellow", "green"];

test.describe("Code graph tests", () => {
let browser: BrowserWrapper;
Expand Down Expand Up @@ -59,7 +62,68 @@ test.describe("Code graph tests", () => {
await codeGraph.selectGraph(GRAPH_ID);
await codeGraph.fillSearchBar(character);
await delay(1000);
expect(await codeGraph.isNotificationError()).toBe(expectedRes);
expect((await codeGraph.getSearchBarInputValue()).includes(character)).toBe(expectedRes);
});
});

test(`Verify zoom in functionality on canvas`, async () => {
const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl);
await codeGraph.selectGraph(GRAPH_ID);
const initialNodeAnalysis = await codeGraph.getCanvasAnalysis();
await codeGraph.clickZoomIn();
await codeGraph.clickZoomIn();
const updatedNodeAnalysis = await codeGraph.getCanvasAnalysis();
for (const color of colors) {
const initialRadius = initialNodeAnalysis[color][0].radius;
const updatedRadius = updatedNodeAnalysis[color][0].radius;
expect(initialRadius).toBeDefined();
expect(updatedRadius).toBeDefined();
expect(updatedRadius).toBeGreaterThan(initialRadius);
}
})

test(`Verify zoom out functionality on canvas`, async () => {
const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl);
await codeGraph.selectGraph(GRAPH_ID);
const initialNodeAnalysis = await codeGraph.getCanvasAnalysis();
for (let i = 0; i < 5; i++) {
await codeGraph.clickZoomOut();
}
const updatedNodeAnalysis = await codeGraph.getCanvasAnalysis();
for (const color of colors) {
const initialRadius = initialNodeAnalysis[color][0].radius;
const updatedRadius = updatedNodeAnalysis[color][0].radius;
expect(initialRadius).toBeDefined();
expect(updatedRadius).toBeDefined();
expect(updatedRadius).toBeLessThan(initialRadius);
}
})

test(`Verify center graph button centers nodes in canvas`, async () => {
const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl);
await codeGraph.selectGraph(GRAPH_ID);
const initialNodeAnalysis = await codeGraph.getCanvasAnalysis();
await codeGraph.clickZoomIn();
await codeGraph.clickZoomIn();
await codeGraph.clickCenter();
const updatedNodeAnalysis = await codeGraph.getCanvasAnalysis();
for (const color of colors) {
const initialRadius = Math.round(initialNodeAnalysis[color][0].radius);
const updatedRadius = Math.round(updatedNodeAnalysis[color][0].radius);
expect(initialRadius).toBeDefined();
expect(updatedRadius).toBeDefined();
expect(updatedRadius).toBeCloseTo(initialRadius);
}
})

test(`Validate node removal functionality via element menu in canvas`, async () => {
const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl);
await codeGraph.selectGraph(GRAPH_ID);
const initialNodeAnalysis = await codeGraph.getCanvasAnalysis();
await codeGraph.rightClickOnNode(initialNodeAnalysis.red[0].x, initialNodeAnalysis.red[0].y);
await codeGraph.clickOnRemoveNodeViaElementMenu();
const updatedNodeAnalysis = await codeGraph.getCanvasAnalysis();
expect(initialNodeAnalysis.red.length).toBeGreaterThan(updatedNodeAnalysis.red.length);
});

});
Loading