Skip to content

Commit f2eb317

Browse files
committed
Add sample playwright tests for folium
First try: just do the draw plugin.
1 parent e468548 commit f2eb317

File tree

8 files changed

+300
-2
lines changed

8 files changed

+300
-2
lines changed

.github/workflows/test_code.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,4 @@ jobs:
3434
run: python -m pip install -e . --no-deps --force-reinstall
3535

3636
- name: Code tests
37-
run: python -m pytest -vv --ignore=tests/selenium
37+
run: python -m pytest -vv --ignore=tests/selenium --ignore=tests/playwright

.github/workflows/test_latest_branca.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ jobs:
3131
run: |
3232
micromamba remove branca --yes --force
3333
python -m pip install git+https://github.com/python-visualization/branca.git
34-
python -m pytest -vv --ignore=tests/selenium
34+
python -m pytest -vv --ignore=tests/selenium --ignore=tests/playwright

.github/workflows/test_playwright.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Run Playwright Tests
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
9+
jobs:
10+
run:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout Folium
15+
uses: actions/checkout@v4
16+
17+
- name: Setup Micromamba env
18+
uses: mamba-org/setup-micromamba@v2
19+
with:
20+
environment-name: TEST
21+
create-args: >-
22+
python=3
23+
--file requirements.txt
24+
25+
- name: Install streamlit-folium
26+
shell: bash -l {0}
27+
run: |
28+
pip install streamlit-folium pytest-playwright pytest-rerunfailures
29+
30+
- name: Install playwright dependencies
31+
shell: bash -l {0}
32+
run: |
33+
playwright install --with-deps
34+
35+
- name: Install annotate-failures-plugin
36+
run: pip install pytest-github-actions-annotate-failures
37+
38+
- name: Install folium from source
39+
shell: bash -l {0}
40+
run: |
41+
python -m pip install -e . --no-deps --force-reinstall
42+
43+
- name: Test with pytest and retry flaky tests up to 3 times
44+
shell: bash -l {0}
45+
run: |
46+
pytest tests/playwright --browser chromium -s --reruns 3 --junit-xml=test-results.xml
47+
48+
- name: Surface failing tests
49+
if: always()
50+
uses: pmeier/pytest-results-action@main
51+
with:
52+
path: test-results.xml
53+
fail-on-empty: false

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ repos:
99
- id: debug-statements
1010
- id: end-of-file-fixer
1111
- id: check-docstring-first
12+
exclude: ^examples/streamlit
1213
- id: check-added-large-files
1314
- id: requirements-txt-fixer
1415
- id: file-contents-sorter

examples/streamlit/main.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import streamlit as st
2+
3+
st.logo("https://python-visualization.github.io/folium/latest/_static/folium_logo.png")
4+
st.title("Python data, leaflet.js maps")
5+
6+
"""
7+
Folium builds on the data wrangling strengths of the Python ecosystem
8+
and the mapping strengths of the Leaflet.js library.
9+
10+
Manipulate your data in Python, then visualize it in a Leaflet map via
11+
Folium."""
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import streamlit as st
2+
3+
st.set_page_config(
4+
page_title="streamlit-folium documentation: Draw Support",
5+
page_icon=":pencil:",
6+
layout="wide",
7+
)
8+
9+
"""
10+
# streamlit-folium: Draw Support
11+
12+
Folium supports some of the [most popular leaflet
13+
plugins](https://python-visualization.github.io/folium/plugins.html). In this example,
14+
we can add the
15+
[`Draw`](https://python-visualization.github.io/folium/plugins.html#folium.plugins.Draw)
16+
plugin to our map, which allows for drawing geometric shapes on the map.
17+
18+
When a shape is drawn on the map, the coordinates that represent that shape are passed
19+
back as a geojson feature via the `all_drawings` and `last_active_drawing` data fields.
20+
21+
Draw something below to see the return value back to Streamlit!
22+
"""
23+
24+
with st.echo(code_location="below"):
25+
import streamlit as st
26+
from streamlit_folium import st_folium
27+
28+
import folium
29+
from folium.plugins import Draw
30+
31+
m = folium.Map(location=[39.949610, -75.150282], zoom_start=5, png_enabled=True)
32+
items = folium.FeatureGroup()
33+
marker = folium.Marker(location=[38, -83]).add_to(items)
34+
items.add_to(m)
35+
36+
Draw(export=False, feature_group=items, show_geometry_on_click=False).add_to(m)
37+
38+
c1, c2 = st.columns(2)
39+
with c1:
40+
output = st_folium(m, width=700, height=500)
41+
42+
with c2:
43+
st.write(output)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import streamlit as st
2+
from streamlit_folium import st_folium
3+
4+
import folium
5+
from folium.plugins import Realtime
6+
7+
st.set_page_config(page_title="iss", layout="wide")
8+
9+
m = folium.Map()
10+
11+
source = folium.JsCode(
12+
"""
13+
function(responseHandler, errorHandler) {
14+
var url = 'https://api.wheretheiss.at/v1/satellites/25544';
15+
16+
fetch(url)
17+
.then((response) => {
18+
return response.json().then((data) => {
19+
var { id, timestamp, longitude, latitude } = data;
20+
21+
return {
22+
'type': 'FeatureCollection',
23+
'features': [{
24+
'type': 'Feature',
25+
'geometry': {
26+
'type': 'Point',
27+
'coordinates': [longitude, latitude]
28+
},
29+
'properties': {
30+
'id': id,
31+
'timestamp': timestamp
32+
}
33+
}]
34+
};
35+
})
36+
})
37+
.then(responseHandler)
38+
.catch(errorHandler);
39+
}
40+
"""
41+
)
42+
43+
on_each_feature = folium.JsCode(
44+
"""
45+
(feature, layer) => {
46+
layer.on("click", (event) => {
47+
Streamlit.setComponentValue({
48+
id: feature.properties.id,
49+
// Be careful, on_each_feature binds only once.
50+
// You need to extract the current location from
51+
// the event.
52+
location: event.sourceTarget.feature.geometry
53+
});
54+
});
55+
}
56+
"""
57+
)
58+
59+
realtime = Realtime(source, on_each_feature=on_each_feature, interval=1000).add_to(m)
60+
realtime.on(
61+
update=folium.JsCode(
62+
"""
63+
(e) => {
64+
console.log('update ', e.target._map);
65+
var map = e.target._map;
66+
var realtime = e.target;
67+
map.fitBounds(realtime.getBounds(), {maxZoom: 8});
68+
}
69+
"""
70+
)
71+
)
72+
73+
left, right = st.columns(2)
74+
with left:
75+
data = st_folium(m, width=1000, returned_objects=[], debug=True)
76+
77+
with right:
78+
st.write(data)

