Skip to content

Commit 12eb171

Browse files
committed
Add Playwright tests, remove websockify from image
1 parent c224790 commit 12eb171

File tree

8 files changed

+112
-74
lines changed

8 files changed

+112
-74
lines changed

.github/workflows/test.yaml

Lines changed: 9 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -40,25 +40,14 @@ jobs:
4040
run: |
4141
docker build --progress=plain --build-arg vncserver=${{ matrix.vncserver }} -t test .
4242
43-
- name: (inside container) websockify --help
44-
run: |
45-
docker run test websockify --help
46-
4743
- name: (inside container) vncserver -help
4844
run: |
4945
# -help flag is not available for TurboVNC, but it emits the -help
5046
# equivalent information anyhow if passed -help, but also errors. Due
5147
# to this, we fallback to use the errorcode of vncsrever -list.
5248
docker run test bash -c "vncserver -help || vncserver -list > /dev/null"
5349
54-
- name: Install websocat, a test dependency"
55-
run: |
56-
wget -q https://github.com/vi/websocat/releases/download/v1.12.0/websocat.x86_64-unknown-linux-musl \
57-
-O /usr/local/bin/websocat
58-
chmod +x /usr/local/bin/websocat
59-
6050
- name: Test vncserver
61-
if: always()
6251
run: |
6352
container_id=$(docker run -d -it -p 5901:5901 test vncserver -xstartup /opt/install/jupyter_remote_desktop_proxy/share/xstartup -verbose -fg -geometry 1680x1050 -SecurityTypes None -rfbport 5901)
6453
sleep 1
@@ -79,71 +68,24 @@ jobs:
7968
8069
docker stop $container_id > /dev/null
8170
if [ "$TEST_OK" == "false" ]; then
82-
echo "One or more tests failed!"
71+
echo "Test failed!"
8372
exit 1
8473
fi
8574
86-
- name: Test websockify'ed vncserver
87-
if: always()
75+
- name: Install playwright
8876
run: |
89-
container_id=$(docker run -d -it -p 5901:5901 test websockify --verbose --log-file=/tmp/websockify.log --heartbeat=30 5901 -- vncserver -xstartup /opt/install/jupyter_remote_desktop_proxy/share/xstartup -verbose -fg -geometry 1680x1050 -SecurityTypes None -rfbport 5901)
90-
sleep 1
77+
python -mpip install -r dev-requirements.txt
78+
python -mplaywright install --with-deps
9179
92-
echo "::group::Install websocat, a test dependency"
93-
docker exec --user root $container_id bash -c '
94-
wget -q https://github.com/vi/websocat/releases/download/v1.12.0/websocat.x86_64-unknown-linux-musl \
95-
-O /usr/local/bin/websocat
96-
chmod +x /usr/local/bin/websocat
97-
'
98-
echo "::endgroup::"
99-
100-
docker exec -it $container_id websocat --binary --one-message --exit-on-eof "ws://localhost:5901/" 2>&1 | tee -a /dev/stderr | \
101-
grep --quiet RFB && echo "Passed test" || { echo "Failed test" && TEST_OK=false; }
102-
103-
echo "::group::websockify logs"
104-
docker exec $container_id bash -c "cat /tmp/websockify.log"
105-
echo "::endgroup::"
106-
107-
echo "::group::vncserver logs"
108-
docker exec $container_id bash -c 'cat ~/.vnc/*.log'
109-
echo "::endgroup::"
110-
111-
docker stop $container_id > /dev/null
112-
if [ "$TEST_OK" == "false" ]; then
113-
echo "One or more tests failed!"
114-
exit 1
115-
fi
116-
117-
- name: Test project's proxy to websockify'ed vncserver
118-
if: always()
80+
- name: Playwright browser test
11981
run: |
12082
container_id=$(docker run -d -it -p 8888:8888 -e JUPYTER_TOKEN=secret test)
12183
sleep 3
84+
export CONTAINER_ID=$container_id
85+
export JUPYTER_HOST=http://localhost:8888
86+
export JUPYTER_TOKEN=secret
12287
123-
curl --silent --fail 'http://localhost:8888/desktop/?token=secret' | grep --quiet 'Jupyter Remote Desktop Proxy' && echo "Passed get index.html test" || { echo "Failed get index.html test" && TEST_OK=false; }
124-
curl --silent --fail 'http://localhost:8888/desktop/static/dist/viewer.js?token=secret' > /dev/null && echo "Passed get viewer.js test" || { echo "Failed get viewer.js test" && TEST_OK=false; }
125-
126-
# The first attempt often fails, but the second always(?) succeeds.
127-
#
128-
# This could be related to jupyter-server-proxy's issue
129-
# https://github.com/jupyterhub/jupyter-server-proxy/issues/459
130-
# because the client/proxy websocket handshake completes before the
131-
# proxy/server handshake. This issue is tracked for this project by
132-
# https://github.com/jupyterhub/jupyter-remote-desktop-proxy/issues/105.
133-
#
134-
websocat --binary --one-message --exit-on-eof 'ws://localhost:8888/desktop-websockify/?token=secret' 2>&1 \
135-
| tee -a /dev/stderr \
136-
| grep --quiet RFB \
137-
&& echo "Passed initial websocket test" \
138-
|| { \
139-
echo "Failed initial websocket test" \
140-
&& sleep 1 \
141-
&& websocat --binary --one-message --exit-on-eof 'ws://localhost:8888/desktop-websockify/?token=secret' 2>&1 \
142-
| tee -a /dev/stderr \
143-
| grep --quiet RFB \
144-
&& echo "Passed second websocket test" \
145-
|| { echo "Failed second websocket test" && TEST_OK=false; } \
146-
}
88+
python -mpytest -vs
14789
14890
echo "::group::jupyter_server logs"
14991
docker logs $container_id
@@ -159,6 +101,3 @@ jobs:
159101
echo "One or more tests failed!"
160102
exit 1
161103
fi
162-
163-
# TODO: Check VNC desktop works, e.g. by comparing Playwright screenshots
164-
# https://playwright.dev/docs/test-snapshots

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,6 @@ dmypy.json
134134

