From 0023ec6c5fa9d3ff5bbfb3cf024dbd6647fd90c4 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 2 Aug 2022 22:23:03 -0400 Subject: [PATCH 1/8] Add general test for WebAgg backend --- .appveyor.yml | 2 + .github/workflows/tests.yml | 3 + azure-pipelines.yml | 3 + environment.yml | 1 + .../test_backend_webagg/chromium.png | Bin 0 -> 3191 bytes .../test_backend_webagg/firefox.png | Bin 0 -> 3170 bytes .../test_backend_webagg/webkit.png | Bin 0 -> 3855 bytes lib/matplotlib/tests/test_backend_webagg.py | 68 +++++++++++++++++- pyproject.toml | 1 + 9 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 lib/matplotlib/tests/baseline_images/test_backend_webagg/chromium.png create mode 100644 lib/matplotlib/tests/baseline_images/test_backend_webagg/firefox.png create mode 100644 lib/matplotlib/tests/baseline_images/test_backend_webagg/webkit.png diff --git a/.appveyor.yml b/.appveyor.yml index 10109c9f80f7..499745279458 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -63,6 +63,8 @@ install: - micromamba env create -f environment.yml python=%PYTHON_VERSION% %EXTRA_PACKAGES% - micromamba activate mpl-dev + # Install browsers for testing + - playwright install --with-deps build_script: # Now build the thing.. diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bf75c80ddae0..3615f5babfbd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -288,6 +288,9 @@ jobs: --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \ --upgrade --only-binary=:all: numpy pandas + - name: Install browsers for testing + run: playwright install --with-deps + - name: Install Matplotlib run: | ccache -s diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c8df751f2419..678e49b114d3 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -85,6 +85,9 @@ stages: --verbose --editable .[dev] displayName: "Install self" + - bash: playwright install --with-deps + displayName: 'Install browsers for testing' + - script: env displayName: 'print env' diff --git a/environment.yml b/environment.yml index 573e93823f88..7d4480aa564c 100644 --- a/environment.yml +++ b/environment.yml @@ -65,6 +65,7 @@ dependencies: - pydocstyle>=5.1.0 - pytest!=4.6.0,!=5.4.0,!=8.1.0 - pytest-cov + - pytest-playwright - pytest-rerunfailures - pytest-timeout - pytest-xdist diff --git a/lib/matplotlib/tests/baseline_images/test_backend_webagg/chromium.png b/lib/matplotlib/tests/baseline_images/test_backend_webagg/chromium.png new file mode 100644 index 0000000000000000000000000000000000000000..d172a625ad0630d1a54c1e1e267daa5193616a56 GIT binary patch literal 3191 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 literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/baseline_images/test_backend_webagg/firefox.png b/lib/matplotlib/tests/baseline_images/test_backend_webagg/firefox.png new file mode 100644 index 0000000000000000000000000000000000000000..f22ac6dd6352688b38b3ceaab96d93f5c580661e GIT binary patch literal 3170 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 jq@&KCm!SJ!4Y;}6PDL+zY0W6Z0cQ<dvt? literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_backend_webagg.py b/lib/matplotlib/tests/test_backend_webagg.py index c63534ad20e3..c52329cdf629 100644 --- a/lib/matplotlib/tests/test_backend_webagg.py +++ b/lib/matplotlib/tests/test_backend_webagg.py @@ -1,7 +1,10 @@ +import io import os +import shutil import sys from unittest.mock import MagicMock +from PIL import Image import pytest import matplotlib.backends.backend_webagg_core @@ -10,10 +13,26 @@ ) from matplotlib.testing import subprocess_run_for_testing +import matplotlib.pyplot as plt +from matplotlib.backends.backend_webagg import WebAggApplication +from matplotlib.testing.compare import compare_images +from matplotlib.testing.decorators import _image_directories +from matplotlib.testing.exceptions import ImageComparisonFailure + + +pytest.importorskip('tornado') + + +try: + import pytest_playwright # noqa +except ImportError: + @pytest.fixture + def page(): + pytest.skip(reason='Missing pytest-playwright') + @pytest.mark.parametrize("backend", ["webagg", "nbagg"]) def test_webagg_fallback(backend): - pytest.importorskip("tornado") if backend == "nbagg": pytest.importorskip("IPython") env = dict(os.environ) @@ -71,10 +90,55 @@ def test_toolbar_button_dispatch_allowlist(): ]) def test_websocket_rejects_cross_origin(host, origin, allowed): """Verify Tornado's default check_origin rejects cross-origin requests.""" - pytest.importorskip("tornado") from matplotlib.backends.backend_webagg import WebAggApplication ws = WebAggApplication.WebSocket.__new__(WebAggApplication.WebSocket) ws.request = MagicMock() ws.request.headers = {"Host": host} assert ws.check_origin(origin) is allowed + + +@pytest.mark.backend('webagg') +def test_webagg_general(page): + from playwright.sync_api import expect + + # Listen for all console logs. + page.on('console', lambda msg: print(f'CONSOLE: {msg.text}')) + + fig, ax = plt.subplots(facecolor='w') + + # Don't start the Tornado event loop, but use the existing event loop + # started by the `page` fixture. + WebAggApplication.initialize() + WebAggApplication.started = True + + page.goto(f'http://{WebAggApplication.address}:{WebAggApplication.port}/') + expect(page).to_have_title('MPL | WebAgg current figures') + + # Check title. + expect(page.locator('div.ui-dialog-title')).to_have_text('Figure 1') + + # Check toolbar buttons. + assert page.locator('button.mpl-widget').count() == len([ + name for name, *_ in fig.canvas.manager.ToolbarCls.toolitems + if name is not None]) + + # Check canvas actually contains something. + baseline_dir, result_dir = _image_directories(test_webagg_general) + browser = page.context.browser.browser_type.name + actual = result_dir / f'{browser}.png' + expected = result_dir / f'{browser}-expected.png' + + canvas = page.locator('canvas.mpl-canvas') + actual_bytes = canvas.screenshot() + im = Image.open(io.BytesIO(actual_bytes)) + # Hide the resize grip, which varies across OS/browser. + if browser == 'firefox': + im.paste((255, 255, 255), + box=(im.width - 20, im.height - 20, im.width, im.height)) + im.save(actual) + shutil.copyfile(baseline_dir / f'{browser}.png', expected) + + err = compare_images(expected, actual, tol=0) + if err: + raise ImageComparisonFailure(err) diff --git a/pyproject.toml b/pyproject.toml index a487d45e27d6..46192cd5a135 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -131,6 +131,7 @@ test = [ "psutil; sys_platform != 'cygwin'", "pytest!=4.6.0,!=5.4.0,!=8.1.0", "pytest-cov", + "pytest-playwright", "pytest-rerunfailures!=16.0", "pytest-timeout", "pytest-xdist", From e9490ad006a82b5c53594c0bfc6e5dd357cbad75 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 2 Aug 2022 23:11:01 -0400 Subject: [PATCH 2/8] WebAgg: Add test for the toolbar --- lib/matplotlib/tests/test_backend_webagg.py | 65 +++++++++++++++++++-- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/tests/test_backend_webagg.py b/lib/matplotlib/tests/test_backend_webagg.py index c52329cdf629..bdda6669355a 100644 --- a/lib/matplotlib/tests/test_backend_webagg.py +++ b/lib/matplotlib/tests/test_backend_webagg.py @@ -118,11 +118,6 @@ def test_webagg_general(page): # Check title. expect(page.locator('div.ui-dialog-title')).to_have_text('Figure 1') - # Check toolbar buttons. - assert page.locator('button.mpl-widget').count() == len([ - name for name, *_ in fig.canvas.manager.ToolbarCls.toolitems - if name is not None]) - # Check canvas actually contains something. baseline_dir, result_dir = _image_directories(test_webagg_general) browser = page.context.browser.browser_type.name @@ -142,3 +137,63 @@ def test_webagg_general(page): err = compare_images(expected, actual, tol=0) if err: raise ImageComparisonFailure(err) + + +@pytest.mark.filterwarnings('ignore:Treat the new Tool classes:UserWarning') +@pytest.mark.backend('webagg') +@pytest.mark.parametrize('toolbar', ['toolbar2', 'toolmanager']) +def test_webagg_toolbar(page, toolbar): + from playwright.sync_api import expect + + # Listen for all console logs. + page.on('console', lambda msg: print(f'CONSOLE: {msg.text}')) + + plt.rcParams['toolbar'] = toolbar + + fig, ax = plt.subplots(facecolor='w') + + # Don't start the Tornado event loop, but use the existing event loop + # started by the `page` fixture. + WebAggApplication.initialize() + WebAggApplication.started = True + + page.goto(f'http://{WebAggApplication.address}:{WebAggApplication.port}/') + + expect(page.locator('button.mpl-widget')).to_have_count( + len([ + name for name, *_ in fig.canvas.manager.ToolbarCls.toolitems + if name is not None])) + + home = page.locator('button.mpl-widget').nth(0) + expect(home).to_be_visible() + + back = page.locator('button.mpl-widget').nth(1) + expect(back).to_be_visible() + forward = page.locator('button.mpl-widget').nth(2) + expect(forward).to_be_visible() + if toolbar == 'toolbar2': + # ToolManager doesn't implement history button disabling. + # https://github.com/matplotlib/matplotlib/issues/17979 + expect(back).to_be_disabled() + expect(forward).to_be_disabled() + + pan = page.locator('button.mpl-widget').nth(3) + expect(pan).to_be_visible() + zoom = page.locator('button.mpl-widget').nth(4) + expect(zoom).to_be_visible() + + save = page.locator('button.mpl-widget').nth(5) + expect(save).to_be_visible() + format_dropdown = page.locator('select.mpl-widget') + expect(format_dropdown).to_be_visible() + + if toolbar == 'toolmanager': + # Location in status bar is not supported by ToolManager. + return + + ax.set_position([0, 0, 1, 1]) + bbox = page.locator('canvas.mpl-canvas').bounding_box() + x, y = bbox['x'] + bbox['width'] / 2, bbox['y'] + bbox['height'] / 2 + page.mouse.move(x, y, steps=2) + message = page.locator('span.mpl-message') + expect(message).to_have_text('(x, y) = (0.500, 0.500)') From b85c1dd647a571d5abf8c743a602ac8f2b0b090a Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 2 Aug 2022 23:24:03 -0400 Subject: [PATCH 3/8] WebAgg: Add test for toolbar save button --- lib/matplotlib/tests/test_backend_webagg.py | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/matplotlib/tests/test_backend_webagg.py b/lib/matplotlib/tests/test_backend_webagg.py index bdda6669355a..4c9dbf7451f9 100644 --- a/lib/matplotlib/tests/test_backend_webagg.py +++ b/lib/matplotlib/tests/test_backend_webagg.py @@ -197,3 +197,30 @@ def test_webagg_toolbar(page, toolbar): page.mouse.move(x, y, steps=2) message = page.locator('span.mpl-message') expect(message).to_have_text('(x, y) = (0.500, 0.500)') + + +@pytest.mark.backend('webagg') +def test_webagg_toolbar_save(page): + from playwright.sync_api import expect + + # Listen for all console logs. + page.on('console', lambda msg: print(f'CONSOLE: {msg.text}')) + + fig, ax = plt.subplots(facecolor='w') + + # Don't start the Tornado event loop, but use the existing event loop + # started by the `page` fixture. + WebAggApplication.initialize() + WebAggApplication.started = True + + page.goto(f'http://{WebAggApplication.address}:{WebAggApplication.port}/') + + save = page.locator('button.mpl-widget').nth(5) + expect(save).to_be_visible() + + with page.context.expect_page() as new_page_info: + save.click() + new_page = new_page_info.value + + new_page.wait_for_load_state() + assert new_page.url.endswith('download.png') From 6c897118f9e121aa180ba86e59297ed7474945fa Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 3 Aug 2022 02:01:01 -0400 Subject: [PATCH 4/8] WebAgg: Add a test for resizing the figure --- lib/matplotlib/tests/test_backend_webagg.py | 33 +++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/lib/matplotlib/tests/test_backend_webagg.py b/lib/matplotlib/tests/test_backend_webagg.py index 4c9dbf7451f9..8af597b8a946 100644 --- a/lib/matplotlib/tests/test_backend_webagg.py +++ b/lib/matplotlib/tests/test_backend_webagg.py @@ -139,6 +139,39 @@ def test_webagg_general(page): raise ImageComparisonFailure(err) +@pytest.mark.backend('webagg') +def test_webagg_resize(page): + # Listen for all console logs. + page.on('console', lambda msg: print(f'CONSOLE: {msg.text}')) + # Increase the viewport to at least twice the size of the default figure. + # Playwright seems to have a bug with mouse movement in Firefox that places the + # cursor somewhere near the origin when outside the viewport size. + page.set_viewport_size({'width': 1600, 'height': 1200}) + + fig, ax = plt.subplots(facecolor='w') + orig_bbox = fig.bbox.frozen() + + # Don't start the Tornado event loop, but use the existing event loop + # started by the `page` fixture. + WebAggApplication.initialize() + WebAggApplication.started = True + + page.goto(f'http://{WebAggApplication.address}:{WebAggApplication.port}/') + + canvas = page.locator('canvas.mpl-canvas') + + # Resize the canvas to be twice as big. + bbox = canvas.bounding_box() + x, y = bbox['x'] + bbox['width'] - 1, bbox['y'] + bbox['height'] - 1 + page.mouse.move(x, y) + page.mouse.down() + page.mouse.move(x + bbox['width'], y + bbox['height']) + page.mouse.up() + + assert fig.bbox.height == orig_bbox.height * 2 + assert fig.bbox.width == orig_bbox.width * 2 + + @pytest.mark.filterwarnings('ignore:Treat the new Tool classes:UserWarning') @pytest.mark.backend('webagg') @pytest.mark.parametrize('toolbar', ['toolbar2', 'toolmanager']) From 9ae8944788b93de86ebefb1dc15cba250cdd1803 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 3 Aug 2022 04:10:17 -0400 Subject: [PATCH 5/8] WebAgg: Add tests for pan/zoom tool --- lib/matplotlib/tests/test_backend_webagg.py | 116 ++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/lib/matplotlib/tests/test_backend_webagg.py b/lib/matplotlib/tests/test_backend_webagg.py index 8af597b8a946..ca8b1145975d 100644 --- a/lib/matplotlib/tests/test_backend_webagg.py +++ b/lib/matplotlib/tests/test_backend_webagg.py @@ -1,5 +1,6 @@ import io import os +import re import shutil import sys from unittest.mock import MagicMock @@ -257,3 +258,118 @@ def test_webagg_toolbar_save(page): new_page.wait_for_load_state() assert new_page.url.endswith('download.png') + + +@pytest.mark.backend('webagg') +def test_webagg_toolbar_pan(page): + from playwright.sync_api import expect + + # Listen for all console logs. + page.on('console', lambda msg: print(f'CONSOLE: {msg.text}')) + + fig, ax = plt.subplots(facecolor='w') + ax.plot([3, 2, 1]) + orig_lim = ax.viewLim.frozen() + # Make figure coords ~= axes coords, with ticks visible for inspection. + ax.set_position([0, 0, 1, 1]) + ax.tick_params(axis='y', direction='in', pad=-22) + ax.tick_params(axis='x', direction='in', pad=-15) + + # Don't start the Tornado event loop, but use the existing event loop + # started by the `page` fixture. + WebAggApplication.initialize() + WebAggApplication.started = True + + page.goto(f'http://{WebAggApplication.address}:{WebAggApplication.port}/') + + canvas = page.locator('canvas.mpl-canvas') + expect(canvas).to_be_visible() + home = page.locator('button.mpl-widget').nth(0) + expect(home).to_be_visible() + pan = page.locator('button.mpl-widget').nth(3) + expect(pan).to_be_visible() + zoom = page.locator('button.mpl-widget').nth(4) + expect(zoom).to_be_visible() + + active_re = re.compile(r'active') + expect(pan).not_to_have_class(active_re) + expect(zoom).not_to_have_class(active_re) + assert ax.get_navigate_mode() is None + pan.click() + expect(pan).to_have_class(active_re) + expect(zoom).not_to_have_class(active_re) + assert ax.get_navigate_mode() == 'PAN' + + # Pan 50% of the figure diagonally toward bottom-right. + bbox = canvas.bounding_box() + x, y = bbox['x'] + bbox['width'] / 4, bbox['y'] + bbox['height'] / 4 + page.mouse.move(x, y) + page.mouse.down() + page.mouse.move(x + bbox['width'] / 2, y + bbox['height'] / 2, + steps=20) + page.mouse.up() + + assert ax.get_xlim() == (orig_lim.x0 - orig_lim.width / 2, + orig_lim.x1 - orig_lim.width / 2) + assert ax.get_ylim() == (orig_lim.y0 + orig_lim.height / 2, + orig_lim.y1 + orig_lim.height / 2) + + # Reset. + home.click() + assert ax.viewLim.bounds == orig_lim.bounds + + # Pan 50% of the figure diagonally toward bottom-right, while holding 'x' + # key, to constrain the pan horizontally. + bbox = canvas.bounding_box() + x, y = bbox['x'] + bbox['width'] / 4, bbox['y'] + bbox['height'] / 4 + page.mouse.move(x, y) + page.mouse.down() + page.keyboard.down('x') + page.mouse.move(x + bbox['width'] / 2, y + bbox['height'] / 2, + steps=20) + page.mouse.up() + page.keyboard.up('x') + + assert ax.get_xlim() == (orig_lim.x0 - orig_lim.width / 2, + orig_lim.x1 - orig_lim.width / 2) + assert ax.get_ylim() == (orig_lim.y0, orig_lim.y1) + + # Reset. + home.click() + assert ax.viewLim.bounds == orig_lim.bounds + + # Pan 50% of the figure diagonally toward bottom-right, while holding 'y' + # key, to constrain the pan vertically. + bbox = canvas.bounding_box() + x, y = bbox['x'] + bbox['width'] / 4, bbox['y'] + bbox['height'] / 4 + page.mouse.move(x, y) + page.mouse.down() + page.keyboard.down('y') + page.mouse.move(x + bbox['width'] / 2, y + bbox['height'] / 2, + steps=20) + page.mouse.up() + page.keyboard.up('y') + + assert ax.get_xlim() == (orig_lim.x0, orig_lim.x1) + assert ax.get_ylim() == (orig_lim.y0 + orig_lim.height / 2, + orig_lim.y1 + orig_lim.height / 2) + + # Reset. + home.click() + assert ax.viewLim.bounds == orig_lim.bounds + + # Zoom 50% of the figure diagonally toward bottom-right. + bbox = canvas.bounding_box() + x, y = bbox['x'], bbox['y'] + page.mouse.move(x, y) + page.mouse.down(button='right') + page.mouse.move(x + bbox['width'] / 2, y + bbox['height'] / 2, + steps=20) + page.mouse.up(button='right') + + # Expands in x-direction. + assert ax.viewLim.x0 == orig_lim.x0 + assert ax.viewLim.x1 < orig_lim.x1 - orig_lim.width / 2 + # Contracts in y-direction. + assert ax.viewLim.y1 == orig_lim.y1 + assert ax.viewLim.y0 < orig_lim.y0 - orig_lim.height / 2 From 50d258b36f7becfee24b5146deaf89f60bf865fb Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 3 Aug 2022 04:21:10 -0400 Subject: [PATCH 6/8] WebAgg: Add tests of the zoom-to-rect tool --- lib/matplotlib/tests/test_backend_webagg.py | 115 ++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/lib/matplotlib/tests/test_backend_webagg.py b/lib/matplotlib/tests/test_backend_webagg.py index ca8b1145975d..5fbcbd19dd88 100644 --- a/lib/matplotlib/tests/test_backend_webagg.py +++ b/lib/matplotlib/tests/test_backend_webagg.py @@ -373,3 +373,118 @@ def test_webagg_toolbar_pan(page): # Contracts in y-direction. assert ax.viewLim.y1 == orig_lim.y1 assert ax.viewLim.y0 < orig_lim.y0 - orig_lim.height / 2 + + +@pytest.mark.backend('webagg') +def test_webagg_toolbar_zoom(page): + from playwright.sync_api import expect + + # Listen for all console logs. + page.on('console', lambda msg: print(f'CONSOLE: {msg.text}')) + + fig, ax = plt.subplots(facecolor='w') + ax.plot([3, 2, 1]) + orig_lim = ax.viewLim.frozen() + # Make figure coords ~= axes coords, with ticks visible for inspection. + ax.set_position([0, 0, 1, 1]) + ax.tick_params(axis='y', direction='in', pad=-22) + ax.tick_params(axis='x', direction='in', pad=-15) + + # Don't start the Tornado event loop, but use the existing event loop + # started by the `page` fixture. + WebAggApplication.initialize() + WebAggApplication.started = True + + page.goto(f'http://{WebAggApplication.address}:{WebAggApplication.port}/') + + canvas = page.locator('canvas.mpl-canvas') + expect(canvas).to_be_visible() + home = page.locator('button.mpl-widget').nth(0) + expect(home).to_be_visible() + pan = page.locator('button.mpl-widget').nth(3) + expect(pan).to_be_visible() + zoom = page.locator('button.mpl-widget').nth(4) + expect(zoom).to_be_visible() + + active_re = re.compile(r'active') + expect(pan).not_to_have_class(active_re) + expect(zoom).not_to_have_class(active_re) + assert ax.get_navigate_mode() is None + zoom.click() + expect(pan).not_to_have_class(active_re) + expect(zoom).to_have_class(active_re) + assert ax.get_navigate_mode() == 'ZOOM' + + # Zoom 25% in on each side. + bbox = canvas.bounding_box() + x, y = bbox['x'] + bbox['width'] / 4, bbox['y'] + bbox['height'] / 4 + page.mouse.move(x, y) + page.mouse.down() + page.mouse.move(x + bbox['width'] / 2, y + bbox['height'] / 2, + steps=20) + page.mouse.up() + + assert ax.get_xlim() == (orig_lim.x0 + orig_lim.width / 4, + orig_lim.x1 - orig_lim.width / 4) + assert ax.get_ylim() == (orig_lim.y0 + orig_lim.height / 4, + orig_lim.y1 - orig_lim.height / 4) + + # Reset. + home.click() + + # Zoom 25% in on each side, while holding 'x' key, to constrain the zoom + # horizontally.. + bbox = canvas.bounding_box() + x, y = bbox['x'] + bbox['width'] / 4, bbox['y'] + bbox['height'] / 4 + page.mouse.move(x, y) + page.mouse.down() + page.keyboard.down('x') + page.mouse.move(x + bbox['width'] / 2, y + bbox['height'] / 2, + steps=20) + page.mouse.up() + page.keyboard.up('x') + + assert ax.get_xlim() == (orig_lim.x0 + orig_lim.width / 4, + orig_lim.x1 - orig_lim.width / 4) + assert ax.get_ylim() == (orig_lim.y0, orig_lim.y1) + + # Reset. + home.click() + + # Zoom 25% in on each side, while holding 'y' key, to constrain the zoom + # vertically. + bbox = canvas.bounding_box() + x, y = bbox['x'] + bbox['width'] / 4, bbox['y'] + bbox['height'] / 4 + page.mouse.move(x, y) + page.mouse.down() + page.keyboard.down('y') + page.mouse.move(x + bbox['width'] / 2, y + bbox['height'] / 2, + steps=20) + page.mouse.up() + page.keyboard.up('y') + + assert ax.get_xlim() == (orig_lim.x0, orig_lim.x1) + assert ax.get_ylim() == (orig_lim.y0 + orig_lim.height / 4, + orig_lim.y1 - orig_lim.height / 4) + + # Reset. + home.click() + + # Zoom 25% out on each side. + bbox = canvas.bounding_box() + x, y = bbox['x'] + bbox['width'] / 4, bbox['y'] + bbox['height'] / 4 + page.mouse.move(x, y) + page.mouse.down(button='right') + page.mouse.move(x + bbox['width'] / 2, y + bbox['height'] / 2, + steps=20) + page.mouse.up(button='right') + + # Limits were doubled, but based on the central point. + cx = orig_lim.x0 + orig_lim.width / 2 + x0 = cx - orig_lim.width + x1 = cx + orig_lim.width + assert ax.get_xlim() == (x0, x1) + cy = orig_lim.y0 + orig_lim.height / 2 + y0 = cy - orig_lim.height + y1 = cy + orig_lim.height + assert ax.get_ylim() == (y0, y1) From 7ebdb057433e1267938d7dfe9400b04002e10946 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 4 Oct 2022 04:07:10 -0400 Subject: [PATCH 7/8] ci: Explicitly ask for all browsers --- .github/workflows/tests.yml | 7 ++++--- azure-pipelines.yml | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3615f5babfbd..1c05f95e850e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -212,10 +212,10 @@ jobs: ~/.cache/matplotlib !~/.cache/matplotlib/tex.cache !~/.cache/matplotlib/test_cache - key: 6-${{ matrix.os }}-py${{ matrix.python-version }}-mpl-${{ github.ref }}-${{ github.sha }} + key: 7-${{ matrix.os }}-py${{ matrix.python-version }}-mpl-${{ github.ref }}-${{ github.sha }} restore-keys: | - 6-${{ matrix.os }}-py${{ matrix.python-version }}-mpl-${{ github.ref }}- - 6-${{ matrix.os }}-py${{ matrix.python-version }}-mpl- + 7-${{ matrix.os }}-py${{ matrix.python-version }}-mpl-${{ github.ref }}- + 7-${{ matrix.os }}-py${{ matrix.python-version }}-mpl- - name: Install Python dependencies run: | @@ -324,6 +324,7 @@ jobs: export PYTHON_GIL=0 fi pytest -rfEsXR -n auto \ + --browser chromium --browser firefox --browser webkit \ --maxfail=50 --timeout=300 --durations=25 \ --cov-report=xml --cov=lib --log-level=DEBUG --color=yes diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 678e49b114d3..b51378d26e3d 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -122,6 +122,7 @@ stages: PYTHONFAULTHANDLER=1 pytest -rfEsXR -n 2 \ --maxfail=50 --timeout=300 --durations=25 \ + --browser chromium --browser firefox --browser webkit \ --junitxml=junit/test-results.xml --cov-report=xml --cov=lib if [[ $VS_VER == 2022 ]]; then From 28ad8616fe9821276999a96569ba8148c8407f49 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 4 Nov 2022 03:33:13 -0400 Subject: [PATCH 8/8] Debug other OSs --- .github/workflows/tests.yml | 7 +++++++ lib/matplotlib/tests/test_backend_webagg.py | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1c05f95e850e..3d553b110260 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -325,6 +325,7 @@ jobs: fi pytest -rfEsXR -n auto \ --browser chromium --browser firefox --browser webkit \ + --slowmo=100 --tracing=on --video=on \ --maxfail=50 --timeout=300 --durations=25 \ --cov-report=xml --cov=lib --log-level=DEBUG --color=yes @@ -395,6 +396,12 @@ jobs: name: "${{ matrix.python-version }} ${{ matrix.os }} ${{ matrix.name-suffix }} result images" path: ./result_images + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: failure() + with: + name: "${{ matrix.python-version }} ${{ matrix.os }} ${{ matrix.name-suffix }} playwright" + path: ./test-results + # Separate dependent job to only upload one issue from the matrix of jobs create-issue: if: ${{ failure() && github.event_name == 'schedule' }} diff --git a/lib/matplotlib/tests/test_backend_webagg.py b/lib/matplotlib/tests/test_backend_webagg.py index 5fbcbd19dd88..43e9aa2fba41 100644 --- a/lib/matplotlib/tests/test_backend_webagg.py +++ b/lib/matplotlib/tests/test_backend_webagg.py @@ -161,12 +161,16 @@ def test_webagg_resize(page): canvas = page.locator('canvas.mpl-canvas') + print(f'{orig_bbox=}') # Resize the canvas to be twice as big. bbox = canvas.bounding_box() + print(f'{bbox=}') x, y = bbox['x'] + bbox['width'] - 1, bbox['y'] + bbox['height'] - 1 + print(f'{x=} {y=}') page.mouse.move(x, y) page.mouse.down() page.mouse.move(x + bbox['width'], y + bbox['height']) + print(f'{x + bbox["width"]=} {y + bbox["height"]=}') page.mouse.up() assert fig.bbox.height == orig_bbox.height * 2