diff --git a/.appveyor.yml b/.appveyor.yml
index 0b65ef32f3..58eb4e668a 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -27,4 +27,4 @@ install:
test_script:
- pip install -e . --no-deps --force-reinstall
- - pytest -n 2 -rxXs tests
+ - pytest -n 2 -rxXs tests --ignore=tests/selenium
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 4e820c7ee9..af8fc249cc 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -71,13 +71,13 @@ The basic workflow for contributing is:
pip install -r requirements.txt
pip install -r requirements-dev.txt
```
-6. Install Firefox, download [geckodriver](https://github.com/mozilla/geckodriver/releases)
- and put it in the PATH.
+6. Install Chrome, download [chromedriver](https://sites.google.com/a/chromium.org/chromedriver/) and put it in the PATH.
7. Make changes to your local copy of the folium repository
8. Make sure the tests pass:
* in the repository folder do `pip install -e . --no-deps` (needed for notebook tests)
- * run `python -m pytest tests`
* run `flake8 folium --max-line-length=120`
+ * run `python -m pytest tests --ignore=tests/selenium`
+ * run `python -m pytest tests/selenium`
* resolve all errors
9. Commit those changes
```
diff --git a/.gitignore b/.gitignore
index fdfd03981d..629209ac67 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,11 @@ examples/results/*
/_mac/*.xml
/inspection/*.xml
geckodriver.log
+geckodriver.exe
geckodriver.tar.gz
geckodriver/
+chromedriver.exe
+chromedriver/
miniconda.sh
+latest_logs/
+*.nbconvert.ipynb
diff --git a/.travis.yml b/.travis.yml
index db395c6cbe..3241896a93 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,6 +7,7 @@ env:
addons:
firefox: latest
+ # chrome addon fails to install
env:
global:
@@ -21,6 +22,8 @@ matrix:
env: PY=3.7
- name: notebooks-code
env: PY=3.7
+ - name: selenium
+ env: PY=3.7
- name: latest-branca
env: PY=3.7
- name: docs
@@ -54,7 +57,18 @@ before_install:
mkdir geckodriver
tar -xzf geckodriver.tar.gz -C geckodriver
export PATH=$PATH:$PWD/geckodriver
-
+ # Install Chrome
+ - |
+ wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
+ sudo dpkg -i google-chrome-stable_current_amd64.deb
+ sudo apt-get install -y -f
+ # sudo dpkg -i google-chrome-stable_current_amd64.deb
+ # Install chromedriver
+ - |
+ wget https://chromedriver.storage.googleapis.com/$(curl https://chromedriver.storage.googleapis.com/LATEST_RELEASE)/chromedriver_linux64.zip -O chromedriver.zip
+ mkdir chromedriver
+ unzip chromedriver.zip -d chromedriver
+ export PATH=$PATH:$PWD/chromedriver
# Test source distribution.
install:
@@ -72,13 +86,13 @@ script:
fi
- if [[ $TRAVIS_JOB_NAME == 'default' ]]; then
- pytest /tmp -vv --ignore=tests/notebooks/test_notebooks.py ;
+ pytest /tmp -vv --ignore=/tmp/tests/selenium ;
fi
- if [[ $TRAVIS_JOB_NAME == 'latest-branca' ]]; then
conda uninstall branca ;
pip install git+https://github.com/python-visualization/branca.git ;
- pytest /tmp -vv --ignore=tests/notebooks/test_notebooks.py ;
+ pytest /tmp -vv --ignore=/tmp/tests/selenium ;
fi
- if [[ $TRAVIS_JOB_NAME == 'notebooks-conding-standard' ]]; then
@@ -91,6 +105,10 @@ script:
pytest --nbval-lax /tmp/examples ;
fi
+ - if [[ $TRAVIS_JOB_NAME == 'selenium' ]]; then
+ pytest /tmp/tests/selenium -vv ;
+ fi
+
# Docs
- |
if [[ $TRAVIS_JOB_NAME == 'docs' ]]; then
diff --git a/examples/GeodedeticImageOverlay.ipynb b/examples/GeodedeticImageOverlay.ipynb
index 91e0a07669..3cc3e62b11 100644
--- a/examples/GeodedeticImageOverlay.ipynb
+++ b/examples/GeodedeticImageOverlay.ipynb
@@ -9,7 +9,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "0.5.0+105.g065f6f3.dirty\n"
+ "0.10.1+17.ge9b9b47.dirty\n"
]
}
],
@@ -79,10 +79,10 @@
{
"data": {
"text/html": [
- "
"
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 4,
@@ -119,10 +119,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 5,
@@ -160,10 +160,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 6,
@@ -214,37 +214,12 @@
]
},
{
- "cell_type": "code",
- "execution_count": 7,
+ "cell_type": "markdown",
"metadata": {},
- "outputs": [
- {
- "data": {
- "text/html": [
- "\n",
- " \n",
- " "
- ],
- "text/plain": [
- ""
- ]
- },
- "execution_count": 7,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
"source": [
- "from IPython.display import IFrame\n",
+ "From https://scitools.org.uk/cartopy/docs/latest/gallery/waves.html\n",
"\n",
- "url = 'http://scitools.org.uk/cartopy/docs/latest/gallery/waves.html'\n",
- "IFrame(url, width=900, height=750)"
+ ""
]
}
],
@@ -264,7 +239,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.6.5"
+ "version": "3.7.6"
}
},
"nbformat": 4,
diff --git a/examples/Plugins.ipynb b/examples/Plugins.ipynb
index 077c425263..c190dd1040 100644
--- a/examples/Plugins.ipynb
+++ b/examples/Plugins.ipynb
@@ -16,7 +16,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "0.8.3+52.g2758dc7.dirty\n"
+ "0.10.1+22.gc481461.dirty\n"
]
}
],
@@ -54,10 +54,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 2,
@@ -97,10 +97,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 3,
@@ -141,10 +141,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 4,
@@ -176,10 +176,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 5,
@@ -225,10 +225,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 6,
@@ -282,10 +282,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 7,
@@ -322,10 +322,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 8,
@@ -417,21 +417,21 @@
},
{
"cell_type": "code",
- "execution_count": 9,
+ "execution_count": 15,
"metadata": {
- "scrolled": true
+ "scrolled": false
},
"outputs": [
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
- "execution_count": 9,
+ "execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
@@ -496,7 +496,7 @@
" 'id': 'house',\n",
" 'icon': 'marker',\n",
" 'iconstyle': {\n",
- " 'iconUrl': 'http://downloadicons.net/sites/default/files/small-house-with-a-chimney-icon-70053.png',\n",
+ " 'iconUrl': 'https://leafletjs.com/examples/geojson/baseball-marker.png',\n",
" 'iconSize': [20, 20]\n",
" }\n",
" }\n",
@@ -583,10 +583,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 10,
@@ -634,10 +634,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 11,
@@ -685,10 +685,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 12,
@@ -726,10 +726,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 13,
@@ -779,10 +779,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 14,
@@ -818,7 +818,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.7.3"
+ "version": "3.7.6"
}
},
"nbformat": 4,
diff --git a/examples/Quickstart.ipynb b/examples/Quickstart.ipynb
index a5f0928cac..ba10220044 100644
--- a/examples/Quickstart.ipynb
+++ b/examples/Quickstart.ipynb
@@ -42,10 +42,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 2,
@@ -88,10 +88,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 4,
@@ -102,7 +102,6 @@
"source": [
"folium.Map(\n",
" location=[45.5236, -122.6750],\n",
- " tiles='Stamen Toner',\n",
" zoom_start=13\n",
")"
]
@@ -148,10 +147,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 5,
@@ -189,10 +188,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 6,
@@ -244,10 +243,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 7,
@@ -298,10 +297,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 8,
@@ -336,10 +335,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 9,
@@ -401,10 +400,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 11,
@@ -460,7 +459,7 @@
},
{
"cell_type": "code",
- "execution_count": 12,
+ "execution_count": 20,
"metadata": {
"scrolled": false
},
@@ -468,13 +467,13 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
- "execution_count": 12,
+ "execution_count": 20,
"metadata": {},
"output_type": "execute_result"
}
@@ -487,8 +486,8 @@
"\n",
"m = folium.Map(\n",
" location=[-59.1759, -11.6016],\n",
- " tiles='Mapbox Bright',\n",
- " zoom_start=2 # Limited levels of zoom for free Mapbox tiles.\n",
+ " tiles='cartodbpositron',\n",
+ " zoom_start=2,\n",
")\n",
"\n",
"folium.GeoJson(\n",
@@ -525,10 +524,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 13,
@@ -580,10 +579,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 14,
@@ -637,10 +636,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 15,
@@ -696,10 +695,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 16,
@@ -746,10 +745,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 17,
@@ -814,7 +813,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.7.3"
+ "version": "3.7.6"
},
"toc": {
"base_numbering": 1,
diff --git a/examples/SmoothFactor.ipynb b/examples/SmoothFactor.ipynb
index 4020b3d2ac..9e289dd5d1 100644
--- a/examples/SmoothFactor.ipynb
+++ b/examples/SmoothFactor.ipynb
@@ -9,7 +9,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "0.8.3+52.g2758dc7.dirty\n"
+ "0.10.1+20.gcc3eea0.dirty\n"
]
}
],
@@ -41,10 +41,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 2,
@@ -59,7 +59,7 @@
"\n",
"m = folium.Map(\n",
" location=[-59.1759, -11.6016],\n",
- " tiles='Mapbox Bright',\n",
+ " tiles='cartodbpositron',\n",
" zoom_start=2\n",
")\n",
"\n",
@@ -110,7 +110,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.7.3"
+ "version": "3.7.6"
}
},
"nbformat": 4,
diff --git a/examples/TilesExample.ipynb b/examples/TilesExample.ipynb
index 36d2ec0cf2..af198497dc 100644
--- a/examples/TilesExample.ipynb
+++ b/examples/TilesExample.ipynb
@@ -9,7 +9,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "0.5.0+27.g2d457b0.dirty\n"
+ "0.10.1+20.gcc3eea0.dirty\n"
]
}
],
@@ -39,10 +39,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 3,
@@ -58,60 +58,6 @@
"m"
]
},
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/html": [
- ""
- ],
- "text/plain": [
- ""
- ]
- },
- "execution_count": 4,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "m = folium.Map(location=[lat, lon], tiles='Mapbox Bright', zoom_start=zoom_start)\n",
- "\n",
- "m.save(os.path.join('results', 'TilesExample_1.html'))\n",
- "\n",
- "m"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/html": [
- ""
- ],
- "text/plain": [
- ""
- ]
- },
- "execution_count": 5,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "m = folium.Map(location=[lat, lon], tiles='Mapbox Control Room', zoom_start=zoom_start)\n",
- "\n",
- "m.save(os.path.join('results', 'TilesExample_2.html'))\n",
- "\n",
- "m"
- ]
- },
{
"cell_type": "code",
"execution_count": 6,
@@ -120,10 +66,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 6,
@@ -147,10 +93,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 7,
@@ -174,10 +120,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 8,
@@ -201,10 +147,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 9,
@@ -228,10 +174,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 10,
@@ -262,10 +208,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 11,
@@ -289,40 +235,14 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "### There are plenty tile sources to choose from:"
+ "### There are plenty tile sources to choose from"
]
},
{
- "cell_type": "code",
- "execution_count": 12,
+ "cell_type": "markdown",
"metadata": {},
- "outputs": [
- {
- "data": {
- "text/html": [
- "\n",
- " \n",
- " "
- ],
- "text/plain": [
- ""
- ]
- },
- "execution_count": 12,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
"source": [
- "from IPython.display import IFrame\n",
- "\n",
- "IFrame('http://leaflet-extras.github.io/leaflet-providers/preview/', width=900, height=750)"
+ "For a list of many more tile providers go to http://leaflet-extras.github.io/leaflet-providers/preview/"
]
}
],
@@ -343,7 +263,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.6.3"
+ "version": "3.7.6"
}
},
"nbformat": 4,
diff --git a/examples/plugin-PolyLineOffset.ipynb b/examples/plugin-PolyLineOffset.ipynb
index c048507b28..50cc3310d9 100644
--- a/examples/plugin-PolyLineOffset.ipynb
+++ b/examples/plugin-PolyLineOffset.ipynb
@@ -9,7 +9,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "0.8.3+52.g2758dc7.dirty\n"
+ "0.10.1+22.gc481461.dirty\n"
]
}
],
@@ -52,10 +52,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 2,
@@ -67,7 +67,7 @@
"from folium import plugins\n",
"\n",
"\n",
- "m = folium.Map(location=[58.0, -11.0], zoom_start=4, tiles=\"Mapbox Bright\")\n",
+ "m = folium.Map(location=[58.0, -11.0], zoom_start=4, tiles=\"cartodbpositron\")\n",
"\n",
"coords = [\n",
" [58.44773, -28.65234],\n",
@@ -122,10 +122,10 @@
{
"data": {
"text/html": [
- ""
+ ""
],
"text/plain": [
- ""
+ ""
]
},
"execution_count": 3,
@@ -304,7 +304,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.7.3"
+ "version": "3.7.6"
}
},
"nbformat": 4,
diff --git a/tests/selenium/test_selenium.py b/tests/selenium/test_selenium.py
new file mode 100644
index 0000000000..3cae774ed1
--- /dev/null
+++ b/tests/selenium/test_selenium.py
@@ -0,0 +1,123 @@
+
+import base64
+import glob
+from html.parser import HTMLParser
+import os
+import subprocess
+
+import nbconvert
+import pytest
+from selenium.webdriver import Chrome, ChromeOptions
+from selenium.common.exceptions import UnexpectedAlertPresentException
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support.expected_conditions import visibility_of_element_located
+
+
+def create_driver():
+ """Create a Selenium WebDriver instance."""
+ options = ChromeOptions()
+ options.add_argument('--no-sandbox')
+ options.add_argument('--disable-dev-shm-usage')
+ options.add_argument('--disable-gpu')
+ options.add_argument('--headless')
+ driver = Chrome(options=options)
+ return driver
+
+
+@pytest.fixture(scope='module')
+def driver():
+ """Pytest fixture that yields a Selenium WebDriver instance"""
+ driver = create_driver()
+ try:
+ yield driver
+ finally:
+ driver.quit()
+
+
+def clean_window(driver):
+ # open new tab
+ driver.execute_script('window.open();')
+ # close old tab
+ driver.close()
+ # switch to new tab
+ driver.switch_to.window(driver.window_handles[0])
+
+
+def find_notebooks():
+ """Return a list of filenames of the example notebooks."""
+ path = os.path.dirname(__file__)
+ pattern = os.path.join(path, '..', '..', 'examples', '*.ipynb')
+ files = glob.glob(pattern)
+ files = [f for f in files if not f.endswith('.nbconvert.ipynb')]
+ if files:
+ return files
+ else:
+ raise IOError('Could not find the notebooks')
+
+
+@pytest.mark.parametrize('filepath', find_notebooks())
+def test_notebook(filepath, driver):
+ for filepath_html in get_notebook_html(filepath):
+ clean_window(driver)
+ driver.get('file://' + filepath_html)
+ wait = WebDriverWait(driver, timeout=10)
+ map_is_visible = visibility_of_element_located((By.CSS_SELECTOR, '.folium-map'))
+ try:
+ assert wait.until(map_is_visible)
+ except UnexpectedAlertPresentException:
+ # in Plugins.ipynb we get an alert about geolocation permission
+ # for some reason it cannot be closed or avoided, so just ignore it
+ print('skipping', filepath_html, 'because of alert')
+ continue
+ logs = driver.get_log('browser')
+ for log in logs:
+ if log['level'] == 'SEVERE':
+ msg = ' '.join(log['message'].split()[2:])
+ raise RuntimeError('Javascript error: "{}".'.format(msg))
+
+
+def get_notebook_html(filepath_notebook, execute=True):
+ """Store iframes from a notebook in html files, remove them when done."""
+ if execute:
+ subprocess.run([
+ 'jupyter', 'nbconvert', '--to', 'notebook', '--execute', filepath_notebook,
+ ])
+ filepath_notebook = filepath_notebook.replace('.ipynb', '.nbconvert.ipynb')
+
+ html_exporter = nbconvert.HTMLExporter()
+ html_exporter.template_file = 'basic'
+ body, _ = html_exporter.from_filename(filepath_notebook)
+
+ parser = IframeParser()
+ parser.feed(body)
+ iframes = parser.iframes
+
+ for i, iframe in enumerate(iframes):
+ filepath_html = filepath_notebook.replace('.ipynb', '.{}.html'.format(i))
+ filepath_html = os.path.abspath(filepath_html)
+ with open(filepath_html, 'wb') as f:
+ f.write(iframe)
+ try:
+ yield filepath_html
+ finally:
+ os.remove(filepath_html)
+
+
+class IframeParser(HTMLParser):
+ """Extract the iframes from an html page."""
+
+ def __init__(self):
+ super().__init__()
+ self.iframes = []
+
+ def handle_starttag(self, tag, attrs):
+ if tag == 'iframe':
+ attrs = dict(attrs)
+ if 'data-html' in attrs:
+ html_base64 = attrs['data-html']
+ else: # legacy, can be removed when all notebooks have `data-html`.
+ src = attrs['src']
+ html_base64 = src.split(',')[-1]
+ html_bytes = base64.b64decode(html_base64)
+ self.iframes.append(html_bytes)