Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI tests with Playwright #413

Merged
merged 41 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
7c58c7f
wip
blakerosenthal May 8, 2024
e36f4b4
working health page
blakerosenthal May 12, 2024
45a7f33
working api server
blakerosenthal May 13, 2024
b83aafe
fix headless mode
blakerosenthal May 13, 2024
463f564
fix timeout logic
blakerosenthal May 13, 2024
c6f2153
add more tests, auth header not working
blakerosenthal May 14, 2024
c5f3eee
assign text ports and skip failing auth test
blakerosenthal May 14, 2024
f80cadb
Merge branch 'main' into ui-tests
blakerosenthal May 14, 2024
c9ccca8
placeholder test for ui chat
blakerosenthal May 14, 2024
1a6f840
install playwright to gh env
blakerosenthal May 14, 2024
1dc7548
make an api wrapper to try to get around the pickling issue on mac/wi…
blakerosenthal May 14, 2024
1c81128
always install playwright
blakerosenthal May 14, 2024
cd68b85
some PR review fixes
blakerosenthal May 16, 2024
fc61dbe
move shared stuff to utils
blakerosenthal May 16, 2024
6dc91eb
wip
blakerosenthal May 22, 2024
d9f19cb
wip: replace server with cli function
blakerosenthal May 22, 2024
b7e59a5
remove breakpoint
blakerosenthal May 22, 2024
d62bf5e
increase timeout
pmeier May 23, 2024
e6ab71b
server mods
blakerosenthal May 24, 2024
382d71b
use playwright Page directly
blakerosenthal May 24, 2024
b378a51
check to make sure document is in database
blakerosenthal May 24, 2024
53b4c4f
increase timeout
blakerosenthal May 24, 2024
0d80801
increase timeout again
blakerosenthal May 28, 2024
aae2f22
use consistent ports
blakerosenthal May 28, 2024
36e6f43
try with slowmo
blakerosenthal May 30, 2024
050dc72
reorder element expectations; remove slowmo
blakerosenthal May 30, 2024
a983307
hack
blakerosenthal May 30, 2024
f49bdd8
upload playwright video on failing tests
blakerosenthal May 31, 2024
648823d
syntax fix
blakerosenthal May 31, 2024
7038f30
run upload on failure
blakerosenthal May 31, 2024
855c0d3
get tests to pass again
blakerosenthal May 31, 2024
7672c0c
Merge branch 'main' into ui-tests
blakerosenthal Jun 7, 2024
50c0ae1
separate ui and non-ui tests
blakerosenthal Jun 7, 2024
4dade55
avoid relative imports; better func name
blakerosenthal Jun 7, 2024
162e982
use function scope for test ports
blakerosenthal Jun 7, 2024
8c82a4d
review nits
blakerosenthal Jun 7, 2024
dcd4339
make Server a context manager
blakerosenthal Jun 7, 2024
c8ba9dc
try to fix CI matrix
blakerosenthal Jun 7, 2024
d2dfcce
attempt #2
blakerosenthal Jun 7, 2024
8a60430
nits
pmeier Jun 10, 2024
37e9812
remove unused func
blakerosenthal Jun 10, 2024
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
4 changes: 4 additions & 0 deletions .github/actions/setup-env/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ runs:
shell: bash -el {0}
run: mamba install --yes --channel conda-forge redis-server

- name: Install playwright
shell: bash -el {0}
run: playwright install

- name: Install ragna
shell: bash -el {0}
run: |
Expand Down
68 changes: 67 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,76 @@ jobs:

- name: Run unit tests
id: tests
run: pytest --junit-xml=test-results.xml --durations=25
run: |
blakerosenthal marked this conversation as resolved.
Show resolved Hide resolved
pytest \
--ignore tests/deploy/ui \
--junit-xml=test-results.xml \
--durations=25 \
pmeier marked this conversation as resolved.
Show resolved Hide resolved

- name: Surface failing tests
if: steps.tests.outcome != 'success'
uses: pmeier/pytest-results-action@v0.3.0
with:
path: test-results.xml

