Repository: Kinto/kinto (language: python)

Issue:
Investigate selenium test errors
We've had some recent inconsistent test runs with browser-tests where selenium will fail due to stale DOM elements. Initial investigations couldn't find a good solution so we have a time.sleep(1) line in the browser tests now. Locate and fix this the root cause.


Discussion / hints:
We also now have errors on Selenium > 4.12

Fix patch:
diff --git a/Makefile b/Makefile
index c57ebbdbc..5726bf295 100644
--- a/Makefile
+++ b/Makefile
@@ -116,6 +116,7 @@ functional: install-dev need-kinto-running
 	$(VENV)/bin/py.test tests/functional.py
 
 browser-test: need-kinto-running
+	$(VENV)/bin/playwright install firefox
 	$(VENV)/bin/py.test tests/browser.py
 
 clean:
diff --git a/constraints.in b/constraints.in
index b706aa9ec..1a2c98377 100644
--- a/constraints.in
+++ b/constraints.in
@@ -33,7 +33,7 @@ bravado_core
 pytest
 pytest-cache
 pytest-cov
-selenium
+playwright
 webtest
 # dev
 build
diff --git a/constraints.txt b/constraints.txt
index 0ad1011a8..f24719053 100644
--- a/constraints.txt
+++ b/constraints.txt
@@ -1,17 +1,15 @@
 #
-# This file is autogenerated by pip-compile with Python 3.9
+# This file is autogenerated by pip-compile with Python 3.10
 # by the following command:
 #
-#    pip-compile --strip-extras
+#    pip-compile --output-file=constraints.txt --strip-extras constraints.in
 #
 arrow==1.3.0
     # via isoduration
 attrs==23.2.0
     # via
     #   jsonschema
-    #   outcome
     #   referencing
-    #   trio
 bcrypt==4.1.2
     # via -r constraints.in
 beautifulsoup4==4.12.3
@@ -23,7 +21,6 @@ build==1.0.3
 certifi==2023.11.17
     # via
     #   requests
-    #   selenium
     #   sentry-sdk
 charset-normalizer==3.3.2
     # via requests
@@ -46,27 +43,21 @@ coverage==7.4.0
 dockerflow==2024.1.0
     # via -r constraints.in
 exceptiongroup==1.2.0
-    # via
-    #   pytest
-    #   trio
-    #   trio-websocket
+    # via pytest
 execnet==2.0.2
     # via pytest-cache
 fqdn==1.5.1
     # via jsonschema
 greenlet==3.0.3
-    # via sqlalchemy
-h11==0.14.0
-    # via wsproto
+    # via
+    #   playwright
+    #   sqlalchemy
 hupper==1.12
     # via pyramid
 idna==3.6
     # via
     #   jsonschema
     #   requests
-    #   trio
-importlib-metadata==7.0.1
-    # via build
 iniconfig==2.0.0
     # via pytest
 iso8601==2.1.0
@@ -96,8 +87,6 @@ msgpack==1.0.7
     # via bravado-core
 newrelic==9.6.0
     # via -r constraints.in
-outcome==1.3.0.post0
-    # via trio
 packaging==23.2
     # via
     #   build
@@ -111,10 +100,14 @@ plaster==1.1.2
     #   pyramid
 plaster-pastedeploy==1.0.1
     # via pyramid
+playwright==1.41.1
+    # via -r constraints.in
 pluggy==1.3.0
     # via pytest
 psycopg2==2.9.9
     # via -r constraints.in
+pyee==11.0.1
+    # via playwright
 pyproject-hooks==1.0.0
     # via build
 pyramid==2.0.2
@@ -130,8 +123,6 @@ pyramid-multiauth==1.0.1
     # via -r constraints.in
 pyramid-tm==2.5
     # via -r constraints.in
-pysocks==1.7.1
-    # via urllib3
 pytest==7.4.4
     # via
     #   -r constraints.in
@@ -176,8 +167,6 @@ rpds-py==0.17.1
     #   referencing
 ruff==0.1.15
     # via -r constraints.in
-selenium==4.12.0
-    # via -r constraints.in
 sentry-sdk==1.39.2
     # via -r constraints.in
 simplejson==3.19.2
@@ -188,10 +177,6 @@ six==1.16.0
     #   cornice-swagger
     #   python-dateutil
     #   rfc3339-validator
-sniffio==1.3.0
-    # via trio
-sortedcontainers==2.4.0
-    # via trio
 soupsieve==2.5
     # via beautifulsoup4
 sqlalchemy==2.0.25
@@ -220,16 +205,11 @@ translationstring==1.4
     # via
     #   colander
     #   pyramid
-trio==0.24.0
-    # via
-    #   selenium
-    #   trio-websocket
-trio-websocket==0.11.1
-    # via selenium
 types-python-dateutil==2.8.19.20240106
     # via arrow
 typing-extensions==4.9.0
     # via
+    #   pyee
     #   sqlalchemy
     #   swagger-spec-validator
 uri-template==1.3.0
@@ -237,7 +217,6 @@ uri-template==1.3.0
 urllib3==2.1.0
     # via
     #   requests
-    #   selenium
     #   sentry-sdk
 venusian==3.1.0
     # via
@@ -257,10 +236,6 @@ webtest==3.0.0
     # via -r constraints.in
 werkzeug==3.0.1
     # via -r constraints.in
