Skip to content

Commit

Permalink
Add rudimentary frontend tests for all functionalities
Browse files Browse the repository at this point in the history
Implements requested tests in issue #159.

Backend and frontend tests now use their own dedicated aiida profile to avoid
changes in the aiida database that might affect the tests. For that they have
been moved to their own module folder.

Frontend tests reuse ran aiida workgraph calculation to save time.

CI tests have been splitted into three runs of pytest (see issue #225)
  • Loading branch information
agoscinski committed Aug 14, 2024
1 parent fce96c4 commit a067792
Show file tree
Hide file tree
Showing 7 changed files with 286 additions and 106 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,10 @@ jobs:
env:
AIIDA_WARN_v3: 1
run: |
pytest -v tests --cov --durations=0
# Have to split tests into see issue #225
pytest -m "not frontend and not backend" -v --cov --durations=0
pytest -m backend -v --cov-append --durations=0
pytest -m frontend -v --cov-append --durations=0
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4.0.1
Expand Down
14 changes: 12 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,19 @@ npm --prefix aiida_workgraph/web/frontend start

The frontend server will refresh

### Troubleshooting
#### Tools for writing frontend tests

#### Tests are not updating after changes in code
To determine the right commands for invoking DOM elements playwright offers a
tool that outputs commands while navigating through the GUI. It requires a
webserver to be running so it can be started with
```console
workgraph web start
playwright codegen
```

#### Troubleshooting

##### Tests are not updating after changes in code

You might want to clean your cache

Expand Down
21 changes: 21 additions & 0 deletions tests/web/backend/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import pytest
from fastapi.testclient import TestClient
import os

@pytest.fixture(scope="module", autouse=True)
def aiida_profile(aiida_config, aiida_profile_factory):
"""Create and load a profile with RabbitMQ as broker for backend tests."""
with aiida_profile_factory(aiida_config, broker_backend="core.rabbitmq") as profile:
yield profile


@pytest.fixture(scope="module")
def set_backend_server_settings(aiida_profile):
os.environ["AIIDA_WORKGRAPH_GUI_PROFILE"] = aiida_profile.name


@pytest.fixture(scope="module")
def client(set_backend_server_settings):
from aiida_workgraph.web.backend.app.api import app

return TestClient(app)
20 changes: 2 additions & 18 deletions tests/web/test_backend.py → tests/web/backend/test_backend.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,17 @@
import pytest
from fastapi.testclient import TestClient

##############################
# Fixtures for backend tests #
##############################


@pytest.fixture(scope="module")
def client(set_backend_server_settings):
from aiida_workgraph.web.backend.app.api import app

return TestClient(app)


#################
# Backend tests #
#################

# Sample test case for the root route
@pytest.mark.backend
def test_root_route(client):
"""Sample test case for the root route"""
response = client.get("/api")
assert response.status_code == 200
assert response.json() == {"message": "Welcome to AiiDA-WorkGraph."}


# Sample test case for the root route
@pytest.mark.backend
def test_workgraph_route(client, wg_calcfunction):
"""Sample test case for the root route"""
wg_calcfunction.run()
response = client.get("/api/workgraph-data")
assert response.status_code == 200
Expand Down
8 changes: 0 additions & 8 deletions tests/web/conftest.py

This file was deleted.

116 changes: 39 additions & 77 deletions tests/web/test_frontend.py → tests/web/frontend/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import pytest
import os
from typing import Generator

from playwright.sync_api import sync_playwright
from playwright.sync_api import expect

from aiida_workgraph import WorkGraph

import uvicorn

Expand All @@ -13,6 +18,7 @@
import socket
import errno


################################
# Utilities for frontend tests #
################################
Expand Down Expand Up @@ -49,7 +55,7 @@ def run_uvicorn_web_server(
with uvicorn_web_server.run_in_thread():
with web_server_started.get_lock():
web_server_started.value = 1
print("wait for stop_web_server")
print("Wait for signal to stop web server.")
while not stop_web_server.value:
time.sleep(1e-3)

Expand All @@ -59,6 +65,32 @@ def run_uvicorn_web_server(
###############################


@pytest.fixture(scope="module")
def aiida_profile(aiida_config, aiida_profile_factory):
"""Create and load a profile with RabbitMQ as broker for frontend tests."""
with aiida_profile_factory(aiida_config, broker_backend="core.rabbitmq") as profile:
yield profile


@pytest.fixture(scope="module")
def set_backend_server_settings(aiida_profile):
os.environ["AIIDA_WORKGRAPH_GUI_PROFILE"] = aiida_profile.name


@pytest.fixture(scope="module")
def ran_wg_calcfunction(
aiida_profile,
) -> Generator[WorkGraph, None, None]:
"""A workgraph with calcfunction."""

wg = WorkGraph(name="test_debug_math")
sumdiff1 = wg.add_task("workgraph.test_sum_diff", "sumdiff1", x=2, y=3)
sumdiff2 = wg.add_task("workgraph.test_sum_diff", "sumdiff2", x=4)
wg.add_link(sumdiff1.outputs[0], sumdiff2.inputs[1])
wg.run()
yield wg


@pytest.fixture(scope="module")
def uvicorn_configuration():
return {
Expand Down Expand Up @@ -100,10 +132,11 @@ def web_server(set_backend_server_settings, uvicorn_configuration):

web_server_proc.start()

print("wait for sevrer started")
print("Wait for server being started.")
while not web_server_started.value:
time.sleep(1e-3)

print("Web server started.")
yield web_server_proc

with stop_web_server.get_lock():
Expand Down Expand Up @@ -137,80 +170,9 @@ def browser():
@pytest.fixture(scope="module")
def page(browser):
with browser.new_page() as page:
# 5 seconds
page.set_default_timeout(5000)
page.set_default_navigation_timeout(5000)
expect.set_options(timeout=5_000)
yield page
page.close()


##################
# Frontend tests #
##################


@pytest.mark.frontend
def test_homepage(web_server, page):
page.goto("http://localhost:8000")

assert page.title() == "AiiDA-WorkGraph App"

# Check for the existence of a specific element on the page
# Attempt to locate the element
element = page.locator("a[href='/workgraph']")

# Check if the element is found
if not element.is_visible():
pytest.fail("Element 'a[href='/wortre']' not found on the page")


@pytest.mark.frontend
def test_workgraph(web_server, page, aiida_profile, wg_calcfunction):
wg_calcfunction.run()

page.goto("http://localhost:8000")
# Since the routing is done by react-router-dom we cannot access it with a call like this
# page.goto("http://localhost:8000/workgraph" but have to navigate to it
page.click('a[href="/workgraph"]')

# Check for the existence of a specific element on the page

# Verify the presence of the WorkGraphTable heading
assert page.locator("h2").inner_text() == "WorkGraph"

# Verify the presence of the search input
assert page.locator(".search-input").is_visible()

# Verify the presence of the table header columns
# Verify the presence of the table header columns
assert page.locator("th:has-text('PK')").is_visible()
assert page.locator("th:has-text('Created')").is_visible()
assert page.locator("th:has-text('Process Label')").is_visible()
assert page.locator("th:has-text('State')").is_visible()
assert page.locator("th:has-text('Actions')").is_visible()

# Verify the presence of pagination controls
assert page.locator(".pagination").is_visible()

# Verify the presence of at least one row in the table
page.wait_for_timeout(8000)
assert page.locator("tr").count() >= 1 # Including header row


@pytest.mark.frontend
def test_workgraph_item(page, wg_calcfunction):
wg = wg_calcfunction
wg.run()
page.goto("http://localhost:8000/workgraph/{}".format(wg.pk))
page.wait_for_timeout(8000)

page.get_by_text("sumdiff3").is_visible()

# Simulate user interaction (e.g., clicking a button)
# Replace the selector with the actual selector of the button you want to click
# You should identify the button that triggers an action in your component
page.get_by_role("button", name="Arrange").click()
page.wait_for_timeout(8000)
# Capture a screenshot
screenshot = page.screenshot()

# Save the screenshot to a file
with open("screenshot.png", "wb") as f:
f.write(screenshot)
Loading

0 comments on commit a067792

Please sign in to comment.