tests/playwright/test_playwright.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
from __future__ import annotations
2+
3+
from contextlib import contextmanager
4+
from time import sleep
5+
6+
import pytest
7+
from playwright.sync_api import Page, expect
8+
9+
LOCAL_TEST = False
10+
11+
PORT = "8503" if LOCAL_TEST else "8699"
12+
13+
14+
@pytest.fixture(scope="module", autouse=True)
15+
def _before_module():
16+
# Run the streamlit app before each module
17+
with run_streamlit():
18+
yield
19+
20+
21+
@pytest.fixture(autouse=True)
22+
def _before_test(page: Page):
23+
page.goto(f"localhost:{PORT}")
24+
page.set_viewport_size({"width": 2000, "height": 2000})
25+
expect.set_options(timeout=5_000)
26+
27+
28+
# Take screenshot of each page if there are failures for this session
29+
@pytest.fixture(autouse=True)
30+
def _after_test(page: Page, request):
31+
yield
32+
if request.node.rep_call.failed:
33+
page.screenshot(path=f"screenshot-{request.node.name}.png", full_page=True)
34+
35+
36+
@contextmanager
37+
def run_streamlit():
38+
"""Run the streamlit app at examples/streamlit_app.py on port 8599"""
39+
import subprocess
40+
41+
if LOCAL_TEST:
42+
try:
43+
yield 1
44+
finally:
45+
pass
46+
else:
47+
p = subprocess.Popen(
48+
[
49+
"streamlit",
50+
"run",
51+
"examples/streamlit/main.py",
52+
"--server.port",
53+
PORT,
54+
"--server.headless",
55+
"true",
56+
]
57+
)
58+
59+
sleep(5)
60+
61+
try:
62+
yield 1
63+
finally:
64+
p.kill()
65+
66+
67+
def click_button_or_marker(page: Page, nth: int = 0, locator: str | None = None):
68+
"""For some reason, there's a discrepancy between how the map markers are
69+
selectable locally and on github actions, perhaps related some error in loading
70+
the actual marker images. This tries both ways to select a marker"""
71+
72+
frame = page.frame_locator('iframe[title="streamlit_folium\\.st_folium"]')
73+
if locator is not None:
74+
frame = frame.locator(locator)
75+
try:
76+
frame.get_by_role("button", name="Marker").nth(nth).click(timeout=5_000)
77+
except Exception:
78+
frame.get_by_role("img").nth(nth).click(timeout=5_000)
79+
80+
81+
def test_draw(page: Page):
82+
# Test draw support
83+
page.get_by_role("link", name="draw feature group").click()
84+
85+
# This is the marker that was drawn beforehand
86+
expect(
87+
page.locator('[data-testid="stCustomComponentV1"]').content_frame.get_by_role(
88+
"button", name="Marker"
89+
)
90+
).to_be_visible()
91+
92+
# Start drawing a rectangle
93+
page.locator('[data-testid="stCustomComponentV1"]').content_frame.get_by_role(
94+
"link", name="Draw a rectangle"
95+
).click()
96+
97+
# I could not record this
98+
# so some trickery to get the mouse movements correct
99+
bbox = page.locator('[data-testid="stCustomComponentV1"]').bounding_box()
100+
101+
# One of the few times I miss javascript
102+
x = bbox["x"]
103+
y = bbox["y"]
104+
width = bbox["width"]
105+
height = bbox["height"]
106+
107+
# careful, my first click attempt triggered the zoom button
108+
page.mouse.click(x + 100, y + 100)
109+
page.mouse.click(x + width - 100, y + height - 100)
110+
111+
# Now check if streamlit shows a Polygon result
112+
expect(page.get_by_text('"Polygon"').first).to_be_visible()

0 commit comments

Comments
 (0)