pytest-ui:
strategy:
matrix:
os:
- ubuntu-latest
- windows-latest
- macos-latest
browser:
- chromium
- firefox
python-version:
- "3.9"
- "3.10"
- "3.11"
exclude:
- python-version: "3.10"
os: windows-latest
- python-version: "3.11"
os: windows-latest
- python-version: "3.10"
os: macos-latest
- python-version: "3.11"
os: macos-latest
include:
- browser: webkit
os: macos-latest
python-version: "3.9"
blakerosenthal marked this conversation as resolved.
Show resolved Hide resolved

fail-fast: false

runs-on: ${{ matrix.os }}

defaults:
run:
shell: bash -el {0}

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup environment
uses: ./.github/actions/setup-env
with:
python-version: ${{ matrix.python-version }}

- name: Run unit tests
id: tests
run: |
pytest tests/deploy/ui \
--browser ${{ matrix.browser }} \
--video=retain-on-failure

- name: Upload playwright video
if: failure()
uses: actions/upload-artifact@v4
with:
name:
playwright-${{ matrix.os }}-${{ matrix.python-version}}-${{ github.run_id }}
path: test-results
1 change: 1 addition & 0 deletions environment-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies:
- pytest >=6
- pytest-mock
- pytest-asyncio
- pytest-playwright
- mypy ==1.10.0
- pre-commit
- types-aiofiles
Expand Down
5 changes: 2 additions & 3 deletions tests/deploy/api/test_batch_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@

from ragna.deploy import Config
from ragna.deploy._api import app

from .utils import authenticate
from tests.deploy.utils import authenticate_with_api


def test_batch_sequential_upload_equivalence(tmp_local_root):
Expand All @@ -23,7 +22,7 @@ def test_batch_sequential_upload_equivalence(tmp_local_root):
with TestClient(
app(config=Config(), ignore_unavailable_components=False)
) as client:
authenticate(client)
authenticate_with_api(client)

document1_upload = (
client.post("/document", json={"name": document_path1.name})
Expand Down
7 changes: 3 additions & 4 deletions tests/deploy/api/test_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
from ragna.core import RagnaException
from ragna.deploy import Config
from ragna.deploy._api import app

from .utils import authenticate
from tests.deploy.utils import authenticate_with_api


@pytest.mark.parametrize("ignore_unavailable_components", [True, False])
Expand All @@ -27,7 +26,7 @@ def test_ignore_unavailable_components(ignore_unavailable_components):
ignore_unavailable_components=ignore_unavailable_components,
)
) as client:
authenticate(client)
authenticate_with_api(client)