135135
# Pyre type checker
136136
.pyre/
137+
138+
# Additional ignores
139+
screenshots/

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ USER root
55
RUN apt-get -y -qq update \
66
&& apt-get -y -qq install \
77
dbus-x11 \
8+
xclip \
89
xfce4 \
910
xfce4-panel \
1011
xfce4-session \
@@ -55,5 +56,4 @@ RUN . /opt/conda/bin/activate && \
5556

5657
COPY --chown=$NB_UID:$NB_GID . /opt/install
5758
RUN . /opt/conda/bin/activate && \
58-
pip install -e /opt/install && \
59-
jupyter server extension enable jupyter_remote_desktop_proxy
59+
pip install /opt/install

dev-requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pillow==10.3.0
2+
playwright==1.44.0
3+
pytest==8.2.2

environment.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,3 @@ dependencies:
44
- jupyter-server-proxy>=4.3.0
55
- jupyterhub-singleuser
66
- pip
7-
# TODO: remove when test.yaml is updated
8-
- websockify

tests/conftest.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from os import getenv
2+
3+
import pytest
4+
from playwright.sync_api import sync_playwright
5+
6+
HEADLESS = getenv("HEADLESS", "1").lower() == "1"
7+
8+
9+
@pytest.fixture()
10+
def browser():
11+
# browser_type in ["chromium", "firefox", "webkit"]
12+
with sync_playwright() as playwright:
13+
browser = playwright.firefox.launch(headless=HEADLESS)
14+
context = browser.new_context()
15+
page = context.new_page()
16+
yield page
17+
context.clear_cookies()
18+
browser.close()

tests/reference/desktop.png

209 KB
Loading

tests/test_browser.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from os import getenv
2+
from pathlib import Path
3+
from shutil import which
4+
from subprocess import check_output
5+
from uuid import uuid4
6+
7+
from PIL import Image, ImageChops
8+
from playwright.sync_api import expect
9+
10+
HERE = Path(__file__).absolute().parent
11+
12+
CONTAINER_ID = getenv("CONTAINER_ID", "test")
13+
JUPYTER_HOST = getenv("JUPYTER_HOST", "http://localhost:8888")
14+
JUPYTER_TOKEN = getenv("JUPYTER_TOKEN", "secret")
15+
16+
17+
def compare_screenshot(test_image, threshold=1):
18+
# Compare images by calculating the mean absolute difference
19+
# Images must be the same size
20+
# threshold: Average difference per pixel, this depends on the image type
21+
# e.g. for 24 bit images (8 bit RGB pixels) threshold=1 means a maximum
22+
# difference of 1 bit per pixel per channel
23+
reference = Image.open(HERE / "reference" / "desktop.png")
24+
test = Image.open(test_image)
25+
26+
# Absolute difference
27+
# Convert to RGB, alpha channel breaks ImageChops
28+
diff = ImageChops.difference(reference.convert("RGB"), test.convert("RGB"))
29+
diff_data = diff.getdata()
30+
31+
m = sum(sum(px) for px in diff_data) / diff_data.size[0] / diff_data.size[1]
32+
assert m < threshold
33+
34+
35+
# To debug this set environment variable HEADLESS=0
36+
def test_desktop(browser):
37+
page = browser
38+
page.goto(f"{JUPYTER_HOST}/lab?token={JUPYTER_TOKEN}")
39+
page.wait_for_url(f"{JUPYTER_HOST}/lab")
40+
41+
# JupyterLab extension icon
42+
expect(page.get_by_text("Desktop [↗]")).to_be_visible()
43+
with page.expect_popup() as page1_info:
44+
page.get_by_text("Desktop [↗]").click()
45+
page1 = page1_info.value
46+
page1.wait_for_url(f"{JUPYTER_HOST}/desktop/")
47+
48+
expect(page1.get_by_text("Status: Connected")).to_be_visible()
49+
expect(page1.locator("canvas")).to_be_visible()
50+
51+
# Screenshot the desktop element only
52+
# May take a few seconds to load
53+
page1.wait_for_timeout(5000)
54+
# Use a nontemp folder so we can check it manually if necessary
55+
screenshot = "screenshots/desktop.png"
56+
page1.locator("canvas").screenshot(path=screenshot)
57+
58+
# Open clipboard, enter random text, close clipboard
59+
clipboard_text = str(uuid4())
60+
page1.get_by_role("link", name="Remote Clipboard").click()
61+
page1.wait_for_selector("#clipboard-text")
62+
page1.locator("#clipboard-text").click()
63+
page1.locator("#clipboard-text").fill(clipboard_text)
64+
page1.get_by_role("link", name="Remote Clipboard").click()
65+
66+
# Exec into container to check clipboard contents
67+
for engine in ["docker", "podman"]:
68+
if which(engine):
69+
break
70+
else:
71+
raise RuntimeError("Container engine not found")
72+
clipboard = check_output(
73+
[engine, "exec", "-eDISPLAY=:1", CONTAINER_ID, "xclip", "-o"]
74+
)
75+
assert clipboard.decode() == clipboard_text
76+
77+
compare_screenshot(screenshot)

0 commit comments

Comments
 (0)