diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 3893ea77..06b8b283 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -33,9 +33,20 @@ jobs: npm install npm run build NEXTAUTH_SECRET=SECRET npm start & npx playwright test --reporter=dot,list + - name: Ensure required directories exist + run: | + mkdir -p playwright-report + mkdir -p playwright-report/artifacts - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} + if: always() with: name: playwright-report path: playwright-report/ retention-days: 30 + - name: Upload failed test screenshots + if: always() + uses: actions/upload-artifact@v4 + with: + name: failed-test-screenshots + path: playwright-report/artifacts/ + retention-days: 30 diff --git a/.github/workflows/release-image.yml b/.github/workflows/release-image.yml new file mode 100644 index 00000000..679a5ae8 --- /dev/null +++ b/.github/workflows/release-image.yml @@ -0,0 +1,37 @@ +name: Release image to DockerHub + +on: + workflow_dispatch: + push: + tags: ["v*.*.*"] + branches: + - main + +jobs: + build-and-release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set tags + run: | + if ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }}; then + echo "TAGS=falkordb/code-graph-frontend:latest,falkordb/code-graph-frontend:${{ github.ref_name }}" >> $GITHUB_ENV + else + echo "TAGS=falkordb/code-graph-frontend:edge" >> $GITHUB_ENV + fi + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ env.TAGS }} diff --git a/Dockerfile b/Dockerfile index d5862299..d338bda6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Use a Node.js base image -FROM node:20 +FROM node:22 # Set working directory WORKDIR /app diff --git a/README.md b/README.md index 02bfb84d..edd18fcb 100644 --- a/README.md +++ b/README.md @@ -8,28 +8,80 @@ ## Getting Started [Live Demo](https://code-graph.falkordb.com/) +## Run locally +This project is composed of three pieces: + +1. FalkorDB Graph DB - this is where your graphs are stored and queried +2. Code-Graph-Backend - backend logic +3. Code-Graph-Frontend - website + +You'll need to start all three components: + ### Run FalkorDB ```bash docker run -p 6379:6379 -it --rm falkordb/falkordb ``` -### Install node packages +### Run Code-Graph-Backend + +#### Clone the Backend ```bash -npm install +git clone https://github.com/FalkorDB/code-graph-backend.git ``` -### Set your OpenAI key +#### Setup environment variables + +`SECRET_TOKEN` - user defined token used to authorize the request +```bash +export FALKORDB_HOST=localhost FALKORDB_PORT=6379 \ + OPENAI_API_KEY= SECRET_TOKEN= \ + FLASK_RUN_HOST=0.0.0.0 FLASK_RUN_PORT=5000 ``` -export OPENAI_API_KEY=YOUR_OPENAI_API_KEY + +#### Install dependencies & run + +```bash +cd code-graph-backend + +pip install --no-cache-dir -r requirements.txt + +flask --app api/index.py run --debug > flask.log 2>&1 & + ``` -### Run the development server +### Run Code-Graph-Frontend + +#### Clone the Frontend ```bash +git clone https://github.com/FalkorDB/code-graph.git +``` + +#### Setup environment variables + +```bash +export BACKEND_URL=http://${FLASK_RUN_HOST}:${FLASK_RUN_PORT} \ + SECRET_TOKEN= OPENAI_API_KEY= +``` + +#### Install dependencies & run + +```bash +cd code-graph +npm install npm run dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +### Process a local repository +```bash +curl -X POST http://127.0.0.1:5000/analyze_folder -H "Content-Type: application/json" -d '{"path": "", "ignore": ["./.github", "./sbin", "./.git","./deps", "./bin", "./build"]}' -H "Authorization: " +``` + +Note: At the moment code-graph can analyze both the C & Python source files. +Support for additional languages e.g. JavaScript, Go, Java is planned to be added +in the future. + +Browse to [http://localhost:3000](http://localhost:3000) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 6e2c127d..6af9ac79 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -387,7 +387,7 @@ export function Chat({ repo, path, setPath, graph, selectedPathId, isPathRespons parentClassName="w-full" graph={graph} onValueChange={({ name, id }) => setPath(prev => ({ start: { name, id }, end: prev?.end }))} - value={path?.start?.name} + value={path?.start?.name || ""} placeholder="Start typing starting point" type="text" icon={} @@ -397,7 +397,7 @@ export function Chat({ repo, path, setPath, graph, selectedPathId, isPathRespons setPath(prev => ({ end: { name, id }, start: prev?.start }))} placeholder="Start typing end point" type="text" diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index 7a99062d..a47cb798 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -271,7 +271,7 @@ export function CodeGraph({
setSearchNode({ name })} icon={} handleSubmit={handleSearchSubmit} diff --git a/app/components/elementMenu.tsx b/app/components/elementMenu.tsx index e1451f8a..b205ec0d 100644 --- a/app/components/elementMenu.tsx +++ b/app/components/elementMenu.tsx @@ -45,6 +45,7 @@ export default function ElementMenu({ obj, objects, setPath, handleRemove, posit left: Math.max(-34, Math.min(position.x - 33 - containerWidth / 2, (parentRef?.current?.clientWidth || 0) + 32 - containerWidth)), top: Math.min(position.y - 153, (parentRef?.current?.clientHeight || 0) - 9), }} + id="elementMenu" > { objects.some(o => o.id === obj.id) && objects.length > 1 ? diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..2e4ac22b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,36 @@ +version: "3.9" + +services: + falkordb: + image: falkordb/falkordb:latest + ports: + - "6379:6379" + - "3001:3000" + volumes: + - ./:/data/ + stdin_open: true # Keep the container's STDIN open + tty: true # Allocate a pseudo-TTY + + code-graph-frontend: + image: falkordb/code-graph-frontend:latest + ports: + - "3000:3000" + depends_on: + - code-graph-backend + environment: + - BACKEND_URL=http://code-graph-backend:5000 # Backend service URL + - SECRET_TOKEN=Vespa + + code-graph-backend: + image: falkordb/code-graph-backend:latest + ports: + - "4000:5000" + depends_on: + - falkordb + environment: + - FALKORDB_HOST=falkordb + - FALKORDB_PORT=6379 + - OPENAI_API_KEY=YOUR_OPENAI_API_KEY + - SECRET_TOKEN=Vespa + - FLASK_RUN_HOST=0.0.0.0 + - FLASK_RUN_PORT=5000 diff --git a/e2e/config/testData.ts b/e2e/config/testData.ts index dab5ebf7..3cb008e2 100644 --- a/e2e/config/testData.ts +++ b/e2e/config/testData.ts @@ -20,7 +20,7 @@ export const nodesPath: { firstNode: string; secondNode: string }[] = [ ]; export const nodes: { nodeName: string; }[] = [ - { nodeName: "import_data"}, + { nodeName: "ask"}, { nodeName: "add_edge" }, { nodeName: "test_kg_delete"}, { nodeName: "list_graphs"} diff --git a/e2e/logic/POM/codeGraph.ts b/e2e/logic/POM/codeGraph.ts index 5c3afa2c..7cc9ccd7 100644 --- a/e2e/logic/POM/codeGraph.ts +++ b/e2e/logic/POM/codeGraph.ts @@ -1,6 +1,6 @@ import { Locator, Page } from "playwright"; import BasePage from "../../infra/ui/basePage"; -import { delay, waitToBeEnabled } from "../utils"; +import { waitForElementToBeVisible, waitForStableText, waitToBeEnabled } from "../utils"; declare global { interface Window { @@ -156,6 +156,10 @@ export default class CodeGraph extends BasePage { return this.page.locator("//img[@alt='Waiting for response']"); } + private get waitingForResponseIndicator(): Locator { + return this.page.locator('img[alt="Waiting for response"]'); + } + /* Canvas Locators*/ private get canvasElement(): Locator { @@ -193,6 +197,10 @@ export default class CodeGraph extends BasePage { private get nodeDetailsPanel(): Locator { return this.page.locator("//div[@data-name='node-details-panel']"); } + + private get elementMenu(): Locator { + return this.page.locator("//div[@id='elementMenu']"); + } private get nodedetailsPanelHeader(): Locator { return this.page.locator("//div[@data-name='node-details-panel']/header/p"); @@ -220,6 +228,10 @@ export default class CodeGraph extends BasePage { private get copyToClipboardNodePanelDetails(): Locator { return this.page.locator(`//div[@data-name='node-details-panel']//button[@title='Copy src to clipboard']`); } + + private get nodeToolTip(): Locator { + return this.page.locator("//div[contains(@class, 'graph-tooltip')]"); + } /* NavBar functionality */ async clickOnFalkorDbLogo(): Promise { @@ -241,66 +253,81 @@ export default class CodeGraph extends BasePage { } async clickCreateNewProjectBtn(): Promise { + const isVisible = await waitForElementToBeVisible(this.createNewProjectBtn); + if (!isVisible) throw new Error("'Create New Project' button is not visible!"); await this.createNewProjectBtn.click(); } - + async isCreateNewProjectDialog(): Promise { - return await this.createNewProjectDialog.isVisible(); + return await waitForElementToBeVisible(this.createNewProjectDialog); } - - async clickonTipBtn(): Promise { + + async clickOnTipBtn(): Promise { await this.tipBtn.click(); } async isTipMenuVisible(): Promise { - await delay(500); - return await this.genericMenu.isVisible(); + await this.page.waitForTimeout(500); + return await waitForElementToBeVisible(this.genericMenu); } - - async clickonTipMenuCloseBtn(): Promise { + + async clickOnTipMenuCloseBtn(): Promise { + const isVisible = await waitForElementToBeVisible(this.tipMenuCloseBtn); + if (!isVisible) throw new Error("'Tip Menu Close' button is not visible!"); await this.tipMenuCloseBtn.click(); } + /* Chat functionality */ - async clickOnshowPathBtn(): Promise { + async clickOnShowPathBtn(): Promise { await this.showPathBtn.click(); } - async clickAskquestionBtn(): Promise { + async clickAskQuestionBtn(): Promise { + const isVisible = await waitForElementToBeVisible(this.askquestionBtn); + if (!isVisible) throw new Error("'Ask Question' button is not visible!"); await this.askquestionBtn.click(); } - + async sendMessage(message: string) { - await waitToBeEnabled(this.askquestionInput); + await waitToBeEnabled(this.askquestionBtn); await this.askquestionInput.fill(message); await this.askquestionBtn.click(); } - + async clickOnLightBulbBtn(): Promise { await this.lightbulbBtn.click(); } async getTextInLastChatElement(): Promise{ - await this.page.waitForSelector('img[alt="Waiting for response"]', { state: 'hidden' }); - await delay(2000); - return (await this.lastElementInChat.textContent())!; + await this.waitingForResponseIndicator.waitFor({ state: 'hidden' }); + return await waitForStableText(this.lastElementInChat); } - async getLastChatElementButtonCount(): Promise{ + async getLastChatElementButtonCount(): Promise { + const isVisible = await waitForElementToBeVisible(this.lastChatElementButtonCount); + if (!isVisible) return null; return await this.lastChatElementButtonCount.count(); } - async scrollToTop() { + async scrollToTop(): Promise { + const isVisible = await waitForElementToBeVisible(this.chatContainer); + if (!isVisible) throw new Error("Chat container is not visible!"); + await this.chatContainer.evaluate((chat) => { - chat.scrollTop = 0; + chat.scrollTop = 0; }); } async getScrollMetrics() { - const scrollTop = await this.chatContainer.evaluate((el) => el.scrollTop); - const scrollHeight = await this.chatContainer.evaluate((el) => el.scrollHeight); - const clientHeight = await this.chatContainer.evaluate((el) => el.clientHeight); - return { scrollTop, scrollHeight, clientHeight }; + const isVisible = await waitForElementToBeVisible(this.chatContainer); + if (!isVisible) throw new Error("Chat container is not visible!"); + + return await this.chatContainer.evaluate((el) => ({ + scrollTop: el.scrollTop, + scrollHeight: el.scrollHeight, + clientHeight: el.clientHeight + })); } async isAtBottom(): Promise { @@ -316,33 +343,43 @@ export default class CodeGraph extends BasePage { await this.selectInputForShowPath(inputNum).fill(node); await this.selectFirstPathOption(inputNum).click(); } - + async isNodeVisibleInLastChatPath(node: string): Promise { - await this.locateNodeInLastChatPath(node).waitFor({ state: 'visible' }); - return await this.locateNodeInLastChatPath(node).isVisible(); + const nodeLocator = this.locateNodeInLastChatPath(node); + return await waitForElementToBeVisible(nodeLocator); } async isNotificationError(): Promise { - await delay(500); + await this.page.waitForTimeout(500); return await this.notificationError.isVisible(); } async clickOnNotificationErrorCloseBtn(): Promise { + const isVisible = await waitForElementToBeVisible(this.notificationErrorCloseBtn); + if (!isVisible) throw new Error("Notification error close button is not visible!"); await this.notificationErrorCloseBtn.click(); } - + async clickOnQuestionOptionsMenu(): Promise { + const isVisible = await waitForElementToBeVisible(this.questionOptionsMenu); + if (!isVisible) throw new Error("Question options menu is not visible!"); await this.questionOptionsMenu.click(); } - + async selectAndGetQuestionInOptionsMenu(questionNumber: string): Promise { - await this.selectQuestionInMenu(questionNumber).click(); - return await this.selectQuestionInMenu(questionNumber).innerHTML(); + const question = this.selectQuestionInMenu(questionNumber); + const isVisible = await waitForElementToBeVisible(question); + if (!isVisible) throw new Error(`Question ${questionNumber} in menu is not visible!`); + + await question.click(); + return await question.innerHTML(); } - + async getLastQuestionInChat(): Promise { + const isVisible = await waitForElementToBeVisible(this.lastQuestionInChat); + if (!isVisible) throw new Error("Last question in chat is not visible!"); return await this.lastQuestionInChat.innerText(); - } + } /* CodeGraph functionality */ async selectGraph(graph: string | number): Promise { @@ -354,7 +391,7 @@ export default class CodeGraph extends BasePage { await this.selectGraphInComboBoxByName(graph).waitFor({ state : 'visible'}) await this.selectGraphInComboBoxByName(graph).click(); } - await delay(2000); // graph animation delay + await this.page.waitForTimeout(2000); // graph animation delay } async createProject(url : string): Promise { @@ -383,8 +420,9 @@ export default class CodeGraph extends BasePage { } async selectSearchBarOptionBtn(buttonNum: string): Promise { - await delay(1000); - await this.searchBarOptionBtn(buttonNum).click(); + const button = this.searchBarOptionBtn(buttonNum); + await button.waitFor({ state : "visible"}) + await button.click(); } async getSearchBarInputValue(): Promise { @@ -414,16 +452,37 @@ export default class CodeGraph extends BasePage { async clickCenter(): Promise { await this.centerBtn.click(); - await delay(2000); //animation delay + await this.page.waitForTimeout(2000); //animation delay } async clickOnRemoveNodeViaElementMenu(): Promise { - await this.elementMenuButton("Remove").click(); + const button = this.elementMenuButton("Remove"); + const isVisible = await waitForElementToBeVisible(button); + if (!isVisible) throw new Error("'Remove' button is not visible!"); + await button.click(); } - async nodeClick(x: number, y: number): Promise { + async nodeClick(x: number, y: number): Promise { + for (let attempt = 1; attempt <= 2; attempt++) { + await this.canvasElement.hover({ position: { x, y } }); + await this.page.waitForTimeout(500); + + if (await waitForElementToBeVisible(this.nodeToolTip)) { + await this.canvasElement.click({ position: { x, y }, button: 'right' }); + return; + } + await this.page.waitForTimeout(1000); + } + + throw new Error("Tooltip not visible after multiple attempts!"); + } + + async nodeClicktest(x: number, y: number): Promise { + console.log(`Clicking node at (${x}, ${y})`); + await this.page.waitForTimeout(500); await this.canvasElement.hover({ position: { x, y } }); await this.canvasElement.click({ position: { x, y }, button: 'right' }); + await this.page.waitForTimeout(5000); } async selectCodeGraphCheckbox(checkbox: string): Promise { @@ -453,17 +512,27 @@ export default class CodeGraph extends BasePage { } async isNodeDetailsPanel(): Promise { + await this.page.waitForTimeout(500); return this.nodeDetailsPanel.isVisible(); } async clickOnViewNode(): Promise { - await this.elementMenuButton("View Node").click(); + const button = this.elementMenuButton("View Node"); + const isButtonVisible = await waitForElementToBeVisible(button); + if (!isButtonVisible) throw new Error("'View Node' button is not visible!"); + await button.click(); } async getNodeDetailsHeader(): Promise { - await this.elementMenuButton("View Node").click(); - const text = await this.nodedetailsPanelHeader.innerHTML(); - return text; + const isMenuVisible = await waitForElementToBeVisible(this.elementMenu); + if (!isMenuVisible) throw new Error("Element menu did not appear!"); + + await this.clickOnViewNode(); + + const isHeaderVisible = await waitForElementToBeVisible(this.nodedetailsPanelHeader); + if (!isHeaderVisible) throw new Error("Node details panel header did not appear!"); + + return this.nodedetailsPanelHeader.innerHTML(); } async clickOnNodeDetailsCloseBtn(): Promise{ @@ -477,14 +546,17 @@ export default class CodeGraph extends BasePage { } async clickOnCopyToClipboardNodePanelDetails(): Promise { + const isButtonVisible = await waitForElementToBeVisible(this.copyToClipboardNodePanelDetails); + if (!isButtonVisible) throw new Error("'copy to clipboard button is not visible!"); await this.copyToClipboardNodePanelDetails.click(); - await delay(1000) return await this.page.evaluate(() => navigator.clipboard.readText()); } async clickOnCopyToClipboard(): Promise { - await this.elementMenuButton("Copy src to clipboard").click(); - await delay(1000) + const button = this.elementMenuButton("Copy src to clipboard"); + const isVisible = await waitForElementToBeVisible(button); + if (!isVisible) throw new Error("View Node button is not visible!"); + await button.click(); return await this.page.evaluate(() => navigator.clipboard.readText()); } @@ -493,16 +565,22 @@ export default class CodeGraph extends BasePage { } async getNodeDetailsPanelElements(): Promise { - await this.elementMenuButton("View Node").click(); - await delay(500) + const button = this.elementMenuButton("View Node"); + const isVisible = await waitForElementToBeVisible(button); + if (!isVisible) throw new Error("View Node button is not visible!"); + await button.click(); + + const isPanelVisible = await waitForElementToBeVisible(this.nodedetailsPanelElements.first()); + if (!isPanelVisible) throw new Error("Node details panel did not appear!"); + const elements = await this.nodedetailsPanelElements.all(); return Promise.all(elements.map(element => element.innerHTML())); } async getGraphDetails(): Promise { await this.canvasElementBeforeGraphSelection.waitFor({ state: 'detached' }); - await delay(2000) - await this.page.waitForFunction(() => !!window.graph); + await this.page.waitForFunction(() => window.graph && window.graph.elements.nodes.length > 0); + await this.page.waitForTimeout(2000); //canvas animation const graphData = await this.page.evaluate(() => { return window.graph; @@ -512,30 +590,48 @@ export default class CodeGraph extends BasePage { } async transformNodeCoordinates(graphData: any): Promise { - const { canvasLeft, canvasTop, canvasWidth, canvasHeight, transform } = await this.canvasElement.evaluate((canvas: HTMLCanvasElement) => { - const rect = canvas.getBoundingClientRect(); - const ctx = canvas.getContext('2d'); - const transform = ctx?.getTransform()!; - return { - canvasLeft: rect.left, - canvasTop: rect.top, - canvasWidth: rect.width, - canvasHeight: rect.height, - transform, - }; - }); + let maxRetries = 3; + let transform = null; + let canvasRect = null; + await this.page.waitForFunction(() => window.graph && window.graph.elements?.nodes?.length > 0); + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + await this.page.waitForTimeout(1000); + + const result = await this.canvasElement.evaluate((canvas: HTMLCanvasElement) => { + const rect = canvas.getBoundingClientRect(); + const ctx = canvas.getContext('2d'); + return { + canvasLeft: rect.left, + canvasTop: rect.top, + canvasWidth: rect.width, + canvasHeight: rect.height, + transform: ctx?.getTransform() || null, + }; + }); + + if (!result.transform) { + console.warn(`Attempt ${attempt}: Transform not available yet, retrying...`); + continue; + } + + transform = result.transform; + canvasRect = result; + break; + } + + if (!transform) throw new Error("Canvas transform data not available after multiple attempts!"); - const screenCoordinates = graphData.elements.nodes.map((node: any) => { - const adjustedX = node.x * transform.a + transform.e; + return graphData.elements.nodes.map((node: any) => { + const adjustedX = node.x * transform.a + transform.e; const adjustedY = node.y * transform.d + transform.f; - const screenX = canvasLeft + adjustedX - 35; - const screenY = canvasTop + adjustedY - 190; + const screenX = canvasRect!.canvasLeft + adjustedX - 35; + const screenY = canvasRect!.canvasTop + adjustedY - 190; - return {...node, screenX, screenY,}; + return { ...node, screenX, screenY }; }); - - return screenCoordinates; } + async getCanvasScaling(): Promise<{ scaleX: number; scaleY: number }> { const { scaleX, scaleY } = await this.canvasElement.evaluate((canvas: HTMLCanvasElement) => { diff --git a/e2e/logic/utils.ts b/e2e/logic/utils.ts index 6ba3475d..17b575d8 100644 --- a/e2e/logic/utils.ts +++ b/e2e/logic/utils.ts @@ -2,19 +2,50 @@ import { Locator } from "@playwright/test"; export const delay = (ms: number): Promise => new Promise(resolve => setTimeout(resolve, ms)); -export const waitToBeEnabled = async (locator: Locator, timeout: number = 5000): Promise => { - const startTime = Date.now(); +export const waitToBeEnabled = async (locator: Locator, timeout: number = 5000): Promise => { + const elementHandle = await locator.elementHandle(); + if (!elementHandle) throw new Error("Element not found"); - while (Date.now() - startTime < timeout) { - if (await locator.isEnabled()) { - return true; + await locator.page().waitForFunction( + (el) => el && !(el as HTMLElement).hasAttribute("disabled"), + elementHandle, + { timeout } + ); +}; + +export const waitForStableText = async (locator: Locator, timeout: number = 5000): Promise => { + const elementHandle = await locator.elementHandle(); + if (!elementHandle) throw new Error("Element not found"); + + let previousText = ""; + let stableText = ""; + const pollingInterval = 300; + const maxChecks = timeout / pollingInterval; + + for (let i = 0; i < maxChecks; i++) { + stableText = await locator.textContent() ?? ""; + if (stableText === previousText && stableText.trim().length > 0) { + return stableText; } - await new Promise(resolve => setTimeout(resolve, 100)); + previousText = stableText; + await locator.page().waitForTimeout(pollingInterval); } - return false; + return stableText; }; +export const waitForElementToBeVisible = async (locator:Locator,time=400,retry=5):Promise => { + + while(retry > 0){ + if(await locator.isVisible()){ + return true + } + retry = retry-1 + await delay(time) + } + return false +} + export function findNodeByName(nodes: { name: string }[], nodeName: string): any { return nodes.find((node) => node.name === nodeName); } \ No newline at end of file diff --git a/e2e/tests/canvas.spec.ts b/e2e/tests/canvas.spec.ts index 022500e3..e54858d8 100644 --- a/e2e/tests/canvas.spec.ts +++ b/e2e/tests/canvas.spec.ts @@ -100,7 +100,7 @@ test.describe("Canvas tests", () => { test(`Verify "Clear graph" button resets canvas view for path ${path.firstNode} and ${path.secondNode}`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); - await codeGraph.clickOnshowPathBtn(); + await codeGraph.clickOnShowPathBtn(); await codeGraph.insertInputForShowPath("1", path.firstNode); await codeGraph.insertInputForShowPath("2", path.secondNode); const initialGraph = await codeGraph.getGraphDetails(); @@ -185,7 +185,7 @@ test.describe("Canvas tests", () => { test(`Verify successful node path connection in canvas between ${firstNode} and ${secondNode} via UI`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); - await codeGraph.clickOnshowPathBtn(); + await codeGraph.clickOnShowPathBtn(); await codeGraph.insertInputForShowPath("1", firstNode); await codeGraph.insertInputForShowPath("2", secondNode); const result = await codeGraph.getGraphDetails(); @@ -200,7 +200,7 @@ test.describe("Canvas tests", () => { test(`Validate node path connection in canvas ui and confirm via api for path ${path.firstNode} and ${path.secondNode}`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); - await codeGraph.clickOnshowPathBtn(); + await codeGraph.clickOnShowPathBtn(); await codeGraph.insertInputForShowPath("1", path.firstNode); await codeGraph.insertInputForShowPath("2", path.secondNode); const result = await codeGraph.getGraphDetails(); diff --git a/e2e/tests/chat.spec.ts b/e2e/tests/chat.spec.ts index 0d1f71cc..d81ac33d 100644 --- a/e2e/tests/chat.spec.ts +++ b/e2e/tests/chat.spec.ts @@ -31,7 +31,7 @@ test.describe("Chat tests", () => { await chat.selectGraph(GRAPH_ID); const isLoadingArray: boolean[] = []; - for (let i = 0; i < 5; i++) { + for (let i = 0; i < 3; i++) { await chat.sendMessage(Node_Question); const isLoading: boolean = await chat.getpreviousQuestionLoadingImage(); isLoadingArray.push(isLoading); @@ -39,29 +39,59 @@ test.describe("Chat tests", () => { const prevIsLoading = isLoadingArray[i - 1]; expect(prevIsLoading).toBe(false); } + await delay(3000); } }); test("Verify auto-scroll and manual scroll in chat", async () => { const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); await chat.selectGraph(GRAPH_ID); - for (let i = 0; i < 5; i++) { + for (let i = 0; i < 3; i++) { await chat.sendMessage(Node_Question); + await delay(3000); } - await delay(500); + await delay(500); // delay for scroll await chat.scrollToTop(); const { scrollTop } = await chat.getScrollMetrics(); expect(scrollTop).toBeLessThanOrEqual(1); - await chat.sendMessage("Latest Message"); - await delay(500); + await chat.sendMessage(Node_Question); + await delay(500); // delay for scroll expect(await chat.isAtBottom()).toBe(true); }); + test(`Validate consistent UI responses for repeated questions in chat`, async () => { + const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); + await chat.selectGraph(GRAPH_ID); + const responses: string[] = []; + for (let i = 0; i < 3; i++) { + await chat.sendMessage(Node_Question); + const result = await chat.getTextInLastChatElement(); + const number = result.match(/\d+/g)?.[0]!; + responses.push(number); + await delay(3000); //delay before asking next question + } + const identicalResponses = responses.every((value) => value === responses[0]); + expect(identicalResponses).toBe(true); + }); + + test(`Validate UI response matches API response for a given question in chat`, async () => { + const api = new ApiCalls(); + const apiResponse = await api.askQuestion(PROJECT_NAME, Node_Question); + const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); + await chat.selectGraph(GRAPH_ID); + await delay(3000); + await chat.sendMessage(Node_Question); + const uiResponse = await chat.getTextInLastChatElement(); + const number = uiResponse.match(/\d+/g)?.[0]!; + + expect(number).toEqual(apiResponse.result.response.match(/\d+/g)?.[0]); + }); + nodesPath.forEach((path) => { test(`Verify successful node path connection between two nodes in chat for ${path.firstNode} and ${path.secondNode}`, async () => { const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); await chat.selectGraph(GRAPH_ID); - await chat.clickOnshowPathBtn(); + await chat.clickOnShowPathBtn(); await chat.insertInputForShowPath("1", path.firstNode); await chat.insertInputForShowPath("2", path.secondNode); expect(await chat.isNodeVisibleInLastChatPath(path.firstNode)).toBe(true); @@ -73,7 +103,7 @@ test.describe("Chat tests", () => { test(`Verify unsuccessful node path connection between two nodes in chat for ${path.firstNode} and ${path.secondNode}`, async () => { const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); await chat.selectGraph(GRAPH_ID); - await chat.clickOnshowPathBtn(); + await chat.clickOnShowPathBtn(); await chat.insertInputForShowPath("1", path.secondNode); await chat.insertInputForShowPath("2", path.firstNode); await delay(500); @@ -84,7 +114,7 @@ test.describe("Chat tests", () => { test("Validate error notification and its closure when sending an empty question in chat", async () => { const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); await chat.selectGraph(GRAPH_ID); - await chat.clickAskquestionBtn(); + await chat.clickAskQuestionBtn(); expect(await chat.isNotificationError()).toBe(true); await chat.clickOnNotificationErrorCloseBtn(); expect(await chat.isNotificationError()).toBe(false); @@ -98,34 +128,8 @@ test.describe("Chat tests", () => { await chat.clickOnQuestionOptionsMenu(); const selectedQuestion = await chat.selectAndGetQuestionInOptionsMenu(questionNumber.toString()); expect(selectedQuestion).toEqual(await chat.getLastQuestionInChat()) + const result = await chat.getTextInLastChatElement(); + expect(result).toBeDefined(); }); } - - test(`Validate consistent UI responses for repeated questions in chat`, async () => { - const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); - await chat.selectGraph(GRAPH_ID); - const responses: string[] = []; - for (let i = 0; i < 3; i++) { - await chat.sendMessage(Node_Question); - const result = await chat.getTextInLastChatElement(); - const number = result.match(/\d+/g)?.[0]!; - responses.push(number); - - } - const identicalResponses = responses.every((value) => value === responses[0]); - expect(identicalResponses).toBe(true); - }); - - test(`Validate UI response matches API response for a given question in chat`, async () => { - const api = new ApiCalls(); - const apiResponse = await api.askQuestion(PROJECT_NAME, Node_Question); - const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); - await chat.selectGraph(GRAPH_ID); - - await chat.sendMessage(Node_Question); - const uiResponse = await chat.getTextInLastChatElement(); - const number = uiResponse.match(/\d+/g)?.[0]!; - - expect(number).toEqual(apiResponse.result.response.match(/\d+/g)?.[0]); - }); }); diff --git a/e2e/tests/navBar.spec.ts b/e2e/tests/navBar.spec.ts index 9f294a75..f282ca0a 100644 --- a/e2e/tests/navBar.spec.ts +++ b/e2e/tests/navBar.spec.ts @@ -41,9 +41,9 @@ test.describe(' Navbar tests', () => { test("Validate Tip popup visibility and closure functionality", async () => { const navBar = await browser.createNewPage(CodeGraph, urls.baseUrl); - await navBar.clickonTipBtn(); + await navBar.clickOnTipBtn(); expect(await navBar.isTipMenuVisible()).toBe(true); - await navBar.clickonTipMenuCloseBtn(); + await navBar.clickOnTipMenuCloseBtn(); expect(await navBar.isTipMenuVisible()).toBe(false); }); }); diff --git a/e2e/tests/nodeDetailsPanel.spec.ts b/e2e/tests/nodeDetailsPanel.spec.ts index aa59a6fe..3aea588c 100644 --- a/e2e/tests/nodeDetailsPanel.spec.ts +++ b/e2e/tests/nodeDetailsPanel.spec.ts @@ -25,6 +25,7 @@ test.describe("Node details panel tests", () => { const graphData = await codeGraph.getGraphDetails(); const convertCoordinates = await codeGraph.transformNodeCoordinates(graphData); const targetNode = findNodeByName(convertCoordinates, node.nodeName); + expect(targetNode).toBeDefined(); await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); await codeGraph.clickOnViewNode(); expect(await codeGraph.isNodeDetailsPanel()).toBe(true) @@ -78,7 +79,7 @@ test.describe("Node details panel tests", () => { test(`Validate view node panel keys via api for ${node.nodeName}`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); - const graphData = await codeGraph.getGraphDetails(); + const graphData = await codeGraph.getGraphDetails(); const convertCoordinates = await codeGraph.transformNodeCoordinates(graphData); const node1 = findNodeByName(convertCoordinates, node.nodeName); const api = new ApiCalls(); diff --git a/package-lock.json b/package-lock.json index 1a67026a..83462b39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "code-graph", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "code-graph", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2", diff --git a/package.json b/package.json index b9cf75f9..0a789aa3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "code-graph", - "version": "0.1.0", + "version": "0.2.0", "private": true, "scripts": { "dev": "HOST=0.0.0.0 PORT=3000 next dev", diff --git a/playwright.config.ts b/playwright.config.ts index 7a5292d5..8b5b1550 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -16,13 +16,14 @@ export default defineConfig({ /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, + forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 2 : undefined, + workers: process.env.CI ? 3 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', + reporter: [['html', { outputFolder: 'playwright-report' }]], + outputDir: 'playwright-report/artifacts', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ @@ -30,6 +31,7 @@ export default defineConfig({ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', + screenshot: 'only-on-failure', }, /* Configure projects for major browsers */