-wsproto==1.2.0
-    # via trio-websocket
-zipp==3.17.0
-    # via importlib-metadata
 zope-deprecation==5.0
     # via pyramid
 zope-interface==6.1
diff --git a/docs/community.rst b/docs/community.rst
index 1abc7afc3..3751913c3 100644
--- a/docs/community.rst
+++ b/docs/community.rst
@@ -235,16 +235,7 @@ In another terminal, run the end-to-end tests with:
 
 Browser Tests
 -------------
-
-Make sure the `geckodriver <https://github.com/mozilla/geckodriver/releases>`_ binary is available in your path.
-
-.. note::
-
-    If your installation of *Firefox* is custom, specify the path of its binary using an alias:
-
-    ::
-
-        alias geckodriver="geckodriver --binary /path/to/firefox"
+We use `playwright <https://playwright.dev/>`_ for browser testing. The tests included in this repo are very simple and verify the admin UI can at least authenticate with the current kinto back-end. Comprehensive unit tests are maintained in the kinto-admin repo.
 
 
 In a terminal, run an instance with the provided ``browser.ini`` configuration:
diff --git a/pyproject.toml b/pyproject.toml
index b21d218c3..3385307c1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -87,7 +87,7 @@ test = [
     "pytest",
     "pytest-cache",
     "pytest-cov",
-    "selenium",
+    "playwright",
     "webtest",
 ]
 dev = [


Test patch:
diff --git a/tests/browser.py b/tests/browser.py
index 3496aaf6d..eaca64dfa 100644
--- a/tests/browser.py
+++ b/tests/browser.py
@@ -1,68 +1,38 @@
-import time
+import re
 import unittest
-from urllib.parse import urljoin
 
-import requests
-from selenium import webdriver
-from selenium.webdriver.common.by import By
+from playwright.sync_api import expect, sync_playwright
 
 
-SERVER_URL = "http://localhost:8888/v1"
-DEFAULT_AUTH = ("user", "p4ssw0rd")
+baseUrl = "http://localhost:8888/v1/"
+auth = {"user": "user", "password": "p4ssw0rd"}
+
+browser = sync_playwright().start().firefox.launch()
+context = browser.new_context(base_url=baseUrl)
+page = browser.new_page()
 
 
 class BrowserTest(unittest.TestCase):
     def setUp(self):
-        options = webdriver.FirefoxOptions()
-        options.headless = True
-        self.driver = webdriver.Firefox(options=options)
-        self.driver.implicitly_wait(10)  # seconds
-
-    @classmethod
-    def setUpClass(cls):
-        # Make sure our user exists.
-        requests.post(
-            urljoin(SERVER_URL, "/accounts"),
-            json={"data": {"id": DEFAULT_AUTH[0], "password": DEFAULT_AUTH[1]}},
-        )
-
-        # Create a bucket and a collection for our user.
-        bucket_url = urljoin(SERVER_URL, "/buckets/workspace")
-        collection_url = f"{bucket_url}/collections/articles"
-        session = requests.Session()
-        session.auth = DEFAULT_AUTH
-        resp = session.put(bucket_url)
-        resp.raise_for_status()
-        resp = session.put(collection_url)
-        resp.raise_for_status()
+        request = context.request
 
-    def tearDown(self):
-        self.driver.close()
+        request.post("accounts", data={"data": {"id": auth["user"], "password": auth["password"]}})
 
-    def test_admin_ui_renders_properly(self):
-        base_url = urljoin(SERVER_URL, "/v1/admin/")
-        self.driver.get(base_url)
+    def test_login_and_view_home_page(self):
+        page.goto(f"{baseUrl}admin/")
 
-        # Load auth page.
-        header = self.driver.find_element(By.CSS_SELECTOR, ".content div > h1")
-        self.assertIn("Administration", header.text)
-        self.assertTrue(header.is_displayed())
+        expect(page).to_have_title(re.compile("Kinto Administration"))
 
-        # Select Kinto Accounts.
-        radio = self.driver.find_element(By.XPATH, "//label[contains(.,'Kinto Account Auth')]")
-        radio.click()
+        page.get_by_label("Kinto Account Auth").click()
+        txtUsername = page.get_by_label(re.compile("Username"))
+        txtPassword = page.get_by_label(re.compile("Password"))
 
-        # Fill username and password.
-        user_field = self.driver.find_element(By.ID, "root_credentials_username")
-        user_field.send_keys(DEFAULT_AUTH[0])
-        user_pass = self.driver.find_element(By.ID, "root_credentials_password")
-        user_pass.send_keys(DEFAULT_AUTH[1])
-        # Login
-        submit = self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']")
-        submit.click()
+        txtUsername.fill(auth["user"])
+        txtPassword.fill(auth["password"])
+        page.get_by_text(re.compile("Sign in using Kinto Account Auth")).click()
 
-        # Navigate to simple review page (uses React Hooks and broke a few times)
-        review_url = base_url + "#/buckets/workspace/collections/articles/simple-review"
-        self.driver.get(review_url)
-        time.sleep(1)
-        self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, ".alert-warning").is_displayed())
+        expect(page.get_by_text("Kinto Administration")).to_be_visible()
+        expect(page.get_by_text("project_name")).to_be_visible()
+        expect(page.get_by_text("project_version")).to_be_visible()
+        expect(page.get_by_text("http_api_version")).to_be_visible()
+        expect(page.get_by_text("project_docs")).to_be_visible()