Skip to content

Commit 82d2268

Browse files
authored
Merge pull request #1891 from yuvipanda/binderhub-local
Add playwright based UI integration tests for existing UI
2 parents 0cd2b5b + c8193d7 commit 82d2268

File tree

13 files changed

+393
-187
lines changed

13 files changed

+393
-187
lines changed

.github/workflows/playwright.yaml

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
name: Playwright Tests
2+
3+
on:
4+
pull_request:
5+
paths-ignore:
6+
- "**.md"
7+
- "**.rst"
8+
- "docs/**"
9+
- "examples/**"
10+
- ".github/workflows/**"
11+
- "!.github/workflows/playwright.yaml"
12+
push:
13+
paths-ignore:
14+
- "**.md"
15+
- "**.rst"
16+
- "docs/**"
17+
- "examples/**"
18+
- ".github/workflows/**"
19+
- "!.github/workflows/playwright.yaml"
20+
branches-ignore:
21+
- "dependabot/**"
22+
- "pre-commit-ci-update-config"
23+
- "update-*"
24+
workflow_dispatch:
25+
26+
jobs:
27+
tests:
28+
runs-on: ubuntu-22.04
29+
timeout-minutes: 10
30+
31+
permissions:
32+
contents: read
33+
env:
34+
GITHUB_ACCESS_TOKEN: "${{ secrets.github_token }}"
35+
36+
steps:
37+
- uses: actions/checkout@v4
38+
39+
- name: Setup OS level dependencies
40+
run: |
41+
sudo apt-get update
42+
sudo apt-get install --yes \
43+
build-essential \
44+
curl \
45+
libcurl4-openssl-dev \
46+
libssl-dev
47+
48+
- uses: actions/setup-node@v4
49+
id: setup-node
50+
with:
51+
node-version: "22"
52+
53+
- name: Cache npm
54+
uses: actions/cache@v4
55+
with:
56+
path: ~/.npm
57+
key: node-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('**/package.json') }}-${{ github.job }}
58+
59+
- name: Run webpack to build static assets
60+
run: |
61+
npm install
62+
npm run webpack
63+
64+
- uses: actions/setup-python@v5
65+
id: setup-python
66+
with:
67+
python-version: "3.12"
68+
69+
- name: Cache pip
70+
uses: actions/cache@v4
71+
with:
72+
path: ~/.cache/pip
73+
key: python-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/*requirements.txt') }}-${{ github.job }}
74+
75+
- name: Setup test dependencies
76+
run: |
77+
npm i -g configurable-http-proxy
78+
79+
pip install --no-binary pycurl -r dev-requirements.txt
80+
pip install -e .
81+
82+
- name: Install playwright browser
83+
run: |
84+
playwright install firefox
85+
86+
- name: Run playwright tests
87+
run: |
88+
py.test --cov=binderhub -s integration-tests/
89+
90+
- uses: actions/upload-artifact@v4
91+
if: always()
92+
with:
93+
name: playwright-traces
94+
path: test-results/
95+
96+
# Upload test coverage info to codecov
97+
- uses: codecov/codecov-action@v5

.github/workflows/test.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -335,18 +335,18 @@ jobs:
335335
- name: Run main tests
336336
if: matrix.test == 'main'
337337
# running the "main" tests means "all tests that aren't auth"
338-
run: pytest -m "not auth" --cov=binderhub
338+
run: pytest -m "not auth" --cov=binderhub binderhub/tests/
339339

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

345345
- name: Run helm tests
346346
if: matrix.test == 'helm'
347347
run: |
348348
export BINDER_URL=http://localhost:30901
349-
pytest --helm -m "remote" --cov=binderhub
349+
pytest --helm -m "remote" --cov=binderhub binderhub/tests/
350350
351351
- name: Get BinderHub health and metrics outputs
352352
if: always()
@@ -449,7 +449,7 @@ jobs:
449449
- name: Run remote tests
450450
run: |
451451
export BINDER_URL=http://localhost:8000/services/binder/
452-
pytest -m remote --cov=binderhub
452+
pytest -m remote --cov=binderhub binderhub/tests/
453453
454454
- name: Show hub logs
455455
if: always()

binderhub/tests/conftest.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,12 @@
4242

4343

4444
def pytest_configure(config):
45-
"""This function has meaning to pytest, for more information, see:
46-
https://docs.pytest.org/en/stable/reference.html#pytest.hookspec.pytest_configure
45+
"""
46+
Configure plugins and custom markers
47+
48+
This function is called by pytest after command line arguments have
49+
been parsed. See https://docs.pytest.org/en/stable/reference/reference.html#pytest.hookspec.pytest_configure
50+
for more information.
4751
"""
4852
# register our custom markers
4953
config.addinivalue_line(

binderhub/tests/test_legacy.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""Test legacy redirects"""
2+
3+
import pytest
4+
5+
from .utils import async_requests
6+
7+
8+
@pytest.mark.parametrize(
9+
"old_url, new_url",
10+
[
11+
(
12+
"/repo/binderhub-ci-repos/requirements",
13+
"/v2/gh/binderhub-ci-repos/requirements/master",
14+
),
15+
(
16+
"/repo/binderhub-ci-repos/requirements/",
17+
"/v2/gh/binderhub-ci-repos/requirements/master",
18+
),
19+
(
20+
"/repo/binderhub-ci-repos/requirements/notebooks/index.ipynb",
21+
"/v2/gh/binderhub-ci-repos/requirements/master?urlpath=%2Fnotebooks%2Findex.ipynb",
22+
),
23+
],
24+
)
25+
async def test_legacy_redirect(app, old_url, new_url):
26+
r = await async_requests.get(app.url + old_url, allow_redirects=False)
27+
assert r.status_code == 302
28+
assert r.headers["location"] == new_url

binderhub/tests/test_main.py

Lines changed: 1 addition & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,15 @@
11
"""Test main handlers"""
22

33
import time
4-
from urllib.parse import quote, urlparse
4+
from urllib.parse import quote
55

66
import jwt
77
import pytest
88
from bs4 import BeautifulSoup
99

10-
from binderhub import __version__ as binder_version
11-
1210
from .utils import async_requests
1311

1412

15-
@pytest.mark.parametrize(
16-
"old_url, new_url",
17-
[
18-
(
19-
"/repo/binderhub-ci-repos/requirements",
20-
"/v2/gh/binderhub-ci-repos/requirements/master",
21-
),
22-
(
23-
"/repo/binderhub-ci-repos/requirements/",
24-
"/v2/gh/binderhub-ci-repos/requirements/master",
25-
),
26-
(
27-
"/repo/binderhub-ci-repos/requirements/notebooks/index.ipynb",
28-
"/v2/gh/binderhub-ci-repos/requirements/master?urlpath=%2Fnotebooks%2Findex.ipynb",
29-
),
30-
],
31-
)
32-
async def test_legacy_redirect(app, old_url, new_url):
33-
r = await async_requests.get(app.url + old_url, allow_redirects=False)
34-
assert r.status_code == 302
35-
assert r.headers["location"] == new_url
36-
37-
38-
def _resolve_url(page_url, url):
39-
"""Resolve a URL relative to a page"""
40-
41-
# full URL, nothing to resolve
42-
if "://" in url:
43-
return url
44-
45-
parsed = urlparse(page_url)
46-
47-
if url.startswith("/"):
48-
# absolute path
49-
return f"{parsed.scheme}://{parsed.netloc}{url}"
50-
51-
# relative path URL
52-
53-
if page_url.endswith("/"):
54-
# URL is a directory, resolve relative to dir
55-
path = parsed.path
56-
else:
57-
# URL is not a directory, resolve relative to parent
58-
path = parsed.path.rsplit("/", 1)[0] + "/"
59-
60-
return f"{parsed.scheme}://{parsed.netloc}{path}{url}"
61-
62-
63-
@pytest.mark.remote
64-
async def test_main_page(app):
65-
"""Check the main page and any links on it"""
66-
r = await async_requests.get(app.url)
67-
assert r.status_code == 200
68-
soup = BeautifulSoup(r.text, "html5lib")
69-
70-
# check src links (style, images)
71-
for el in soup.find_all(src=True):
72-
url = _resolve_url(app.url, el["src"])
73-
r = await async_requests.get(url)
74-
assert r.status_code == 200, f"{r.status_code} {url}"
75-
76-
# check hrefs
77-
for el in soup.find_all(href=True):
78-
href = el["href"]
79-
if href.startswith("#"):
80-
continue
81-
url = _resolve_url(app.url, href)
82-
r = await async_requests.get(url)
83-
assert r.status_code == 200, f"{r.status_code} {url}"
84-
85-
8613
@pytest.mark.remote
8714
@pytest.mark.helm
8815
async def test_custom_template(app):
@@ -92,94 +19,6 @@ async def test_custom_template(app):
9219
assert "test-template" in r.text
9320

9421

95-
@pytest.mark.remote
96-
async def test_about_handler(app):
97-
# Check that the about page loads
98-
r = await async_requests.get(app.url + "/about")
99-
assert r.status_code == 200
100-
assert "This website is powered by" in r.text
101-
assert binder_version.split("+")[0] in r.text
102-
103-
104-
@pytest.mark.remote
105-
async def test_versions_handler(app):
106-
# Check that the about page loads
107-
r = await async_requests.get(app.url + "/versions")
108-
assert r.status_code == 200
109-
110-
data = r.json()
111-
# builder_info is different for KubernetesExecutor and LocalRepo2dockerBuild
112-
try:
113-
import repo2docker
114-
115-
allowed_builder_info = [{"repo2docker-version": repo2docker.__version__}]
116-
except ImportError:
117-
allowed_builder_info = []
118-
allowed_builder_info.append({"build_image": app.build_image})
119-
120-
assert data["builder_info"] in allowed_builder_info
121-
assert data["binderhub"].split("+")[0] == binder_version.split("+")[0]
122-
123-
124-
@pytest.mark.parametrize(
125-
"provider_prefix,repo,ref,path,path_type,status_code",
126-
[
127-
("gh", "binderhub-ci-repos/requirements", "master", "", "", 200),
128-
("gh", "binderhub-ci-repos%2Frequirements", "master", "", "", 400),
129-
("gh", "binderhub-ci-repos/requirements", "master/", "", "", 200),
130-
(
131-
"gh",
132-
"binderhub-ci-repos/requirements",
133-
"20c4fe55a9b2c5011d228545e821b1c7b1723652",
134-
"index.ipynb",
135-
"file",
136-
200,
137-
),
138-
(
139-
"gh",
140-
"binderhub-ci-repos/requirements",
141-
"20c4fe55a9b2c5011d228545e821b1c7b1723652",
142-
"%2Fnotebooks%2Findex.ipynb",
143-
"url",
144-
200,
145-
),
146-
("gh", "binderhub-ci-repos/requirements", "master", "has%20space", "file", 200),
147-
(
148-
"gh",
149-
"binderhub-ci-repos/requirements",
150-
"master/",
151-
"%2Fhas%20space%2F",
152-
"file",
153-
200,
154-
),
155-
(
156-
"gh",
157-
"binderhub-ci-repos/requirements",
158-
"master",
159-
"%2Fhas%20space%2F%C3%BCnicode.ipynb",
160-
"file",
161-
200,
162-
),
163-
],
164-
)
165-
async def test_loading_page(
166-
app, provider_prefix, repo, ref, path, path_type, status_code
167-
):
168-
# repo = f'{org}/{repo_name}'
169-
spec = f"{repo}/{ref}"
170-
provider_spec = f"{provider_prefix}/{spec}"
171-
query = f"{path_type}path={path}" if path else ""
172-
uri = f"/v2/{provider_spec}?{query}"
173-
r = await async_requests.get(app.url + uri)
174-
assert r.status_code == status_code, f"{r.status_code} {uri}"
175-
if status_code == 200:
176-
soup = BeautifulSoup(r.text, "html5lib")
177-
assert soup.find(id="log-container")
178-
nbviewer_url = soup.find(id="nbviewer-preview").find("iframe").attrs["src"]
179-
r = await async_requests.get(nbviewer_url)
180-
assert r.status_code == 200, f"{r.status_code} {nbviewer_url}"
181-
182-
18322
@pytest.mark.parametrize(
18423
"origin,host,expected_origin",
18524
[

binderhub/tests/test_version.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Test version handler"""
2+
3+
import pytest
4+
5+
from binderhub import __version__ as binder_version
6+
7+
from .utils import async_requests
8+
9+
10+
@pytest.mark.remote
11+
async def test_versions_handler(app):
12+
# Check that the about page loads
13+
r = await async_requests.get(app.url + "/versions")
14+
assert r.status_code == 200
15+
16+
data = r.json()
17+
# builder_info is different for KubernetesExecutor and LocalRepo2dockerBuild
18+
try:
19+
import repo2docker
20+
21+
allowed_builder_info = [{"repo2docker-version": repo2docker.__version__}]
22+
except ImportError:
23+
allowed_builder_info = []
24+
allowed_builder_info.append({"build_image": app.build_image})
25+
26+
assert data["builder_info"] in allowed_builder_info
27+
assert data["binderhub"].split("+")[0] == binder_version.split("+")[0]

0 commit comments

Comments
 (0)