@@ -303,3 +303,115 @@ def test_webagg_toolbar_pan(random_port, page):
303303 # Contracts in y-direction.
304304 assert ax .viewLim .y1 == orig_lim .y1
305305 assert ax .viewLim .y0 < orig_lim .y0 - orig_lim .height / 2
306+
307+
308+ @pytest .mark .backend ('webagg' )
309+ def test_webagg_toolbar_zoom (random_port , page ):
310+ from playwright .sync_api import expect
311+
312+ fig , ax = plt .subplots (facecolor = 'w' )
313+ ax .plot ([3 , 2 , 1 ])
314+ orig_lim = ax .viewLim .frozen ()
315+ # Make figure coords ~= axes coords, with ticks visible for inspection.
316+ ax .set_position ([0 , 0 , 1 , 1 ])
317+ ax .tick_params (axis = 'y' , direction = 'in' , pad = - 22 )
318+ ax .tick_params (axis = 'x' , direction = 'in' , pad = - 15 )
319+
320+ # Don't start the Tornado event loop, but use the existing event loop
321+ # started by the `page` fixture.
322+ WebAggApplication .initialize ()
323+ WebAggApplication .started = True
324+
325+ page .goto (f'http://{ WebAggApplication .address } :{ WebAggApplication .port } /' )
326+
327+ canvas = page .locator ('canvas.mpl-canvas' )
328+ expect (canvas ).to_be_visible ()
329+ home = page .locator ('button.mpl-widget' ).nth (0 )
330+ expect (home ).to_be_visible ()
331+ pan = page .locator ('button.mpl-widget' ).nth (3 )
332+ expect (pan ).to_be_visible ()
333+ zoom = page .locator ('button.mpl-widget' ).nth (4 )
334+ expect (zoom ).to_be_visible ()
335+
336+ active_re = re .compile (r'active' )
337+ expect (pan ).not_to_have_class (active_re )
338+ expect (zoom ).not_to_have_class (active_re )
339+ assert ax .get_navigate_mode () is None
340+ zoom .click ()
341+ expect (pan ).not_to_have_class (active_re )
342+ expect (zoom ).to_have_class (active_re )
343+ assert ax .get_navigate_mode () == 'ZOOM'
344+
345+ # Zoom 25% in on each side.
346+ bbox = canvas .bounding_box ()
347+ x , y = bbox ['x' ] + bbox ['width' ] / 4 , bbox ['y' ] + bbox ['height' ] / 4
348+ page .mouse .move (x , y )
349+ page .mouse .down ()
350+ page .mouse .move (x + bbox ['width' ] / 2 , y + bbox ['height' ] / 2 ,
351+ steps = 20 )
352+ page .mouse .up ()
353+
354+ assert ax .get_xlim () == (orig_lim .x0 + orig_lim .width / 4 ,
355+ orig_lim .x1 - orig_lim .width / 4 )
356+ assert ax .get_ylim () == (orig_lim .y0 + orig_lim .height / 4 ,
357+ orig_lim .y1 - orig_lim .height / 4 )
358+
359+ # Reset.
360+ home .click ()
361+
362+ # Zoom 25% in on each side, while holding 'x' key, to constrain the zoom
363+ # horizontally..
364+ bbox = canvas .bounding_box ()
365+ x , y = bbox ['x' ] + bbox ['width' ] / 4 , bbox ['y' ] + bbox ['height' ] / 4
366+ page .mouse .move (x , y )
367+ page .mouse .down ()
368+ page .keyboard .down ('x' )
369+ page .mouse .move (x + bbox ['width' ] / 2 , y + bbox ['height' ] / 2 ,
370+ steps = 20 )
371+ page .mouse .up ()
372+ page .keyboard .up ('x' )
373+
374+ assert ax .get_xlim () == (orig_lim .x0 + orig_lim .width / 4 ,
375+ orig_lim .x1 - orig_lim .width / 4 )
376+ assert ax .get_ylim () == (orig_lim .y0 , orig_lim .y1 )
377+
378+ # Reset.
379+ home .click ()
380+
381+ # Zoom 25% in on each side, while holding 'y' key, to constrain the zoom
382+ # vertically.
383+ bbox = canvas .bounding_box ()
384+ x , y = bbox ['x' ] + bbox ['width' ] / 4 , bbox ['y' ] + bbox ['height' ] / 4
385+ page .mouse .move (x , y )
386+ page .mouse .down ()
387+ page .keyboard .down ('y' )
388+ page .mouse .move (x + bbox ['width' ] / 2 , y + bbox ['height' ] / 2 ,
389+ steps = 20 )
390+ page .mouse .up ()
391+ page .keyboard .up ('y' )
392+
393+ assert ax .get_xlim () == (orig_lim .x0 , orig_lim .x1 )
394+ assert ax .get_ylim () == (orig_lim .y0 + orig_lim .height / 4 ,
395+ orig_lim .y1 - orig_lim .height / 4 )
396+
397+ # Reset.
398+ home .click ()
399+
400+ # Zoom 25% out on each side.
401+ bbox = canvas .bounding_box ()
402+ x , y = bbox ['x' ] + bbox ['width' ] / 4 , bbox ['y' ] + bbox ['height' ] / 4
403+ page .mouse .move (x , y )
404+ page .mouse .down (button = 'right' )
405+ page .mouse .move (x + bbox ['width' ] / 2 , y + bbox ['height' ] / 2 ,
406+ steps = 20 )
407+ page .mouse .up (button = 'right' )
408+
409+ # Limits were doubled, but based on the central point.
410+ cx = orig_lim .x0 + orig_lim .width / 2
411+ x0 = cx - orig_lim .width
412+ x1 = cx + orig_lim .width
413+ assert ax .get_xlim () == (x0 , x1 )
414+ cy = orig_lim .y0 + orig_lim .height / 2
415+ y0 = cy - orig_lim .height
416+ y1 = cy + orig_lim .height
417+ assert ax .get_ylim () == (y0 , y1 )
0 commit comments