Skip to content

Commit

Permalink
Merge pull request #1891 from yuvipanda/binderhub-local
Browse files Browse the repository at this point in the history
Add playwright based UI integration tests for existing UI
  • Loading branch information
yuvipanda authored Dec 9, 2024
2 parents 0cd2b5b + c8193d7 commit 82d2268
Show file tree
Hide file tree
Showing 13 changed files with 393 additions and 187 deletions.
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

0 comments on commit 82d2268

Please sign in to comment.