components = client.get("/components").raise_for_status().json()
assert [assistant["title"] for assistant in components["assistants"]] == [
Expand Down Expand Up @@ -66,7 +65,7 @@ def test_unknown_component(tmp_local_root):
with TestClient(
app(config=Config(), ignore_unavailable_components=False)
) as client:
authenticate(client)
authenticate_with_api(client)

document_upload = (
client.post("/document", json={"name": document_path.name})
Expand Down
22 changes: 2 additions & 20 deletions tests/deploy/api/test_e2e.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,11 @@
import json
import time

import pytest
from fastapi.testclient import TestClient

from ragna.assistants import RagnaDemoAssistant
from ragna.deploy import Config
from ragna.deploy._api import app

from .utils import authenticate


class TestAssistant(RagnaDemoAssistant):
def answer(self, prompt, sources, *, multiple_answer_chunks: bool):
# Simulate a "real" assistant through a small delay. See
# https://github.com/Quansight/ragna/pull/401#issuecomment-2095851440
# for why this is needed.
time.sleep(1e-3)
content = next(super().answer(prompt, sources))

if multiple_answer_chunks:
for chunk in content.split(" "):
yield f"{chunk} "
else:
yield content
from tests.deploy.utils import TestAssistant, authenticate_with_api


@pytest.mark.parametrize("multiple_answer_chunks", [True, False])
Expand All @@ -38,7 +20,7 @@ def test_e2e(tmp_local_root, multiple_answer_chunks, stream_answer):
file.write("!\n")

with TestClient(app(config=config, ignore_unavailable_components=False)) as client:
authenticate(client)
authenticate_with_api(client)

assert client.get("/chats").raise_for_status().json() == []

Expand Down
23 changes: 0 additions & 23 deletions tests/deploy/api/utils.py

This file was deleted.

172 changes: 172 additions & 0 deletions tests/deploy/ui/test_ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import socket
import subprocess
import sys
import time

import httpx
import panel as pn
import pytest
from playwright.sync_api import Page, expect

from ragna._utils import timeout_after
from ragna.deploy import Config
from tests.deploy.utils import TestAssistant


def get_available_port():
with socket.socket() as s:
s.bind(("", 0))
return s.getsockname()[1]


@pytest.fixture
def api_port():
return get_available_port()


@pytest.fixture
def ui_port():
return get_available_port()
pmeier marked this conversation as resolved.
Show resolved Hide resolved


@pytest.fixture(scope="session")
def headed_mode(pytestconfig):
return pytestconfig.getoption("headed") or False
blakerosenthal marked this conversation as resolved.
Show resolved Hide resolved


@pytest.fixture
def config(
tmp_local_root,
):
config = Config(
local_root=tmp_local_root,
assistants=[TestAssistant],
ui=dict(port=get_available_port()),
api=dict(port=get_available_port()),
)
path = tmp_local_root / "ragna.toml"
config.to_file(path)
return config


class Server:
def __init__(self, config):
self.config = config
self.base_url = f"http://{config.ui.hostname}:{config.ui.port}"

def server_up(self):
try:
return httpx.get(self.base_url).is_success
except httpx.ConnectError:
return False

@timeout_after(60)
def start(self):
self.proc = subprocess.Popen(
[
sys.executable,
"-m",
"ragna",
"ui",
"--config",
self.config.local_root / "ragna.toml",
"--start-api",
"--ignore-unavailable-components",
"--no-open-browser",
],
stdout=sys.stdout,
stderr=sys.stderr,
)

while not self.server_up():
time.sleep(1)

def stop(self):
self.proc.kill()
pn.state.kill_all_servers()

def __enter__(self):
self.start()
return self

def __exit__(self, *args):
self.stop()


def test_health(config, page: Page) -> None:
with Server(config) as server:
health_url = f"{server.base_url}/health"
response = page.goto(health_url)
assert response.ok


def test_start_chat(config, page: Page) -> None:
with Server(config) as server:
# Index page, no auth
index_url = server.base_url
page.goto(index_url)
expect(page.get_by_role("button", name="Sign In")).to_be_visible()

# Authorize with no credentials
page.get_by_role("button", name="Sign In").click()
expect(page.get_by_role("button", name=" New Chat")).to_be_visible()

# expect auth token to be set
cookies = page.context.cookies()
assert len(cookies) == 1
cookie = cookies[0]
assert cookie.get("name") == "auth_token"
auth_token = cookie.get("value")
assert auth_token is not None

# New page button
new_chat_button = page.get_by_role("button", name=" New Chat")
expect(new_chat_button).to_be_visible()
new_chat_button.click()

document_root = config.local_root / "documents"
document_root.mkdir()
document_name = "test.txt"
document_path = document_root / document_name
with open(document_path, "w") as file:
file.write("!\n")

# File upload selector
with page.expect_file_chooser() as fc_info:
page.locator(".fileUpload").click()
file_chooser = fc_info.value
file_chooser.set_files(document_path)

# Upload document and expect to see it listed
file_list = page.locator(".fileListContainer")
expect(file_list.first).to_have_text(str(document_name))

chat_dialog = page.get_by_role("dialog")
expect(chat_dialog).to_be_visible()
start_chat_button = page.get_by_role("button", name="Start Conversation")
expect(start_chat_button).to_be_visible()
time.sleep(0.5) # hack while waiting for button to be fully clickable
start_chat_button.click(delay=5)

chat_box_row = page.locator(".chat-interface-input-row")
expect(chat_box_row).to_be_visible()

chat_box = chat_box_row.get_by_role("textbox")
expect(chat_box).to_be_visible()

# Document should be in the database
chats_url = f"http://{config.api.hostname}:{config.api.port}/chats"
chats = httpx.get(
chats_url, headers={"Authorization": f"Bearer {auth_token}"}
).json()
assert len(chats) == 1
chat = chats[0]
chat_documents = chat["metadata"]["documents"]
assert len(chat_documents) == 1
assert chat_documents[0]["name"] == document_name

chat_box.fill("Tell me about the documents")

chat_button = chat_box_row.get_by_role("button")
expect(chat_button).to_be_visible()
chat_button.click()
Loading