diff options
Diffstat (limited to 'tests')
-rw-r--r-- | tests/playwright/conftest.py | 48 | ||||
-rw-r--r-- | tests/playwright/test_basic.py | 184 |
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() |