From 0186eef20dbc9737f381069251380211d7d50f32 Mon Sep 17 00:00:00 2001 From: Simon Li Date: Wed, 12 Feb 2025 00:14:01 +0000 Subject: [PATCH] Add basic UI Playwright tests --- .github/workflows/test.yml | 10 +++++++ playwright-requirements.txt | 2 ++ tests/conftest.py | 30 ++++++++++++++++++-- tests/ui/README.md | 9 ++++++ tests/ui/browser/environment.yml | 2 ++ tests/ui/browser/external-verify | 7 +++++ tests/ui/browser/external-verify.py | 41 +++++++++++++++++++++++++++ tests/ui/browser/test-extra-args.yaml | 2 ++ tests/ui/browser/verify | 1 + 9 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 playwright-requirements.txt create mode 100644 tests/ui/README.md create mode 100644 tests/ui/browser/environment.yml create mode 100755 tests/ui/browser/external-verify create mode 100755 tests/ui/browser/external-verify.py create mode 100644 tests/ui/browser/test-extra-args.yaml create mode 100755 tests/ui/browser/verify diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 30045851..c8616b2a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -68,6 +68,10 @@ jobs: - ubuntu_version: "20.04" python_version: "3.6" repo_type: venv + # Playwright test + - ubuntu_version: "24.04" + python_version: "3.13" + repo_type: ui steps: - uses: actions/checkout@v4 @@ -80,6 +84,12 @@ jobs: pip install -r dev-requirements.txt pip freeze + - name: Install UI test dependencies + if: matrix.repo_type == 'ui' + run: | + pip install -r playwright-requirements.txt + playwright install firefox + - name: Install repo2docker run: | python -m build --wheel . diff --git a/playwright-requirements.txt b/playwright-requirements.txt new file mode 100644 index 00000000..d01da011 --- /dev/null +++ b/playwright-requirements.txt @@ -0,0 +1,2 @@ +-r dev-requirements.txt +pytest-playwright diff --git a/tests/conftest.py b/tests/conftest.py index bade0a1a..5d3a7479 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,7 +39,7 @@ def pytest_collect_file(parent, file_path): return RemoteRepoList.from_parent(parent, path=file_path) -def make_test_func(args, skip_build=False, extra_run_kwargs=None): +def make_test_func(args, skip_build=False, extra_run_kwargs=None, external_script=None): """Generate a test function that runs repo2docker""" def test(): @@ -82,6 +82,10 @@ def make_test_func(args, skip_build=False, extra_run_kwargs=None): success = True break assert success, f"Notebook never started in {container}" + + if external_script: + subprocess.check_call([external_script, f"http://localhost:{port}"]) + finally: # stop the container container.stop() @@ -202,12 +206,21 @@ class Repo2DockerTest(pytest.Function): """A pytest.Item for running repo2docker""" def __init__( - self, name, parent, args=None, skip_build=False, extra_run_kwargs=None + self, + name, + parent, + args=None, + skip_build=False, + extra_run_kwargs=None, + external_script=None, ): self.args = args self.save_cwd = os.getcwd() f = parent.obj = make_test_func( - args, skip_build=skip_build, extra_run_kwargs=extra_run_kwargs + args, + skip_build=skip_build, + extra_run_kwargs=extra_run_kwargs, + external_script=external_script, ) super().__init__(name, parent, callobj=f) @@ -246,6 +259,17 @@ class LocalRepo(pytest.File): args.append(str(self.path.parent)) yield Repo2DockerTest.from_parent(self, name="build", args=args) + # If external-verify exists it should be run on the host + external_verify_script = self.path.parent / "external-verify" + if external_verify_script.exists(): + yield Repo2DockerTest.from_parent( + self, + name=self.path.name, + args=args, + skip_build=True, + external_script=external_verify_script, + ) + yield Repo2DockerTest.from_parent( self, name=self.path.name, diff --git a/tests/ui/README.md b/tests/ui/README.md new file mode 100644 index 00000000..04adc855 --- /dev/null +++ b/tests/ui/README.md @@ -0,0 +1,9 @@ +# User interface tests + +This contains very basic [Playwright](https://playwright.dev/python/) tests to check the + +- JupyterLab +- RStudio +- RShiny + +interfaces can be accessed. diff --git a/tests/ui/browser/environment.yml b/tests/ui/browser/environment.yml new file mode 100644 index 00000000..9f6f2aab --- /dev/null +++ b/tests/ui/browser/environment.yml @@ -0,0 +1,2 @@ +dependencies: + - r-base diff --git a/tests/ui/browser/external-verify b/tests/ui/browser/external-verify new file mode 100755 index 00000000..1e78c86f --- /dev/null +++ b/tests/ui/browser/external-verify @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# This script is run outside the container + +set -eux + +export TEST_REPO2DOCKER_URL="${1}/?token=token" +pytest --verbose --color=yes --browser=firefox tests/ui/browser/external-verify.py diff --git a/tests/ui/browser/external-verify.py b/tests/ui/browser/external-verify.py new file mode 100755 index 00000000..bbeec18b --- /dev/null +++ b/tests/ui/browser/external-verify.py @@ -0,0 +1,41 @@ +import os +from subprocess import check_output +from urllib.parse import urlsplit + +import pytest +from playwright.sync_api import Page, expect + + +# To run this test manually: +# - Run: repo2docker tests/ui/browser/ +# - Run: TEST_REPO2DOCKER_URL= python -mpytest --browser=firefox tests/ui/browser/external-verify.py [--headed] +def test_user_interfaces(page: Page) -> None: + url = os.getenv("TEST_REPO2DOCKER_URL") + u = urlsplit(url) + + # Includes token + page.goto(url) + + # Initial page should be Jupyter Notebook + page.wait_for_url(f"{u.scheme}://{u.netloc}/tree") + + # Check JupyterLab + page.goto(f"{u.scheme}://{u.netloc}/lab") + expect(page.get_by_text("Python 3 (ipykernel)").nth(1)).to_be_visible() + + # Check JupyterLab RStudio launcher + with page.expect_popup() as page1_info: + page.get_by_text("RStudio [↗]").click() + page1 = page1_info.value + page1.wait_for_url(f"{u.scheme}://{u.netloc}/rstudio/") + # Top-left logo + expect(page1.locator("#rstudio_rstudio_logo")).to_be_visible() + # Initial RStudio console text + expect(page1.get_by_text("R version ")).to_be_visible() + + # Check JupyterLab RShiny launcher + with page.expect_popup() as page2_info: + page.get_by_text("Shiny [↗]").click() + page2 = page2_info.value + page2.wait_for_url(f"{u.scheme}://{u.netloc}/shiny/") + expect(page2.get_by_text("Index of /")).to_be_visible() diff --git a/tests/ui/browser/test-extra-args.yaml b/tests/ui/browser/test-extra-args.yaml new file mode 100644 index 00000000..574c469a --- /dev/null +++ b/tests/ui/browser/test-extra-args.yaml @@ -0,0 +1,2 @@ +- --env +- JUPYTER_TOKEN=token diff --git a/tests/ui/browser/verify b/tests/ui/browser/verify new file mode 100755 index 00000000..1a248525 --- /dev/null +++ b/tests/ui/browser/verify @@ -0,0 +1 @@ +#!/bin/sh