Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add playwright based UI integration tests for existing UI #1891

Merged
merged 5 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .github/workflows/playwright.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: Playwright Tests

on:
pull_request:
paths-ignore:
- "**.md"
- "**.rst"
- "docs/**"
- "examples/**"
- ".github/workflows/**"
- "!.github/workflows/playwright.yaml"
push:
paths-ignore:
- "**.md"
- "**.rst"
- "docs/**"
- "examples/**"
- ".github/workflows/**"
- "!.github/workflows/playwright.yaml"
branches-ignore:
- "dependabot/**"
- "pre-commit-ci-update-config"
- "update-*"
workflow_dispatch:

jobs:
tests:
runs-on: ubuntu-22.04
timeout-minutes: 10

permissions:
contents: read
env:
GITHUB_ACCESS_TOKEN: "${{ secrets.github_token }}"

steps:
- uses: actions/checkout@v4

- name: Setup OS level dependencies
run: |
sudo apt-get update
sudo apt-get install --yes \
build-essential \
curl \
libcurl4-openssl-dev \
libssl-dev

- uses: actions/setup-node@v4
id: setup-node
with:
node-version: "22"

- name: Cache npm
uses: actions/cache@v4
with:
path: ~/.npm
key: node-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('**/package.json') }}-${{ github.job }}

- name: Run webpack to build static assets
run: |
npm install
npm run webpack

- uses: actions/setup-python@v5
id: setup-python
with:
python-version: "3.12"

- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: python-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/*requirements.txt') }}-${{ github.job }}

- name: Setup test dependencies
run: |
npm i -g configurable-http-proxy

pip install --no-binary pycurl -r dev-requirements.txt
pip install -e .

- name: Install playwright browser
run: |
playwright install firefox

- name: Run playwright tests
run: |
py.test --cov=binderhub -s integration-tests/

- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-traces
path: test-results/

# Upload test coverage info to codecov
- uses: codecov/codecov-action@v5
8 changes: 4 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -335,18 +335,18 @@ jobs:
- name: Run main tests
if: matrix.test == 'main'
# running the "main" tests means "all tests that aren't auth"
run: pytest -m "not auth" --cov=binderhub
run: pytest -m "not auth" --cov=binderhub binderhub/tests/

- name: Run auth tests
if: matrix.test == 'auth'
# running the "auth" tests means "all tests that are marked as auth"
run: pytest -m "auth" --cov=binderhub
run: pytest -m "auth" --cov=binderhub binderhub/tests/

- name: Run helm tests
if: matrix.test == 'helm'
run: |
export BINDER_URL=http://localhost:30901
pytest --helm -m "remote" --cov=binderhub
pytest --helm -m "remote" --cov=binderhub binderhub/tests/

- name: Get BinderHub health and metrics outputs
if: always()
Expand Down Expand Up @@ -449,7 +449,7 @@ jobs:
- name: Run remote tests
run: |
export BINDER_URL=http://localhost:8000/services/binder/
pytest -m remote --cov=binderhub
pytest -m remote --cov=binderhub binderhub/tests/

- name: Show hub logs
if: always()
Expand Down
8 changes: 6 additions & 2 deletions binderhub/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,12 @@


def pytest_configure(config):
"""This function has meaning to pytest, for more information, see:
https://docs.pytest.org/en/stable/reference.html#pytest.hookspec.pytest_configure
"""
Configure plugins and custom markers

This function is called by pytest after command line arguments have
been parsed. See https://docs.pytest.org/en/stable/reference/reference.html#pytest.hookspec.pytest_configure
for more information.
"""
# register our custom markers
config.addinivalue_line(
Expand Down
28 changes: 28 additions & 0 deletions binderhub/tests/test_legacy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Test legacy redirects"""

import pytest

from .utils import async_requests


@pytest.mark.parametrize(
"old_url, new_url",
[
(
"/repo/binderhub-ci-repos/requirements",
"/v2/gh/binderhub-ci-repos/requirements/master",
),
(
"/repo/binderhub-ci-repos/requirements/",
"/v2/gh/binderhub-ci-repos/requirements/master",
),
(
"/repo/binderhub-ci-repos/requirements/notebooks/index.ipynb",
"/v2/gh/binderhub-ci-repos/requirements/master?urlpath=%2Fnotebooks%2Findex.ipynb",
),
],
)
async def test_legacy_redirect(app, old_url, new_url):
r = await async_requests.get(app.url + old_url, allow_redirects=False)
assert r.status_code == 302
assert r.headers["location"] == new_url
163 changes: 1 addition & 162 deletions binderhub/tests/test_main.py
Original file line number Diff line number Diff line change
@@ -1,88 +1,15 @@
"""Test main handlers"""

import time
from urllib.parse import quote, urlparse
from urllib.parse import quote

import jwt
import pytest
from bs4 import BeautifulSoup

from binderhub import __version__ as binder_version

from .utils import async_requests


@pytest.mark.parametrize(
"old_url, new_url",
[
(
"/repo/binderhub-ci-repos/requirements",
"/v2/gh/binderhub-ci-repos/requirements/master",
),
(
"/repo/binderhub-ci-repos/requirements/",
"/v2/gh/binderhub-ci-repos/requirements/master",
),
(
"/repo/binderhub-ci-repos/requirements/notebooks/index.ipynb",
"/v2/gh/binderhub-ci-repos/requirements/master?urlpath=%2Fnotebooks%2Findex.ipynb",
),
],
)
async def test_legacy_redirect(app, old_url, new_url):
r = await async_requests.get(app.url + old_url, allow_redirects=False)
assert r.status_code == 302
assert r.headers["location"] == new_url


def _resolve_url(page_url, url):
"""Resolve a URL relative to a page"""

# full URL, nothing to resolve
if "://" in url:
return url

parsed = urlparse(page_url)

if url.startswith("/"):
# absolute path
return f"{parsed.scheme}://{parsed.netloc}{url}"

# relative path URL

if page_url.endswith("/"):
# URL is a directory, resolve relative to dir
path = parsed.path
else:
# URL is not a directory, resolve relative to parent
path = parsed.path.rsplit("/", 1)[0] + "/"

return f"{parsed.scheme}://{parsed.netloc}{path}{url}"


@pytest.mark.remote
async def test_main_page(app):
"""Check the main page and any links on it"""
r = await async_requests.get(app.url)
assert r.status_code == 200
soup = BeautifulSoup(r.text, "html5lib")

# check src links (style, images)
for el in soup.find_all(src=True):
url = _resolve_url(app.url, el["src"])
r = await async_requests.get(url)
assert r.status_code == 200, f"{r.status_code} {url}"

# check hrefs
for el in soup.find_all(href=True):
href = el["href"]
if href.startswith("#"):
continue
url = _resolve_url(app.url, href)
r = await async_requests.get(url)
assert r.status_code == 200, f"{r.status_code} {url}"


@pytest.mark.remote
@pytest.mark.helm
async def test_custom_template(app):
Expand All @@ -92,94 +19,6 @@ async def test_custom_template(app):
assert "test-template" in r.text


@pytest.mark.remote
async def test_about_handler(app):
# Check that the about page loads
r = await async_requests.get(app.url + "/about")
assert r.status_code == 200
assert "This website is powered by" in r.text
assert binder_version.split("+")[0] in r.text


@pytest.mark.remote
async def test_versions_handler(app):
# Check that the about page loads
r = await async_requests.get(app.url + "/versions")
assert r.status_code == 200

data = r.json()
# builder_info is different for KubernetesExecutor and LocalRepo2dockerBuild
try:
import repo2docker

allowed_builder_info = [{"repo2docker-version": repo2docker.__version__}]
except ImportError:
allowed_builder_info = []
allowed_builder_info.append({"build_image": app.build_image})

assert data["builder_info"] in allowed_builder_info
assert data["binderhub"].split("+")[0] == binder_version.split("+")[0]


@pytest.mark.parametrize(
"provider_prefix,repo,ref,path,path_type,status_code",
[
("gh", "binderhub-ci-repos/requirements", "master", "", "", 200),
("gh", "binderhub-ci-repos%2Frequirements", "master", "", "", 400),
("gh", "binderhub-ci-repos/requirements", "master/", "", "", 200),
(
"gh",
"binderhub-ci-repos/requirements",
"20c4fe55a9b2c5011d228545e821b1c7b1723652",
"index.ipynb",
"file",
200,
),
(
"gh",
"binderhub-ci-repos/requirements",
"20c4fe55a9b2c5011d228545e821b1c7b1723652",
"%2Fnotebooks%2Findex.ipynb",
"url",
200,
),
("gh", "binderhub-ci-repos/requirements", "master", "has%20space", "file", 200),
(
"gh",
"binderhub-ci-repos/requirements",
"master/",
"%2Fhas%20space%2F",
"file",
200,
),
(
"gh",
"binderhub-ci-repos/requirements",
"master",
"%2Fhas%20space%2F%C3%BCnicode.ipynb",
"file",
200,
),
],
)
async def test_loading_page(
app, provider_prefix, repo, ref, path, path_type, status_code
):
# repo = f'{org}/{repo_name}'
spec = f"{repo}/{ref}"
provider_spec = f"{provider_prefix}/{spec}"
query = f"{path_type}path={path}" if path else ""
uri = f"/v2/{provider_spec}?{query}"
r = await async_requests.get(app.url + uri)
assert r.status_code == status_code, f"{r.status_code} {uri}"
if status_code == 200:
soup = BeautifulSoup(r.text, "html5lib")
assert soup.find(id="log-container")
nbviewer_url = soup.find(id="nbviewer-preview").find("iframe").attrs["src"]
r = await async_requests.get(nbviewer_url)
assert r.status_code == 200, f"{r.status_code} {nbviewer_url}"


@pytest.mark.parametrize(
"origin,host,expected_origin",
[
Expand Down
27 changes: 27 additions & 0 deletions binderhub/tests/test_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Test version handler"""

import pytest

from binderhub import __version__ as binder_version

from .utils import async_requests


@pytest.mark.remote
async def test_versions_handler(app):
# Check that the about page loads
r = await async_requests.get(app.url + "/versions")
assert r.status_code == 200

data = r.json()
# builder_info is different for KubernetesExecutor and LocalRepo2dockerBuild
try:
import repo2docker

allowed_builder_info = [{"repo2docker-version": repo2docker.__version__}]
except ImportError:
allowed_builder_info = []
allowed_builder_info.append({"build_image": app.build_image})

assert data["builder_info"] in allowed_builder_info
assert data["binderhub"].split("+")[0] == binder_version.split("+")[0]
Loading
Loading