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)" + "![](https://scitools.org.uk/cartopy/docs/latest/_images/sphx_glr_waves_001.png)" ] } ], @@ -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)