aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/playwright/conftest.py48
-rw-r--r--tests/playwright/test_basic.py184
2 files changed, 232 insertions, 0 deletions
diff --git a/tests/playwright/conftest.py b/tests/playwright/conftest.py
new file mode 100644
index 0000000..dedaf22
--- /dev/null
+++ b/tests/playwright/conftest.py
@@ -0,0 +1,48 @@
+import socket
+import threading
+from wsgiref import simple_server
+
+import pytest
+
+
+@pytest.fixture(scope="session")
+def server_port():
+ """Return a (likely) free port.
+
+ Note that due to OS race conditions between picking the port and opening
+ something on it, it might be taken again, but that is unlikely.
+ """
+ sock = socket.socket(socket.AF_INET)
+ sock.bind(("", 0))
+ port = sock.getsockname()[1]
+ sock.close()
+ return port
+
+
+@pytest.fixture(scope="session", autouse=True)
+def running_server(server_port, app):
+ """Have the app running as an actual server."""
+ server = simple_server.make_server("127.0.0.1", server_port, app)
+ thread = threading.Thread(target=server.serve_forever)
+ thread.daemon = True
+ thread.start()
+ yield
+ server.shutdown()
+
+
+@pytest.fixture
+def browser_context_args(server_port) -> dict:
+ return {"base_url": f"http://localhost:{server_port}"}
+
+
+@pytest.fixture
+def dbaccess(app):
+ """Provide direct access to the database.
+
+ This is needed for the selenium tests, because the normal "dbsession"
+ fixture has a doomed transaction attached. This is nice for keeping the
+ test database clean, but it does mean that the changes are not propagated
+ through and the running WSGI app cannot read them.
+ """
+ session_factory = app.registry["dbsession_factory"]
+ return session_factory()
diff --git a/tests/playwright/test_basic.py b/tests/playwright/test_basic.py
new file mode 100644
index 0000000..b3e340d
--- /dev/null
+++ b/tests/playwright/test_basic.py
@@ -0,0 +1,184 @@
+import datetime
+
+import pytest
+from pyramid.authentication import AuthTktCookieHelper
+from pyramid.testing import DummyRequest
+from playwright.sync_api import Page, expect
+from sqlalchemy import select
+
+from testutils import load_gpx_asset
+from fietsboek import models
+from fietsboek.models.track import Visibility, TrackType
+from fietsboek.config import Config
+
+
+@pytest.fixture
+def john_doe(dbaccess):
+ """Provides a test user (John Doe).
+
+ This fixture either returns the existing John or creates a new one.
+ """
+ query = models.User.query_by_email("john@doe.com")
+ result = dbaccess.execute(query).scalar_one_or_none()
+ if result:
+ return result
+ with dbaccess:
+ user = models.User(name="John Doe", email="john@doe.com", is_verified=True)
+ user.set_password("password")
+ dbaccess.add(user)
+ dbaccess.commit()
+ return user
+
+
+def do_login(settings: dict, page: Page, user: models.User):
+ """Logs the given user in by setting the auth cookie."""
+ config = Config.construct(session_key=settings["session_key"])
+ secret = config.derive_secret("auth-cookie")
+ helper = AuthTktCookieHelper(secret)
+ headers = helper.remember(DummyRequest(), str(user.id))
+ for _, header_val in headers:
+ cookie = header_val.split(";")[0]
+ name, value = cookie.split("=", 1)
+ page.context.add_cookies([
+ {"name": name, "value": value, "domain": "localhost", "path": "/"},
+ ])
+
+
+def test_homepage(page: Page):
+ page.goto("/")
+ assert "Welcome to Fietsboek!" in page.content()
+ assert "Here you can …" in page.content()
+
+
+def test_login_failed(page: Page):
+ page.goto("/")
+ page.get_by_role("button", name="User").click()
+ page.get_by_text("Login").click()
+
+ page.get_by_label("E-Mail").fill("not-john@doe.com")
+ page.get_by_label("Password").fill("password")
+ page.get_by_role("button", name="Login").click()
+
+ expect(page.locator(".alert")).to_have_text("Invalid login credentials")
+
+
+def test_login_success(page: Page, john_doe):
+
+ page.goto("/")
+ page.get_by_role("button", name="User").click()
+ page.get_by_text("Login").click()
+
+ page.get_by_label("E-Mail").fill("john@doe.com")
+ page.get_by_label("Password").fill("password")
+ page.get_by_role("button", name="Login").click()
+
+ expect(page.locator(".alert")).to_have_text("You are now logged in")
+
+
+def test_upload(page: Page, john_doe, app_settings, tmp_path, dbaccess):
+ do_login(app_settings, page, john_doe)
+ page.goto("/")
+ page.get_by_text("Upload").click()
+
+ # We unpack one of the test GPX files
+ gpx_data = load_gpx_asset("Teasi_1.gpx.gz")
+ gpx_path = tmp_path / "Upload.gpx"
+ with open(gpx_path, "wb") as gpx_fobj:
+ gpx_fobj.write(gpx_data)
+
+ page.get_by_label("GPX file").set_input_files(gpx_path)
+ page.locator(".bi-upload").click()
+
+ # We now fill in most of the data
+ page.get_by_label("Title").fill("An awesome track!")
+ page.get_by_label("Date").type("07302022")
+ page.get_by_label("Date").press("Tab")
+ page.get_by_label("Date").type("12:41")
+ page.get_by_label("Visibility").select_option(label="Public")
+ page.get_by_label("Tags").fill("Tolle Tour")
+ page.get_by_role("button", name="Add Tag").click()
+ page.get_by_label("Description").fill("Beschreibung der tollen Tour")
+
+ page.locator(".btn", has_text="Upload").click()
+
+ # Once we have finished the upload, extract the ID of the track and check
+ # the properties
+ new_track_id = int(page.url.rsplit("/", 1)[1])
+ track = dbaccess.execute(select(models.Track).filter_by(id=new_track_id)).scalar_one()
+
+ assert track.title == "An awesome track!"
+ assert track.date.replace(tzinfo=None) == datetime.datetime(2022, 7, 30, 12, 41)
+ assert track.visibility == Visibility.PUBLIC
+ assert track.text_tags() == {"Tolle Tour"}
+ assert track.description == "Beschreibung der tollen Tour"
+
+
+def test_edit(page: Page, john_doe, app_settings, dbaccess):
+ do_login(app_settings, page, john_doe)
+ with dbaccess:
+ track = models.Track(
+ title="Another awesome track",
+ visibility=Visibility.PRIVATE,
+ description="Another description",
+ )
+ track.date = datetime.datetime.now(datetime.timezone.utc)
+ track.gpx_data = load_gpx_asset("Teasi_1.gpx.gz")
+ john_doe.tracks.append(track)
+ dbaccess.flush()
+ track_id = track.id
+ dbaccess.commit()
+
+ page.goto(f"/track/{track_id}")
+ page.locator(".btn", has_text="Edit").click()
+
+ # We now fill in most of the data
+ page.get_by_label("Title").fill("Not so awesome anymore!")
+ page.get_by_label("Date").type("09232019")
+ page.get_by_label("Date").press("Tab")
+ page.get_by_label("Date").type("15:28")
+ page.get_by_label("Visibility").select_option(label="Public")
+ page.get_by_label("Tags").fill("Shitty Tour")
+ page.get_by_role("button", name="Add Tag").click()
+ page.get_by_label("Description").fill("Not so descriptive anymore")
+
+ page.locator(".btn", has_text="Save").click()
+
+ track = dbaccess.execute(select(models.Track).filter_by(id=track_id)).scalar_one()
+
+ assert track.title == "Not so awesome anymore!"
+ assert track.date.replace(tzinfo=None) == datetime.datetime(2019, 9, 23, 15, 28)
+ assert track.visibility == Visibility.PUBLIC
+ assert track.text_tags() == {"Shitty Tour"}
+ assert track.description == "Not so descriptive anymore"
+
+
+def test_browse(page: Page, john_doe, app_settings, dbaccess):
+ do_login(app_settings, page, john_doe)
+ with dbaccess:
+ track = models.Track(
+ title="We're looking for this track",
+ visibility=Visibility.PRIVATE,
+ description="Another description",
+ type=TrackType.ORGANIC,
+ )
+ track.date = datetime.datetime.now(datetime.timezone.utc)
+ track.gpx_data = load_gpx_asset("Teasi_1.gpx.gz")
+ john_doe.tracks.append(track)
+ dbaccess.commit()
+
+ page.goto("/")
+ page.get_by_text("Browse").click()
+
+ expect(page.locator(".card-header", has_text="We're looking for this track")).to_be_visible()
+
+ page.get_by_role("textbox", name="Search terms").fill("Nothing")
+ page.get_by_role("button", name="Apply filters").click()
+
+ expect(
+ page.locator("p", has_text="No results matching the filters were found.")
+ ).to_be_visible()
+
+ page.get_by_role("textbox", name="Search terms").fill("looking for")
+ page.get_by_role("button", name="Apply filters").click()
+
+ expect(page.locator(".card-header", has_text="We're looking for this track")).to_be_visible()