Skip to content

Commit 367a86d

Browse files
John AzizJohn Aziz
John Aziz
authored and
John Aziz
committed
add playwright tests and workflows
1 parent b67dbc9 commit 367a86d

File tree

2 files changed

+205
-1
lines changed

2 files changed

+205
-1
lines changed

.github/workflows/app-tests.yaml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,16 @@ jobs:
7373
- name: Run MyPy
7474
run: python3 -m mypy .
7575
- name: Run Pytest
76-
run: python3 -m pytest
76+
run: python3 -m pytest -s -vv --cov --cov-fail-under=85
77+
- name: Run E2E tests with Playwright
78+
id: e2e
79+
if: runner.os != 'Windows'
80+
run: |
81+
playwright install chromium --with-deps
82+
python3 -m pytest tests/e2e.py --tracing=retain-on-failure
83+
- name: Upload test artifacts
84+
if: ${{ failure() && steps.e2e.conclusion == 'failure' }}
85+
uses: actions/upload-artifact@v4
86+
with:
87+
name: playwright-traces${{ matrix.python_version }}
88+
path: test-results

tests/e2e.py

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import socket
2+
import time
3+
from collections.abc import Generator
4+
from contextlib import closing
5+
from multiprocessing import Process
6+
7+
import pytest
8+
import requests
9+
import uvicorn
10+
from playwright.sync_api import Page, Route, expect
11+
12+
import fastapi_app as app
13+
14+
expect.set_options(timeout=10_000)
15+
16+
17+
def wait_for_server_ready(url: str, timeout: float = 10.0, check_interval: float = 0.5) -> bool:
18+
"""Make requests to provided url until it responds without error."""
19+
conn_error = None
20+
for _ in range(int(timeout / check_interval)):
21+
try:
22+
requests.get(url)
23+
except requests.ConnectionError as exc:
24+
time.sleep(check_interval)
25+
conn_error = str(exc)
26+
else:
27+
return True
28+
raise RuntimeError(conn_error)
29+
30+
31+
@pytest.fixture(scope="session")
32+
def free_port() -> int:
33+
"""Returns a free port for the test server to bind."""
34+
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
35+
s.bind(("", 0))
36+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
37+
return s.getsockname()[1]
38+
39+
40+
def run_server(port: int):
41+
uvicorn.run(app.create_app(testing=True), port=port)
42+
43+
44+
@pytest.fixture()
45+
def live_server_url(mock_session_env, free_port: int) -> Generator[str, None, None]:
46+
proc = Process(target=run_server, args=(free_port,), daemon=True)
47+
proc.start()
48+
url = f"http://localhost:{free_port}/"
49+
wait_for_server_ready(url, timeout=10.0, check_interval=0.5)
50+
yield url
51+
proc.kill()
52+
53+
54+
@pytest.fixture(params=[(480, 800), (600, 1024), (768, 1024), (992, 1024), (1024, 768)])
55+
def sized_page(page: Page, request):
56+
size = request.param
57+
page.set_viewport_size({"width": size[0], "height": size[1]})
58+
yield page
59+
60+
61+
def test_home(page: Page, live_server_url: str):
62+
page.goto(live_server_url)
63+
expect(page).to_have_title("RAG on PostgreSQL")
64+
65+
66+
def test_chat(sized_page: Page, live_server_url: str):
67+
page = sized_page
68+
69+
# Set up a mock route to the /chat endpoint with streaming results
70+
def handle(route: Route):
71+
# Assert that session_state is specified in the request (None for now)
72+
if route.request.post_data_json:
73+
session_state = route.request.post_data_json["sessionState"]
74+
assert session_state is None
75+
# Read the JSONL from our snapshot results and return as the response
76+
f = open(
77+
"tests/snapshots/test_api_routes/test_advanced_chat_streaming_flow/advanced_chat_streaming_flow_response.jsonlines"
78+
)
79+
jsonl = f.read()
80+
f.close()
81+
route.fulfill(body=jsonl, status=200, headers={"Transfer-encoding": "Chunked"})
82+
83+
page.route("*/**/chat/stream", handle)
84+
85+
# Check initial page state
86+
page.goto(live_server_url)
87+
expect(page).to_have_title("RAG on PostgreSQL")
88+
expect(page.get_by_role("heading", name="Product chat")).to_be_visible()
89+
expect(page.get_by_role("button", name="Clear chat")).to_be_disabled()
90+
expect(page.get_by_role("button", name="Developer settings")).to_be_enabled()
91+
92+
# Ask a question and wait for the message to appear
93+
page.get_by_placeholder("Type a new question (e.g. does my plan cover annual eye exams?)").click()
94+
page.get_by_placeholder("Type a new question (e.g. does my plan cover annual eye exams?)").fill(
95+
"Whats the dental plan?"
96+
)
97+
page.get_by_role("button", name="Ask question button").click()
98+
99+
expect(page.get_by_text("Whats the dental plan?")).to_be_visible()
100+
expect(page.get_by_text("The capital of France is Paris.")).to_be_visible()
101+
expect(page.get_by_role("button", name="Clear chat")).to_be_enabled()
102+
103+
# Show the thought process
104+
page.get_by_label("Show thought process").click()
105+
expect(page.get_by_title("Thought process")).to_be_visible()
106+
expect(page.get_by_text("Prompt to generate search arguments")).to_be_visible()
107+
108+
# Clear the chat
109+
page.get_by_role("button", name="Clear chat").click()
110+
expect(page.get_by_text("Whats the dental plan?")).not_to_be_visible()
111+
expect(page.get_by_text("The capital of France is Paris.")).not_to_be_visible()
112+
expect(page.get_by_role("button", name="Clear chat")).to_be_disabled()
113+
114+
115+
def test_chat_customization(page: Page, live_server_url: str):
116+
# Set up a mock route to the /chat endpoint
117+
def handle(route: Route):
118+
if route.request.post_data_json:
119+
overrides = route.request.post_data_json["context"]["overrides"]
120+
assert overrides["use_advanced_flow"] is False
121+
assert overrides["retrieval_mode"] == "vectors"
122+
assert overrides["top"] == 1
123+
assert overrides["prompt_template"] == "You are a cat and only talk about tuna."
124+
125+
# Read the JSON from our snapshot results and return as the response
126+
f = open("tests/snapshots/test_api_routes/test_simple_chat_flow/simple_chat_flow_response.json")
127+
json = f.read()
128+
f.close()
129+
route.fulfill(body=json, status=200)
130+
131+
page.route("*/**/chat", handle)
132+
133+
# Check initial page state
134+
page.goto(live_server_url)
135+
expect(page).to_have_title("RAG on PostgreSQL")
136+
137+
# Customize all the settings
138+
page.get_by_role("button", name="Developer settings").click()
139+
page.get_by_text(
140+
"Use advanced flow with query rewriting and filter formulation. Not compatible with Ollama models."
141+
).click()
142+
page.get_by_label("Retrieve this many matching rows:").click()
143+
page.get_by_label("Retrieve this many matching rows:").fill("1")
144+
page.get_by_text("Vectors + Text (Hybrid)").click()
145+
page.get_by_role("option", name="Vectors", exact=True).click()
146+
page.get_by_label("Override prompt template").click()
147+
page.get_by_label("Override prompt template").fill("You are a cat and only talk about tuna.")
148+
149+
page.get_by_text("Stream chat completion responses").click()
150+
page.locator("button").filter(has_text="Close").click()
151+
152+
# Ask a question and wait for the message to appear
153+
page.get_by_placeholder("Type a new question (e.g. does my plan cover annual eye exams?)").click()
154+
page.get_by_placeholder("Type a new question (e.g. does my plan cover annual eye exams?)").fill(
155+
"Whats the dental plan?"
156+
)
157+
page.get_by_role("button", name="Ask question button").click()
158+
159+
expect(page.get_by_text("Whats the dental plan?")).to_be_visible()
160+
expect(page.get_by_text("The capital of France is Paris.")).to_be_visible()
161+
expect(page.get_by_role("button", name="Clear chat")).to_be_enabled()
162+
163+
164+
def test_chat_nonstreaming(page: Page, live_server_url: str):
165+
# Set up a mock route to the /chat_stream endpoint
166+
def handle(route: Route):
167+
# Read the JSON from our snapshot results and return as the response
168+
f = open("tests/snapshots/test_api_routes/test_advanced_chat_flow/advanced_chat_flow_response.json")
169+
json = f.read()
170+
f.close()
171+
route.fulfill(body=json, status=200)
172+
173+
page.route("*/**/chat", handle)
174+
175+
# Check initial page state
176+
page.goto(live_server_url)
177+
expect(page).to_have_title("RAG on PostgreSQL")
178+
expect(page.get_by_role("button", name="Developer settings")).to_be_enabled()
179+
page.get_by_role("button", name="Developer settings").click()
180+
page.get_by_text("Stream chat completion responses").click()
181+
page.locator("button").filter(has_text="Close").click()
182+
183+
# Ask a question and wait for the message to appear
184+
page.get_by_placeholder("Type a new question (e.g. does my plan cover annual eye exams?)").click()
185+
page.get_by_placeholder("Type a new question (e.g. does my plan cover annual eye exams?)").fill(
186+
"Whats the dental plan?"
187+
)
188+
page.get_by_label("Ask question button").click()
189+
190+
expect(page.get_by_text("Whats the dental plan?")).to_be_visible()
191+
expect(page.get_by_text("The capital of France is Paris.")).to_be_visible()
192+
expect(page.get_by_role("button", name="Clear chat")).to_be_enabled()

0 commit comments

Comments
 (0)