Skip to content

Commit 5ce70a0

Browse files
authored
Merge pull request #184 from FalkorDB/api-logic-tests
Api logic tests
2 parents 58538cd + 342b836 commit 5ce70a0

File tree

18 files changed

+724
-4
lines changed

18 files changed

+724
-4
lines changed

.github/workflows/playwright.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Playwright Tests
2+
on:
3+
push:
4+
branches: [ main, staging ]
5+
pull_request:
6+
branches: [ main, staging ]
7+
jobs:
8+
test:
9+
timeout-minutes: 60
10+
runs-on: ubuntu-latest
11+
services:
12+
falkordb:
13+
image: falkordb/falkordb:latest
14+
ports:
15+
- 6379:6379
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: actions/setup-node@v4
19+
with:
20+
node-version: lts/*
21+
- name: Install dependencies
22+
run: npm ci
23+
- name: Install Playwright Browsers
24+
run: npx playwright install --with-deps
25+
- name: Set up environment variables and run tests
26+
env:
27+
FALKORDB_URL: ${{ secrets.FALKORDB_URL }}
28+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
29+
SECRET_TOKEN: ${{ secrets.SECRET_TOKEN }}
30+
NEXT_PUBLIC_MODE: UNLIMITED
31+
BACKEND_URL: ${{ secrets.BACKEND_URL }}
32+
run: |
33+
npm install
34+
npm run build
35+
NEXTAUTH_SECRET=SECRET npm start & npx playwright test --reporter=dot,list
36+
- uses: actions/upload-artifact@v4
37+
if: ${{ !cancelled() }}
38+
with:
39+
name: playwright-report
40+
path: playwright-report/
41+
retention-days: 30

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,9 @@ next-env.d.ts
3737

3838

3939
# vscode
40-
/.vscode/
40+
/.vscode/
41+
node_modules/
42+
/test-results/
43+
/playwright-report/
44+
/blob-report/
45+
/playwright/.cache/

app/components/Input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon,
121121
return (
122122
<div
123123
className={cn("w-[20dvw] relative pointer-events-none rounded-md gap-4", parentClassName)}
124+
data-name='search-bar'
124125
>
125126
<input
126127
ref={inputRef}
@@ -163,7 +164,6 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon,
163164
onMouseEnter={() => setSelectedOption(index)}
164165
onMouseLeave={() => setSelectedOption(-1)}
165166
onClick={() => {
166-
debugger
167167
onValueChange({ name: option.properties.name, id: option.id })
168168
handelSubmit && handelSubmit(option)
169169
setOpen(false)

app/components/chat.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, isP
466466

467467
return (
468468
<div className="h-full flex flex-col justify-between px-6 pt-10 pb-4 gap-4">
469-
<main ref={containerRef} className="relative grow flex flex-col overflow-y-auto gap-6 px-4">
469+
<main data-name="main-chat" ref={containerRef} className="relative grow flex flex-col overflow-y-auto gap-6 px-4">
470470
{
471471
messages.length === 0 &&
472472
<>
@@ -495,7 +495,7 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, isP
495495
{
496496
repo &&
497497
<div className="flex gap-4 px-4">
498-
<button disabled={isSendMessage} className="p-4 border rounded-md hover:border-[#FF66B3] hover:bg-[#FFF0F7]" onClick={() => setTipOpen(prev => !prev)}>
498+
<button data-name="lightbulb" disabled={isSendMessage} className="p-4 border rounded-md hover:border-[#FF66B3] hover:bg-[#FFF0F7]" onClick={() => setTipOpen(prev => !prev)}>
499499
<Lightbulb />
500500
</button>
501501
<form className="grow flex items-center border rounded-md pr-2" onSubmit={sendQuery}>

e2e/config/constants.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const GRAPH_ID = "1";
2+
export const PROJECT_NAME = "GraphRAG-SDK";
3+
export const CHAT_OPTTIONS_COUNT = 1;
4+
export const Node_Question = "how many nodes do we have?";
5+
export const Edge_Question = "how many edges do we have?";
6+
export const Node_Import_Data = "import_data";
7+
export const Node_Add_Edge = "add_edge";

e2e/config/urls.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"baseUrl": "http://localhost:3000/",
3+
"graphRAGuRL": "https://github.com/FalkorDB/GraphRAG-SDK",
4+
"falkorDBUrl": "https://www.falkordb.com/",
5+
"falkorDbGithubUrl": "https://github.com/FalkorDB/code-graph"
6+
}

e2e/infra/api/apiRequests.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { APIRequestContext, request } from "@playwright/test"
2+
3+
4+
const getRequest = async (url: string, body?: any, availableRequest?: APIRequestContext, headers?: Record<string, string>) => {
5+
const requestOptions = {
6+
data: body,
7+
headers: headers || undefined,
8+
};
9+
10+
const requestContext = availableRequest || (await request.newContext());
11+
return await requestContext.get(url, requestOptions);
12+
};
13+
14+
const postRequest = async (url: string, body?: any, availableRequest?: APIRequestContext, headers?: Record<string, string>) => {
15+
const requestOptions = {
16+
data: body,
17+
headers: headers || undefined,
18+
};
19+
20+
const requestContext = availableRequest || (await request.newContext());
21+
return await requestContext.post(url, requestOptions);
22+
};
23+
24+
export{ getRequest, postRequest }

e2e/infra/ui/basePage.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Page } from 'playwright';
2+
3+
export default class BasePage {
4+
protected page: Page;
5+
6+
constructor(page: Page) {
7+
this.page = page;
8+
}
9+
10+
async initPage(){
11+
await this.page.waitForLoadState()
12+
}
13+
14+
getCurrentURL() : string {
15+
return this.page.url();
16+
}
17+
18+
async refreshPage(){
19+
await this.page.reload({ waitUntil: 'networkidle' });
20+
}
21+
22+
}

e2e/infra/ui/browserWrapper.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { chromium, Browser, BrowserContext, Page } from 'playwright';
2+
import BasePage from './basePage';
3+
4+
export default class BrowserWrapper {
5+
6+
private browser: Browser | null = null;
7+
8+
private context: BrowserContext | null = null;
9+
10+
private page: Page | null = null;
11+
12+
async createNewPage<T extends BasePage>(pageClass: new (page: Page) => T, url?: string) {
13+
if (!this.browser) {
14+
this.browser = await chromium.launch();
15+
}
16+
if (!this.context) {
17+
this.context = await this.browser.newContext();
18+
}
19+
if (!this.page) {
20+
this.page = await this.context.newPage();
21+
}
22+
if (url) {
23+
await this.navigateTo(url)
24+
}
25+
26+
const pageInstance = new pageClass(this.page);
27+
return pageInstance;
28+
}
29+
30+
async getPage() {
31+
if (!this.page) {
32+
throw new Error('Browser is not launched yet!');
33+
}
34+
return this.page;
35+
}
36+
37+
async setPageToFullScreen() {
38+
if (!this.page) {
39+
throw new Error('Browser is not launched yet!');
40+
}
41+
await this.page.setViewportSize({ width: 1920, height: 1080 });
42+
}
43+
44+
async navigateTo(url: string) {
45+
if (!this.page) {
46+
throw new Error('Browser is not launched yet!');
47+
}
48+
await this.page.goto(url);
49+
await this.page.waitForLoadState('networkidle');
50+
}
51+
52+
async closePage() {
53+
this.page ? await this.page.close() : this.page = null;
54+
}
55+
56+
async closeBrowser() {
57+
if (this.browser) {
58+
await this.browser.close();
59+
}
60+
}
61+
62+
}

e2e/logic/POM/codeGraph.ts

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { Locator, Page } from "playwright";
2+
import BasePage from "../../infra/ui/basePage";
3+
import { delay } from "../utils";
4+
5+
export default class CodeGraph extends BasePage {
6+
/* NavBar Locators*/
7+
private get falkorDBLogo(): Locator {
8+
return this.page.locator("//*[img[@alt='FalkorDB']]")
9+
}
10+
11+
private get navBaritem(): (navItem: string) => Locator {
12+
return (navItem: string) => this.page.locator(`//a[p[text() = '${navItem}']]`);
13+
}
14+
15+
private get createNewProjectBtn(): Locator {
16+
return this.page.getByRole('button', { name: 'Create new project' });
17+
}
18+
19+
private get createNewProjectDialog(): Locator {
20+
return this.page.locator("//div[@role='dialog']")
21+
}
22+
23+
/* CodeGraph Locators*/
24+
private get comboBoxbtn(): Locator {
25+
return this.page.locator("//button[@role='combobox']")
26+
}
27+
28+
private get selectGraphInComboBox(): (graph: string) => Locator {
29+
return (graph: string) => this.page.locator(`//div[@role='presentation']//div[@role='option'][${graph}]`);
30+
}
31+
32+
private get lastElementInChat(): Locator {
33+
return this.page.locator("//main[@data-name='main-chat']/*[last()]/p");
34+
}
35+
36+
/* Chat Locators */
37+
private get showPathBtn(): Locator {
38+
return this.page.locator("//button[contains(@class, 'Tip')]");
39+
}
40+
41+
private get askquestionInput(): Locator {
42+
return this.page.locator("//input[contains(@placeholder, 'Ask your question')]");
43+
}
44+
45+
private get askquestionBtn(): Locator {
46+
return this.page.locator("//input[contains(@placeholder, 'Ask your question')]/following::button[1]");
47+
}
48+
49+
private get lightbulbBtn(): Locator {
50+
return this.page.locator("//button[@data-name='lightbulb']");
51+
}
52+
53+
private get lastChatElementButtonCount(): Locator {
54+
return this.page.locator("//main[@data-name='main-chat']/*[last()]/button");
55+
}
56+
57+
private get chatContainer(): Locator {
58+
return this.page.locator("//main[@data-name='main-chat']");
59+
}
60+
61+
private get previousQuestionLoadingImage(): Locator {
62+
return this.page.locator("//main[@data-name='main-chat']/*[last()-2]//img[@alt='Waiting for response']")
63+
}
64+
65+
private get selectInputForShowPath(): (inputNum: string) => Locator {
66+
return (inputNum: string) => this.page.locator(`(//main[@data-name='main-chat']//input)[${inputNum}]`);
67+
}
68+
69+
private get locateNodeInLastChatPath(): (node: string) => Locator {
70+
return (node: string) => this.page.locator(`//main[@data-name='main-chat']/*[last()]//span[contains(text(), '${node}')]`);
71+
}
72+
73+
private get selectFirstPathOption(): (inputNum: string) => Locator {
74+
return (inputNum: string) => this.page.locator(`(//main[@data-name='main-chat']//input)[1]/following::div[${inputNum}]//button[1]`);
75+
}
76+
77+
private get notificationNoPathFound(): Locator {
78+
return this.page.locator("//div[@role='region']//ol//li");
79+
}
80+
81+
/* NavBar functionality */
82+
async clickOnFalkorDbLogo(): Promise<Page> {
83+
await this.page.waitForLoadState('networkidle');
84+
const [newPage] = await Promise.all([
85+
this.page.waitForEvent('popup'),
86+
this.falkorDBLogo.click(),
87+
]);
88+
return newPage
89+
}
90+
91+
async getNavBarItem(navItem : string): Promise<Page> {
92+
await this.page.waitForLoadState('networkidle');
93+
const [newPage] = await Promise.all([
94+
this.page.waitForEvent('popup'),
95+
this.navBaritem(navItem).click(),
96+
]);
97+
return newPage
98+
}
99+
100+
async clickCreateNewProjectBtn(): Promise<void> {
101+
await this.createNewProjectBtn.click();
102+
}
103+
104+
async isCreateNewProjectDialog(): Promise<boolean> {
105+
return await this.createNewProjectDialog.isVisible();
106+
}
107+
108+
/* Chat functionality */
109+
async clickOnshowPathBtn(): Promise<void> {
110+
await this.showPathBtn.click();
111+
}
112+
113+
async clickAskquestionBtn(): Promise<void> {
114+
await this.askquestionBtn.click();
115+
}
116+
117+
async sendMessage(message: string) {
118+
await this.askquestionInput.isEnabled();
119+
await this.askquestionInput.fill(message);
120+
await this.askquestionBtn.click();
121+
}
122+
123+
async clickOnLightBulbBtn(): Promise<void> {
124+
await this.lightbulbBtn.click();
125+
}
126+
127+
async getTextInLastChatElement(): Promise<string | null>{
128+
return await this.lastElementInChat.textContent();
129+
}
130+
131+
async getLastChatElementButtonCount(): Promise<number | null>{
132+
return await this.lastChatElementButtonCount.count();
133+
}
134+
135+
async scrollToTop() {
136+
await this.chatContainer.evaluate((chat) => {
137+
chat.scrollTop = 0;
138+
});
139+
}
140+
141+
async getScrollMetrics() {
142+
const scrollTop = await this.chatContainer.evaluate((el) => el.scrollTop);
143+
const scrollHeight = await this.chatContainer.evaluate((el) => el.scrollHeight);
144+
const clientHeight = await this.chatContainer.evaluate((el) => el.clientHeight);
145+
return { scrollTop, scrollHeight, clientHeight };
146+
}
147+
148+
async isAtBottom(): Promise<boolean> {
149+
const { scrollTop, scrollHeight, clientHeight } = await this.getScrollMetrics();
150+
return Math.abs(scrollTop + clientHeight - scrollHeight) < 1;
151+
}
152+
153+
async getpreviousQuestionLoadingImage(): Promise<boolean> {
154+
return this.previousQuestionLoadingImage.isVisible();
155+
}
156+
157+
async insertInputForShowPath(inputNum: string, node: string): Promise<void> {
158+
await this.selectInputForShowPath(inputNum).fill(node);
159+
await this.selectFirstPathOption(inputNum).click();
160+
}
161+
162+
async isNodeVisibleInLastChatPath(node: string): Promise<boolean> {
163+
return await this.locateNodeInLastChatPath(node).isVisible();
164+
}
165+
166+
async isNotificationNoPathFound(): Promise<boolean> {
167+
return await this.notificationNoPathFound.isVisible();
168+
}
169+
170+
/* CodeGraph functionality */
171+
async selectGraph(graph: string): Promise<void> {
172+
await this.comboBoxbtn.click();
173+
await this.selectGraphInComboBox(graph).waitFor({ state : 'visible'})
174+
await this.selectGraphInComboBox(graph).click();
175+
}
176+
}

0 commit comments

Comments
 (0)