summaryrefslogtreecommitdiffstats
path: root/gfx/layers/apz/test/mochitest
diff options
context:
space:
mode:
Diffstat (limited to 'gfx/layers/apz/test/mochitest')
-rw-r--r--gfx/layers/apz/test/mochitest/FissionTestHelperChild.sys.mjs157
-rw-r--r--gfx/layers/apz/test/mochitest/FissionTestHelperParent.sys.mjs103
-rw-r--r--gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js1987
-rw-r--r--gfx/layers/apz/test/mochitest/apz_test_utils.js1322
-rw-r--r--gfx/layers/apz/test/mochitest/browser.toml80
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_animations_without_apz_sampler.js134
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_extension_popup_window.js189
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_oop_frame.js120
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_background_tab_load_scroll.js117
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_background_tab_scroll.js66
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_content_response_timeout.js88
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_group_fission.js149
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_position_sticky.js105
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_reset_scaling_zoom.js44
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_scroll_thumb_dragging.js77
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_scrollbar_in_extension_popup_window.js138
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_scrolling_in_extension_popup_window.js128
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_scrolling_on_inactive_scroller_in_extension_popup_window.js137
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_select_popup_position.js130
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_select_zoom.js195
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_tab_drag_event_counts.js105
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_tab_drag_zoom.js103
-rw-r--r--gfx/layers/apz/test/mochitest/green100x100.pngbin0 -> 255 bytes
-rw-r--r--gfx/layers/apz/test/mochitest/helper_background_tab_load_scroll.html147
-rw-r--r--gfx/layers/apz/test/mochitest/helper_background_tab_scroll.html9
-rw-r--r--gfx/layers/apz/test/mochitest/helper_basic_onetouchpinch.html90
-rw-r--r--gfx/layers/apz/test/mochitest/helper_basic_pan.html73
-rw-r--r--gfx/layers/apz/test/mochitest/helper_basic_scrollend.html92
-rw-r--r--gfx/layers/apz/test/mochitest/helper_basic_zoom.html71
-rw-r--r--gfx/layers/apz/test/mochitest/helper_browser_test_utils.js11
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1162771.html107
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1271432.html573
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1280013.html73
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1285070.html44
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1299195.html47
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1326290.html63
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1331693.html71
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1346632.html89
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1414336.html97
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1462961.html74
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1473108.html50
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1490393-2.html65
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1490393.html64
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1502010_unconsumed_pan.html76
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1506497_touch_action_fixed_on_fixed.html96
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1509575.html71
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1519339_hidden_smoothscroll.html61
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1544966_zoom_on_touch_action_none.html89
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1550510.html66
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1637113_main_thread_hit_test.html70
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1637135_narrow_viewport.html60
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1638441_fixed_pos_hit_test.html67
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1638458_contextmenu.html82
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1648491_no_pointercancel_with_dtc.html89
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1662800.html61
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1663731_no_pointercancel_on_second_touchstart.html82
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1669625.html79
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1674935.html76
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1682170_pointercancel_on_touchaction_pinchzoom.html75
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1695598.html123
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1714934_mouseevent_buttons.html40
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1719330.html65
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1719855.html100
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1719855_pointercancel_on_touchmove_after_contextmenu_prevented.html74
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1724759.html55
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1756529.html226
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1756814.html80
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1780701.html70
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1783936.html74
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug982141.html130
-rw-r--r--gfx/layers/apz/test/mochitest/helper_check_dp_size.html124
-rw-r--r--gfx/layers/apz/test/mochitest/helper_checkerboard_apzforcedisabled.html93
-rw-r--r--gfx/layers/apz/test/mochitest/helper_checkerboard_no_multiplier.html57
-rw-r--r--gfx/layers/apz/test/mochitest/helper_checkerboard_scrollinfo.html91
-rw-r--r--gfx/layers/apz/test/mochitest/helper_checkerboard_zoom_during_load.html55
-rw-r--r--gfx/layers/apz/test/mochitest/helper_checkerboard_zoomoverflowhidden.html150
-rw-r--r--gfx/layers/apz/test/mochitest/helper_click.html42
-rw-r--r--gfx/layers/apz/test/mochitest/helper_click_interrupt_animation.html96
-rw-r--r--gfx/layers/apz/test/mochitest/helper_content_response_timeout.html26
-rw-r--r--gfx/layers/apz/test/mochitest/helper_disallow_doubletap_zoom_inside_oopif.html58
-rw-r--r--gfx/layers/apz/test/mochitest/helper_displayport_expiry.html77
-rw-r--r--gfx/layers/apz/test/mochitest/helper_div_pan.html43
-rw-r--r--gfx/layers/apz/test/mochitest/helper_dommousescroll.html33
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom.html50
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_bug1702464.html90
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos.html88
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos_overflow.html113
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_gencon.html101
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_horizontal_center.html50
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable.html85
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable2.html109
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_htmlelement.html76
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_img.html43
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_large_overflow.html300
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_noscroll.html59
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing.html46
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing_listener.html47
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif-2.html128
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif.html50
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif_subframe-1.html21
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif_subframe-2.html21
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif_subframe-3.html31
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif_subframe-4.html31
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_scrolled_overflowhidden.html82
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_shadowdom.html69
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_small.html43
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_smooth.html161
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_square.html61
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tablecell.html110
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tallwide.html85
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_textarea.html43
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_touch_action_manipulation.html54
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_touch_action_manipulation_in_iframe.html84
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_touch_action_manipulation_subframe.html21
-rw-r--r--gfx/layers/apz/test/mochitest/helper_drag_bug1719913.html91
-rw-r--r--gfx/layers/apz/test/mochitest/helper_drag_bug1794590.html72
-rw-r--r--gfx/layers/apz/test/mochitest/helper_drag_bug1827330.html70
-rw-r--r--gfx/layers/apz/test/mochitest/helper_drag_click.html69
-rw-r--r--gfx/layers/apz/test/mochitest/helper_drag_root_scrollbar.html61
-rw-r--r--gfx/layers/apz/test/mochitest/helper_drag_scroll.html653
-rw-r--r--gfx/layers/apz/test/mochitest/helper_drag_scrollbar_hittest.html100
-rw-r--r--gfx/layers/apz/test/mochitest/helper_empty.html4
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_oopif.html194
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_transformed_oopif.html130
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_basic.html40
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_checkerboard_severity.html138
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_empty.html34
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_event_region_override.html84
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_force_empty_hit_region.html82
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_positionedcontent.html120
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_under_oopif.html88
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_initial_displayport.html105
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_irregular_areas.html101
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_large_subframe.html67
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_scroll_handoff.html50
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_scroll_oopif.html158
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_setResolution.html59
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_tap.html87
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_tap_in_nested_iframe_on_zoomed.html106
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_tap_on_zoomed.html93
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_touch.html99
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_transforms.html89
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_utils.js130
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fixed_html_hittest.html61
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fixed_pos_displayport.html101
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fixed_position_scroll_hittest.html51
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fullscreen.html53
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_backface_hidden.html67
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_basic.html141
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_bug1119497.html54
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_bug1257288.html74
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_bug1715187.html69
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_bug1715187_oopif.html13
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_bug1715369.html74
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_iframe.html13
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_oopif.html13
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-1.html124
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-2.html157
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-3.html56
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-4.html194
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_checkerboard.html57
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_clippath.html118
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_clipped_fixed_modal.html85
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_deep_scene_stack.html57
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_fixed-2.html74
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_fixed-3.html113
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_fixed.html82
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_fixed_bg.html53
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_fixed_in_scrolled_transform.html91
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_fixed_item_over_oop_iframe.html61
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_float_bug1434846.html56
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_float_bug1443518.html56
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_hidden_inactive_scrollframe.html55
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_hoisted_scrollinfo.html81
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-2.html69
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-3.html70
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective.html60
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective_child.html13
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_nested_transforms_bug1459696.html80
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_obscuration.html77
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_overscroll.html249
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_overscroll_contextmenu.html129
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_overscroll_subframe.html132
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_pointerevents_svg.html177
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_spam.html100
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_sticky_bug1478304.html58
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_touchaction.html353
-rw-r--r--gfx/layers/apz/test/mochitest/helper_horizontal_checkerboard.html65
-rw-r--r--gfx/layers/apz/test/mochitest/helper_iframe1.html14
-rw-r--r--gfx/layers/apz/test/mochitest/helper_iframe2.html14
-rw-r--r--gfx/layers/apz/test/mochitest/helper_iframe_pan.html49
-rw-r--r--gfx/layers/apz/test/mochitest/helper_iframe_textarea.html15
-rw-r--r--gfx/layers/apz/test/mochitest/helper_interrupted_reflow.html712
-rw-r--r--gfx/layers/apz/test/mochitest/helper_key_scroll.html109
-rw-r--r--gfx/layers/apz/test/mochitest/helper_keyboard_scrollend.html138
-rw-r--r--gfx/layers/apz/test/mochitest/helper_long_tap.html197
-rw-r--r--gfx/layers/apz/test/mochitest/helper_main_thread_smooth_scroll_scrollend.html47
-rw-r--r--gfx/layers/apz/test/mochitest/helper_mainthread_scroll_bug1662379.html174
-rw-r--r--gfx/layers/apz/test/mochitest/helper_minimum_scale_1_0.html46
-rw-r--r--gfx/layers/apz/test/mochitest/helper_no_scalable_with_initial_scale.html48
-rw-r--r--gfx/layers/apz/test/mochitest/helper_onetouchpinch_nested.html103
-rw-r--r--gfx/layers/apz/test/mochitest/helper_overflowhidden_zoom.html83
-rw-r--r--gfx/layers/apz/test/mochitest/helper_override_root.html62
-rw-r--r--gfx/layers/apz/test/mochitest/helper_override_subdoc.html15
-rw-r--r--gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425573.html44
-rw-r--r--gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425603.html76
-rw-r--r--gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1494440.html50
-rw-r--r--gfx/layers/apz/test/mochitest/helper_overscroll_in_apz_test_data.html29
-rw-r--r--gfx/layers/apz/test/mochitest/helper_overscroll_in_subscroller.html165
-rw-r--r--gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-1.html88
-rw-r--r--gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-2.html65
-rw-r--r--gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-3.html77
-rw-r--r--gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-4.html79
-rw-r--r--gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-5.html110
-rw-r--r--gfx/layers/apz/test/mochitest/helper_position_sticky_flicker.html25
-rw-r--r--gfx/layers/apz/test/mochitest/helper_position_sticky_scroll_handoff.html88
-rw-r--r--gfx/layers/apz/test/mochitest/helper_programmatic_scroll_behavior.html81
-rw-r--r--gfx/layers/apz/test/mochitest/helper_relative_scroll_smoothness.html156
-rw-r--r--gfx/layers/apz/test/mochitest/helper_reset_zoom_bug1818967.html55
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_anchoring_on_wheel.html58
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll.html54
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll_with_set_timeout.html56
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_inactive_perspective.html45
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_inactive_zindex.html46
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1516056.html62
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1562757.html64
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_by_wheel.html65
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_detector.html108
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_on_position_fixed.html60
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_over_scrollbar.html48
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_snap_no_valid_snap_position.html45
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_panning.html93
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_scrollbar_dragging.html105
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_snap_on_page_down_scroll.html90
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scroll.html88
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scrollBy.html71
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_tables_perspective.html66
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_thumb_dragging.html20
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scrollbar_snap_bug1501062.html135
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scrollbarbutton_repeat.html101
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scrollbarbuttonclick_checkerboard.html75
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scrollbartrack_click_overshoot.html90
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scrollby_bug1531796.html36
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scrollend_bubbles.html99
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scrollframe_activation_on_load.html89
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scrollto_tap.html59
-rw-r--r--gfx/layers/apz/test/mochitest/helper_self_closer.html12
-rw-r--r--gfx/layers/apz/test/mochitest/helper_smoothscroll_spam.html51
-rw-r--r--gfx/layers/apz/test/mochitest/helper_smoothscroll_spam_interleaved.html57
-rw-r--r--gfx/layers/apz/test/mochitest/helper_subframe_style.css15
-rw-r--r--gfx/layers/apz/test/mochitest/helper_tab_scroll_scrollIntoView.html52
-rw-r--r--gfx/layers/apz/test/mochitest/helper_tall.html504
-rw-r--r--gfx/layers/apz/test/mochitest/helper_tap.html32
-rw-r--r--gfx/layers/apz/test/mochitest/helper_tap_default_passive.html96
-rw-r--r--gfx/layers/apz/test/mochitest/helper_tap_fullzoom.html33
-rw-r--r--gfx/layers/apz/test/mochitest/helper_tap_passive.html80
-rw-r--r--gfx/layers/apz/test/mochitest/helper_test_autoscrolling_in_oop_frame.html9
-rw-r--r--gfx/layers/apz/test/mochitest/helper_test_reset_scaling_zoom.html23
-rw-r--r--gfx/layers/apz/test/mochitest/helper_test_select_popup_position.html23
-rw-r--r--gfx/layers/apz/test/mochitest/helper_test_select_popup_position_transformed_in_parent.html25
-rw-r--r--gfx/layers/apz/test/mochitest/helper_test_select_popup_position_zoomed.html25
-rw-r--r--gfx/layers/apz/test/mochitest/helper_test_select_zoom.html43
-rw-r--r--gfx/layers/apz/test/mochitest/helper_test_tab_drag_event_counts.html24
-rw-r--r--gfx/layers/apz/test/mochitest/helper_test_tab_drag_zoom.html18
-rw-r--r--gfx/layers/apz/test/mochitest/helper_touch_action.html123
-rw-r--r--gfx/layers/apz/test/mochitest/helper_touch_action_complex.html137
-rw-r--r--gfx/layers/apz/test/mochitest/helper_touch_action_ordering_block.html39
-rw-r--r--gfx/layers/apz/test/mochitest/helper_touch_action_ordering_zindex.html37
-rw-r--r--gfx/layers/apz/test/mochitest/helper_touch_action_regions.html345
-rw-r--r--gfx/layers/apz/test/mochitest/helper_touch_action_zero_opacity_bug1500864.html45
-rw-r--r--gfx/layers/apz/test/mochitest/helper_touch_drag_root_scrollbar.html51
-rw-r--r--gfx/layers/apz/test/mochitest/helper_touch_synthesized_mouseevents.html103
-rw-r--r--gfx/layers/apz/test/mochitest/helper_touchpad_pinch_and_pan.html49
-rw-r--r--gfx/layers/apz/test/mochitest/helper_transform_end_on_keyboard_scroll.html58
-rw-r--r--gfx/layers/apz/test/mochitest/helper_transform_end_on_wheel_scroll.html28
-rw-r--r--gfx/layers/apz/test/mochitest/helper_visual_scrollbars_pagescroll.html119
-rw-r--r--gfx/layers/apz/test/mochitest/helper_visual_smooth_scroll.html53
-rw-r--r--gfx/layers/apz/test/mochitest/helper_visualscroll_clamp_restore.html63
-rw-r--r--gfx/layers/apz/test/mochitest/helper_visualscroll_nonrcd_rsf.html87
-rw-r--r--gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe.html52
-rw-r--r--gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe_child.html11
-rw-r--r--gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_non_scrollable_iframe.html113
-rw-r--r--gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe.html33
-rw-r--r--gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe_child.html71
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_dynamic_toolbar_bug1828235.html54
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_fixed_bug1673511.html42
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_iframe-2.html104
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_iframe.html105
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_iframe_subframe.html19
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_multiline.html94
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nested_position_fixed.html46
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom.html39
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom_bug1738696.html51
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_scroll.html51
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_touch-action.html67
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_zoom.html39
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_zoom_in_position_fixed.html47
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoom_after_gpu_process_restart.html63
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoom_keyboardscroll.html74
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoom_oopif.html54
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoom_out_clamped_scrollpos.html85
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoom_out_with_mainthread_clamping.html110
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoom_prevented.html75
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoom_restore_position_tabswitch.html74
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoom_with_dynamic_toolbar.html45
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoom_with_touchpad.html110
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomed_pan.html79
-rw-r--r--gfx/layers/apz/test/mochitest/mochitest.toml234
-rw-r--r--gfx/layers/apz/test/mochitest/test_abort_smooth_scroll_by_instant_scroll.html51
-rw-r--r--gfx/layers/apz/test/mochitest/test_bug1151667.html63
-rw-r--r--gfx/layers/apz/test/mochitest/test_bug1253683.html59
-rw-r--r--gfx/layers/apz/test/mochitest/test_bug1277814.html105
-rw-r--r--gfx/layers/apz/test/mochitest/test_bug1304689-2.html130
-rw-r--r--gfx/layers/apz/test/mochitest/test_bug1304689.html134
-rw-r--r--gfx/layers/apz/test/mochitest/test_frame_reconstruction.html218
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_bug1534549.html37
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_checkerboarding.html83
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_displayport.html31
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_double_tap_zoom-2.html100
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_double_tap_zoom.html66
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_fullscreen.html32
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_hittest-1.html59
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_hittest-2.html72
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_hittest-3.html50
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_hittest-overscroll.html54
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_keyboard-2.html47
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_keyboard.html41
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_mainthread.html49
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_minimum_scale_size.html48
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_mouseevents.html104
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_overrides.html37
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_overscroll.html35
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_overscroll_handoff.html46
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_pointerevents.html49
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_programmatic_scroll_behavior.html38
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_scroll_linked_effect.html33
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_scroll_snap.html67
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_scrollend.html74
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_scrollframe_activation.html49
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_touchevents-2.html69
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_touchevents-3.html47
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_touchevents-4.html54
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_touchevents-5.html54
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_touchevents.html55
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_wheelevents.html74
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_zoom-2.html81
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_zoom.html80
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_zoomToFocusedInput.html60
-rw-r--r--gfx/layers/apz/test/mochitest/test_interrupted_reflow.html38
-rw-r--r--gfx/layers/apz/test/mochitest/test_layerization.html312
-rw-r--r--gfx/layers/apz/test/mochitest/test_relative_update.html92
-rw-r--r--gfx/layers/apz/test/mochitest/test_scroll_inactive_bug1190112.html553
-rw-r--r--gfx/layers/apz/test/mochitest/test_scroll_inactive_flattened_frame.html50
-rw-r--r--gfx/layers/apz/test/mochitest/test_scroll_subframe_scrollbar.html116
-rw-r--r--gfx/layers/apz/test/mochitest/test_smoothness.html67
-rw-r--r--gfx/layers/apz/test/mochitest/test_touch_listeners_impacting_wheel.html119
-rw-r--r--gfx/layers/apz/test/mochitest/test_wheel_scroll.html109
-rw-r--r--gfx/layers/apz/test/mochitest/test_wheel_transactions.html150
358 files changed, 33961 insertions, 0 deletions
diff --git a/gfx/layers/apz/test/mochitest/FissionTestHelperChild.sys.mjs b/gfx/layers/apz/test/mochitest/FissionTestHelperChild.sys.mjs
new file mode 100644
index 0000000000..c0695b7abb
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/FissionTestHelperChild.sys.mjs
@@ -0,0 +1,157 @@
+// This code runs in the content process that holds the window to which
+// this actor is attached. There is one instance of this class for each
+// "inner window" (i.e. one per content document, including iframes/nested
+// iframes).
+// There is a 1:1 relationship between instances of this class and
+// FissionTestHelperParent instances, and the pair are entangled such
+// that they can communicate with each other regardless of which process
+// they live in.
+
+export class FissionTestHelperChild extends JSWindowActorChild {
+ constructor() {
+ super();
+ this._msgCounter = 0;
+ this._oopifResponsePromiseResolvers = [];
+ }
+
+ cw() {
+ return this.contentWindow.wrappedJSObject;
+ }
+
+ initialize() {
+ // This exports a bunch of things into the content window so that
+ // the test can access them. Most things are scoped inside the
+ // FissionTestHelper object on the window to avoid polluting the global
+ // namespace.
+
+ let cw = this.cw();
+ Cu.exportFunction(
+ (cond, msg) => this.sendAsyncMessage("ok", { cond, msg }),
+ cw,
+ { defineAs: "ok" }
+ );
+ Cu.exportFunction(
+ (a, b, msg) => this.sendAsyncMessage("is", { a, b, msg }),
+ cw,
+ { defineAs: "is" }
+ );
+
+ let FissionTestHelper = Cu.createObjectIn(cw, {
+ defineAs: "FissionTestHelper",
+ });
+ FissionTestHelper.startTestPromise = new cw.Promise(
+ Cu.exportFunction(resolve => {
+ this._startTestPromiseResolver = resolve;
+ }, cw)
+ );
+
+ Cu.exportFunction(this.subtestDone.bind(this), FissionTestHelper, {
+ defineAs: "subtestDone",
+ });
+
+ Cu.exportFunction(this.subtestFailed.bind(this), FissionTestHelper, {
+ defineAs: "subtestFailed",
+ });
+
+ Cu.exportFunction(this.sendToOopif.bind(this), FissionTestHelper, {
+ defineAs: "sendToOopif",
+ });
+ Cu.exportFunction(this.fireEventInEmbedder.bind(this), FissionTestHelper, {
+ defineAs: "fireEventInEmbedder",
+ });
+ }
+
+ // Called by the subtest to indicate completion to the top-level browser-chrome
+ // mochitest.
+ subtestDone() {
+ let cw = this.cw();
+ if (cw.ApzCleanup) {
+ cw.ApzCleanup.execute();
+ }
+ this.sendAsyncMessage("Test:Complete", {});
+ }
+
+ // Called by the subtest to indicate subtest failure. Only one of subtestDone
+ // or subtestFailed should be called.
+ subtestFailed(msg) {
+ this.sendAsyncMessage("ok", { cond: false, msg });
+ this.subtestDone();
+ }
+
+ // Called by the subtest to eval some code in the OOP iframe. This returns
+ // a promise that resolves to the return value from the eval.
+ sendToOopif(iframeElement, stringToEval) {
+ let browsingContextId = iframeElement.browsingContext.id;
+ let msgId = ++this._msgCounter;
+ let cw = this.cw();
+ let responsePromise = new cw.Promise(
+ Cu.exportFunction(resolve => {
+ this._oopifResponsePromiseResolvers[msgId] = resolve;
+ }, cw)
+ );
+ this.sendAsyncMessage("EmbedderToOopif", {
+ browsingContextId,
+ msgId,
+ stringToEval,
+ });
+ return responsePromise;
+ }
+
+ // Called by OOP iframes to dispatch an event in the embedder window. This
+ // can be used by the OOP iframe to asynchronously notify the embedder of
+ // things that happen. The embedder can use promiseOneEvent from
+ // helper_fission_utils.js to listen for these events.
+ fireEventInEmbedder(eventType, data) {
+ this.sendAsyncMessage("OopifToEmbedder", { eventType, data });
+ }
+
+ handleEvent(evt) {
+ switch (evt.type) {
+ case "FissionTestHelper:Init":
+ this.initialize();
+ break;
+ }
+ }
+
+ receiveMessage(msg) {
+ switch (msg.name) {
+ case "Test:Start":
+ this._startTestPromiseResolver();
+ delete this._startTestPromiseResolver;
+ break;
+ case "FromEmbedder":
+ let evalResult = this.contentWindow.eval(msg.data.stringToEval);
+ this.sendAsyncMessage("OopifToEmbedder", {
+ msgId: msg.data.msgId,
+ evalResult,
+ });
+ break;
+ case "FromOopif":
+ if (typeof msg.data.msgId == "number") {
+ if (!(msg.data.msgId in this._oopifResponsePromiseResolvers)) {
+ dump(
+ "Error: FromOopif got a message with unknown numeric msgId in " +
+ this.contentWindow.location.href +
+ "\n"
+ );
+ }
+ this._oopifResponsePromiseResolvers[msg.data.msgId](
+ msg.data.evalResult
+ );
+ delete this._oopifResponsePromiseResolvers[msg.data.msgId];
+ } else if (typeof msg.data.eventType == "string") {
+ let cw = this.cw();
+ let event = new cw.Event(msg.data.eventType);
+ event.data = Cu.cloneInto(msg.data.data, cw);
+ this.contentWindow.dispatchEvent(event);
+ } else {
+ dump(
+ "Warning: Unrecognized FromOopif message received in " +
+ this.contentWindow.location.href +
+ "\n"
+ );
+ }
+ break;
+ }
+ }
+}
diff --git a/gfx/layers/apz/test/mochitest/FissionTestHelperParent.sys.mjs b/gfx/layers/apz/test/mochitest/FissionTestHelperParent.sys.mjs
new file mode 100644
index 0000000000..d71de3b2ad
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/FissionTestHelperParent.sys.mjs
@@ -0,0 +1,103 @@
+// This code always runs in the parent process. There is one instance of
+// this class for each "inner window" (should be one per content document,
+// including iframes/nested iframes).
+// There is a 1:1 relationship between instances of this class and
+// FissionTestHelperChild instances, and the pair are entangled such
+// that they can communicate with each other regardless of which process
+// they live in.
+
+export class FissionTestHelperParent extends JSWindowActorParent {
+ constructor() {
+ super();
+ this._testCompletePromise = new Promise(resolve => {
+ this._testCompletePromiseResolver = resolve;
+ });
+ }
+
+ embedderWindow() {
+ let embedder = this.manager.browsingContext.embedderWindowGlobal;
+ // embedder is of type WindowGlobalParent, defined in WindowGlobalActors.webidl
+ if (!embedder) {
+ dump("ERROR: no embedder found in FissionTestHelperParent\n");
+ }
+ return embedder;
+ }
+
+ docURI() {
+ return this.manager.documentURI.spec;
+ }
+
+ // Returns a promise that is resolved when this parent actor receives a
+ // "Test:Complete" message from the child.
+ getTestCompletePromise() {
+ return this._testCompletePromise;
+ }
+
+ startTest() {
+ this.sendAsyncMessage("Test:Start", {});
+ }
+
+ receiveMessage(msg) {
+ switch (msg.name) {
+ case "ok":
+ FissionTestHelperParent.SimpleTest.ok(
+ msg.data.cond,
+ this.docURI() + " | " + msg.data.msg
+ );
+ break;
+
+ case "is":
+ FissionTestHelperParent.SimpleTest.is(
+ msg.data.a,
+ msg.data.b,
+ this.docURI() + " | " + msg.data.msg
+ );
+ break;
+
+ case "Test:Complete":
+ this._testCompletePromiseResolver();
+ break;
+
+ case "EmbedderToOopif":
+ // This relays messages from the embedder to an OOP-iframe. The browsing
+ // context id in the message data identifies the OOP-iframe.
+ let oopifBrowsingContext = BrowsingContext.get(
+ msg.data.browsingContextId
+ );
+ if (oopifBrowsingContext == null) {
+ FissionTestHelperParent.SimpleTest.ok(
+ false,
+ "EmbedderToOopif couldn't find oopif"
+ );
+ break;
+ }
+ let oopifActor =
+ oopifBrowsingContext.currentWindowGlobal.getActor(
+ "FissionTestHelper"
+ );
+ if (!oopifActor) {
+ FissionTestHelperParent.SimpleTest.ok(
+ false,
+ "EmbedderToOopif couldn't find oopif actor"
+ );
+ break;
+ }
+ oopifActor.sendAsyncMessage("FromEmbedder", msg.data);
+ break;
+
+ case "OopifToEmbedder":
+ // This relays messages from the OOP-iframe to the top-level content
+ // window which is embedding it.
+ let embedderActor = this.embedderWindow().getActor("FissionTestHelper");
+ if (!embedderActor) {
+ FissionTestHelperParent.SimpleTest.ok(
+ false,
+ "OopifToEmbedder couldn't find embedder"
+ );
+ break;
+ }
+ embedderActor.sendAsyncMessage("FromOopif", msg.data);
+ break;
+ }
+ }
+}
diff --git a/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js b/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js
new file mode 100644
index 0000000000..c290965845
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js
@@ -0,0 +1,1987 @@
+// ownerGlobal isn't defined in content privileged windows.
+/* eslint-disable mozilla/use-ownerGlobal */
+
+// Utilities for synthesizing of native events.
+
+async function getResolution() {
+ let resolution = -1; // bogus value in case DWU fails us
+ // Use window.top to get the root content window which is what has
+ // the resolution.
+ resolution = await SpecialPowers.spawn(window.top, [], () => {
+ return SpecialPowers.getDOMWindowUtils(content.window).getResolution();
+ });
+ return resolution;
+}
+
+function getPlatform() {
+ if (navigator.platform.indexOf("Win") == 0) {
+ return "windows";
+ }
+ if (navigator.platform.indexOf("Mac") == 0) {
+ return "mac";
+ }
+ // Check for Android before Linux
+ if (navigator.appVersion.includes("Android")) {
+ return "android";
+ }
+ if (navigator.platform.indexOf("Linux") == 0) {
+ return "linux";
+ }
+ return "unknown";
+}
+
+function nativeVerticalWheelEventMsg() {
+ switch (getPlatform()) {
+ case "windows":
+ return 0x020a; // WM_MOUSEWHEEL
+ case "mac":
+ var useWheelCodepath = SpecialPowers.getBoolPref(
+ "apz.test.mac.synth_wheel_input",
+ false
+ );
+ // Default to 1 (kCGScrollPhaseBegan) to trigger PanGestureInput events
+ // from widget code. Allow setting a pref to override this behaviour and
+ // trigger ScrollWheelInput events instead.
+ return useWheelCodepath ? 0 : 1;
+ case "linux":
+ return 4; // value is unused, pass GDK_SCROLL_SMOOTH anyway
+ }
+ throw new Error(
+ "Native wheel events not supported on platform " + getPlatform()
+ );
+}
+
+function nativeHorizontalWheelEventMsg() {
+ switch (getPlatform()) {
+ case "windows":
+ return 0x020e; // WM_MOUSEHWHEEL
+ case "mac":
+ return 0; // value is unused, can be anything
+ case "linux":
+ return 4; // value is unused, pass GDK_SCROLL_SMOOTH anyway
+ }
+ throw new Error(
+ "Native wheel events not supported on platform " + getPlatform()
+ );
+}
+
+function nativeArrowDownKey() {
+ switch (getPlatform()) {
+ case "windows":
+ return WIN_VK_DOWN;
+ case "mac":
+ return MAC_VK_DownArrow;
+ }
+ throw new Error(
+ "Native key events not supported on platform " + getPlatform()
+ );
+}
+
+function nativeArrowUpKey() {
+ switch (getPlatform()) {
+ case "windows":
+ return WIN_VK_UP;
+ case "mac":
+ return MAC_VK_UpArrow;
+ }
+ throw new Error(
+ "Native key events not supported on platform " + getPlatform()
+ );
+}
+
+function targetIsWindow(aTarget) {
+ return aTarget.Window && aTarget instanceof aTarget.Window;
+}
+
+function targetIsTopWindow(aTarget) {
+ if (!targetIsWindow(aTarget)) {
+ return false;
+ }
+ return aTarget == aTarget.top;
+}
+
+// Given an event target which may be a window or an element, get the associated window.
+function windowForTarget(aTarget) {
+ if (targetIsWindow(aTarget)) {
+ return aTarget;
+ }
+ return aTarget.ownerDocument.defaultView;
+}
+
+// Given an event target which may be a window or an element, get the associated element.
+function elementForTarget(aTarget) {
+ if (targetIsWindow(aTarget)) {
+ return aTarget.document.documentElement;
+ }
+ return aTarget;
+}
+
+// Given an event target which may be a window or an element, get the associatd nsIDOMWindowUtils.
+function utilsForTarget(aTarget) {
+ return SpecialPowers.getDOMWindowUtils(windowForTarget(aTarget));
+}
+
+// Given a pixel scrolling delta, converts it to the platform's native units.
+function nativeScrollUnits(aTarget, aDimen) {
+ switch (getPlatform()) {
+ case "linux": {
+ // GTK deltas are treated as line height divided by 3 by gecko.
+ var targetWindow = windowForTarget(aTarget);
+ var targetElement = elementForTarget(aTarget);
+ var lineHeight =
+ targetWindow.getComputedStyle(targetElement)["font-size"];
+ return aDimen / (parseInt(lineHeight) * 3);
+ }
+ }
+ return aDimen;
+}
+
+function parseNativeModifiers(aModifiers, aWindow = window) {
+ let modifiers = 0;
+ if (aModifiers.capsLockKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CAPS_LOCK;
+ }
+ if (aModifiers.numLockKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUM_LOCK;
+ }
+ if (aModifiers.shiftKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_LEFT;
+ }
+ if (aModifiers.shiftRightKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_RIGHT;
+ }
+ if (aModifiers.ctrlKey) {
+ modifiers |=
+ SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT;
+ }
+ if (aModifiers.ctrlRightKey) {
+ modifiers |=
+ SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT;
+ }
+ if (aModifiers.altKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT;
+ }
+ if (aModifiers.altRightKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_RIGHT;
+ }
+ if (aModifiers.metaKey) {
+ modifiers |=
+ SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT;
+ }
+ if (aModifiers.metaRightKey) {
+ modifiers |=
+ SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT;
+ }
+ if (aModifiers.helpKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_HELP;
+ }
+ if (aModifiers.fnKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_FUNCTION;
+ }
+ if (aModifiers.numericKeyPadKey) {
+ modifiers |=
+ SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUMERIC_KEY_PAD;
+ }
+
+ if (aModifiers.accelKey) {
+ modifiers |= _EU_isMac(aWindow)
+ ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT
+ : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT;
+ }
+ if (aModifiers.accelRightKey) {
+ modifiers |= _EU_isMac(aWindow)
+ ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT
+ : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT;
+ }
+ if (aModifiers.altGrKey) {
+ modifiers |= _EU_isMac(aWindow)
+ ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT
+ : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_GRAPH;
+ }
+ return modifiers;
+}
+
+// Several event sythesization functions below (and their helpers) take a "target"
+// parameter which may be either an element or a window. For such functions,
+// the target's "bounding rect" refers to the bounding client rect for an element,
+// and the window's origin for a window.
+// Not all functions have been "upgraded" to allow a window argument yet; feel
+// free to upgrade others as necessary.
+
+// Get the origin of |aTarget| relative to the root content document's
+// visual viewport in CSS coordinates.
+// |aTarget| may be an element (contained in the root content document or
+// a subdocument) or, as a special case, the root content window.
+// FIXME: Support iframe windows as targets.
+function _getTargetRect(aTarget) {
+ let rect = { left: 0, top: 0, width: 0, height: 0 };
+
+ // If the target is the root content window, its origin relative
+ // to the visual viewport is (0, 0).
+ if (aTarget instanceof Window) {
+ return rect;
+ }
+ if (aTarget.Window && aTarget instanceof aTarget.Window) {
+ // iframe window
+ // FIXME: Compute proper rect against the root content window
+ return rect;
+ }
+
+ // Otherwise, we have an element. Start with the origin of
+ // its bounding client rect which is relative to the enclosing
+ // document's layout viewport. Note that for iframes, the
+ // layout viewport is also the visual viewport.
+ const boundingClientRect = aTarget.getBoundingClientRect();
+ rect.left = boundingClientRect.left;
+ rect.top = boundingClientRect.top;
+ rect.width = boundingClientRect.width;
+ rect.height = boundingClientRect.height;
+
+ // Iterate up the window hierarchy until we reach the root
+ // content window, adding the offsets of any iframe windows
+ // relative to their parent window.
+ while (aTarget.ownerDocument.defaultView.frameElement) {
+ const iframe = aTarget.ownerDocument.defaultView.frameElement;
+ // The offset of the iframe window relative to the parent window
+ // includes the iframe's border, and the iframe's origin in its
+ // containing document.
+ const style = iframe.ownerDocument.defaultView.getComputedStyle(iframe);
+ const borderLeft = parseFloat(style.borderLeftWidth) || 0;
+ const borderTop = parseFloat(style.borderTopWidth) || 0;
+ const borderRight = parseFloat(style.borderRightWidth) || 0;
+ const borderBottom = parseFloat(style.borderBottomWidth) || 0;
+ const paddingLeft = parseFloat(style.paddingLeft) || 0;
+ const paddingTop = parseFloat(style.paddingTop) || 0;
+ const paddingRight = parseFloat(style.paddingRight) || 0;
+ const paddingBottom = parseFloat(style.paddingBottom) || 0;
+ const iframeRect = iframe.getBoundingClientRect();
+ rect.left += iframeRect.left + borderLeft + paddingLeft;
+ rect.top += iframeRect.top + borderTop + paddingTop;
+ if (
+ rect.left + rect.width >
+ iframeRect.right - borderRight - paddingRight
+ ) {
+ rect.width = Math.max(
+ iframeRect.right - borderRight - paddingRight - rect.left,
+ 0
+ );
+ }
+ if (
+ rect.top + rect.height >
+ iframeRect.bottom - borderBottom - paddingBottom
+ ) {
+ rect.height = Math.max(
+ iframeRect.bottom - borderBottom - paddingBottom - rect.top,
+ 0
+ );
+ }
+ aTarget = iframe;
+ }
+
+ return rect;
+}
+
+// Returns the in-process root window for the given |aWindow|.
+function getInProcessRootWindow(aWindow) {
+ let window = aWindow;
+ while (window.frameElement) {
+ window = window.frameElement.ownerDocument.defaultView;
+ }
+ return window;
+}
+
+// Convert (offsetX, offsetY) of target or center of it, in CSS pixels to device
+// pixels relative to the screen.
+// TODO: this function currently does not incorporate some CSS transforms on
+// elements enclosing target, e.g. scale transforms.
+async function coordinatesRelativeToScreen(aParams) {
+ const {
+ target, // The target element or window
+ offsetX, // X offset relative to `target`
+ offsetY, // Y offset relative to `target`
+ atCenter, // Instead of offsetX/offsetY, return center of `target`
+ } = aParams;
+ // Note that |window| might not be the root content window, for two
+ // possible reasons:
+ // 1. The mochitest that's calling into this function is not using a mechanism
+ // like runSubtestsSeriallyInFreshWindows() to load the test page in
+ // a top-level context, so it's loaded into an iframe by the mochitest
+ // harness.
+ // 2. The mochitest itself creates an iframe and calls this function from
+ // script running in the context of the iframe.
+ // Since the resolution applies to the top level content document, below we
+ // use the mozInnerScreen{X,Y} of the top level content window (window.top)
+ // only for the case where this function gets called in the top level content
+ // document. In other cases we use nsIDOMWindowUtils.toScreenRect().
+
+ // We do often specify `window` as the target, if it's the top level window,
+ // `nsIDOMWindowUtils.toScreenRect` isn't suitable because the function is
+ // supposed to be called with values in the document coords, so for example
+ // if desktop zoom is being applied, (0, 0) in the document coords might be
+ // outside of the visual viewport, i.e. it's going to be negative with the
+ // `toScreenRect` conversion, whereas the call sites with `window` of this
+ // function expect (0, 0) position should be the visual viport's offset. So
+ // in such cases we simply use mozInnerScreen{X,Y} to convert the given value
+ // to the screen coords.
+ if (target instanceof Window && window.parent == window) {
+ const resolution = await getResolution();
+ const deviceScale = window.devicePixelRatio;
+ return {
+ x:
+ window.mozInnerScreenX * deviceScale +
+ (atCenter ? 0 : offsetX) * resolution * deviceScale,
+ y:
+ window.mozInnerScreenY * deviceScale +
+ (atCenter ? 0 : offsetY) * resolution * deviceScale,
+ };
+ }
+
+ const rect = _getTargetRect(target);
+
+ const utils = SpecialPowers.getDOMWindowUtils(getInProcessRootWindow(window));
+ const positionInScreenCoords = utils.toScreenRect(
+ rect.left + (atCenter ? rect.width / 2 : offsetX),
+ rect.top + (atCenter ? rect.height / 2 : offsetY),
+ 0,
+ 0
+ );
+
+ return {
+ x: positionInScreenCoords.x,
+ y: positionInScreenCoords.y,
+ };
+}
+
+// Get the bounding box of aElement, and return it in device pixels
+// relative to the screen.
+// TODO: This function should probably take into account the resolution and
+// the relative viewport rect like coordinatesRelativeToScreen() does.
+function rectRelativeToScreen(aElement) {
+ var targetWindow = aElement.ownerDocument.defaultView;
+ var scale = targetWindow.devicePixelRatio;
+ var rect = aElement.getBoundingClientRect();
+ return {
+ x: (targetWindow.mozInnerScreenX + rect.left) * scale,
+ y: (targetWindow.mozInnerScreenY + rect.top) * scale,
+ width: rect.width * scale,
+ height: rect.height * scale,
+ };
+}
+
+// Synthesizes a native mousewheel event and returns immediately. This does not
+// guarantee anything; you probably want to use one of the other functions below
+// which actually wait for results.
+// aX and aY are relative to the top-left of |aTarget|'s bounding rect.
+// aDeltaX and aDeltaY are pixel deltas, and aObserver can be left undefined
+// if not needed.
+async function synthesizeNativeWheel(
+ aTarget,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ aObserver
+) {
+ var pt = await coordinatesRelativeToScreen({
+ offsetX: aX,
+ offsetY: aY,
+ target: aTarget,
+ });
+ if (aDeltaX && aDeltaY) {
+ throw new Error(
+ "Simultaneous wheeling of horizontal and vertical is not supported on all platforms."
+ );
+ }
+ aDeltaX = nativeScrollUnits(aTarget, aDeltaX);
+ aDeltaY = nativeScrollUnits(aTarget, aDeltaY);
+ var msg = aDeltaX
+ ? nativeHorizontalWheelEventMsg()
+ : nativeVerticalWheelEventMsg();
+ var utils = utilsForTarget(aTarget);
+ var element = elementForTarget(aTarget);
+ utils.sendNativeMouseScrollEvent(
+ pt.x,
+ pt.y,
+ msg,
+ aDeltaX,
+ aDeltaY,
+ 0,
+ 0,
+ // Specify MOUSESCROLL_SCROLL_LINES if the test wants to run through wheel
+ // input code path on Mac since it's normal mouse wheel inputs.
+ SpecialPowers.getBoolPref("apz.test.mac.synth_wheel_input", false)
+ ? SpecialPowers.DOMWindowUtils.MOUSESCROLL_SCROLL_LINES
+ : 0,
+ element,
+ aObserver
+ );
+ return true;
+}
+
+// Synthesizes a native pan gesture event and returns immediately.
+// NOTE: This works only on Mac.
+// You can specify kCGScrollPhaseBegan = 1, kCGScrollPhaseChanged = 2 and
+// kCGScrollPhaseEnded = 4 for |aPhase|.
+async function synthesizeNativePanGestureEvent(
+ aTarget,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ aPhase,
+ aObserver
+) {
+ if (getPlatform() != "mac") {
+ throw new Error(
+ `synthesizeNativePanGestureEvent doesn't work on ${getPlatform()}`
+ );
+ }
+
+ var pt = await coordinatesRelativeToScreen({
+ offsetX: aX,
+ offsetY: aY,
+ target: aTarget,
+ });
+ if (aDeltaX && aDeltaY) {
+ throw new Error(
+ "Simultaneous panning of horizontal and vertical is not supported."
+ );
+ }
+
+ aDeltaX = nativeScrollUnits(aTarget, aDeltaX);
+ aDeltaY = nativeScrollUnits(aTarget, aDeltaY);
+
+ var element = elementForTarget(aTarget);
+ var utils = utilsForTarget(aTarget);
+ utils.sendNativeMouseScrollEvent(
+ pt.x,
+ pt.y,
+ aPhase,
+ aDeltaX,
+ aDeltaY,
+ 0 /* deltaZ */,
+ 0 /* modifiers */,
+ 0 /* scroll event unit pixel */,
+ element,
+ aObserver
+ );
+
+ return true;
+}
+
+// Sends a native touchpad pan event and resolve the returned promise once the
+// request has been successfully made to the OS.
+// NOTE: This works only on Windows and Linux.
+// You can specify nsIDOMWindowUtils.PHASE_BEGIN, PHASE_UPDATE and PHASE_END
+// for |aPhase|.
+async function promiseNativeTouchpadPanEventAndWaitForObserver(
+ aTarget,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ aPhase
+) {
+ if (getPlatform() != "windows" && getPlatform() != "linux") {
+ throw new Error(
+ `promiseNativeTouchpadPanEventAndWaitForObserver doesn't work on ${getPlatform()}`
+ );
+ }
+
+ let pt = await coordinatesRelativeToScreen({
+ offsetX: aX,
+ offsetY: aY,
+ target: aTarget,
+ });
+
+ const utils = utilsForTarget(aTarget);
+
+ return new Promise(resolve => {
+ var observer = {
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "touchpadpanevent") {
+ resolve();
+ }
+ },
+ };
+
+ utils.sendNativeTouchpadPan(
+ aPhase,
+ pt.x,
+ pt.y,
+ aDeltaX,
+ aDeltaY,
+ 0,
+ observer
+ );
+ });
+}
+
+async function synthesizeSimpleGestureEvent(
+ aElement,
+ aType,
+ aX,
+ aY,
+ aDirection,
+ aDelta,
+ aModifiers,
+ aClickCount
+) {
+ let pt = await coordinatesRelativeToScreen({
+ offsetX: aX,
+ offsetY: aY,
+ target: aElement,
+ });
+
+ let utils = utilsForTarget(aElement);
+ utils.sendSimpleGestureEvent(
+ aType,
+ pt.x,
+ pt.y,
+ aDirection,
+ aDelta,
+ aModifiers,
+ aClickCount
+ );
+}
+
+// Synthesizes a native pan gesture event and resolve the returned promise once the
+// request has been successfully made to the OS.
+function promiseNativePanGestureEventAndWaitForObserver(
+ aElement,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ aPhase
+) {
+ return new Promise(resolve => {
+ var observer = {
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "mousescrollevent") {
+ resolve();
+ }
+ },
+ };
+ synthesizeNativePanGestureEvent(
+ aElement,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ aPhase,
+ observer
+ );
+ });
+}
+
+// Synthesizes a native mousewheel event and resolve the returned promise once the
+// request has been successfully made to the OS. This does not necessarily
+// guarantee that the OS generates the event we requested. See
+// synthesizeNativeWheel for details on the parameters.
+function promiseNativeWheelAndWaitForObserver(
+ aElement,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY
+) {
+ return new Promise(resolve => {
+ var observer = {
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "mousescrollevent") {
+ resolve();
+ }
+ },
+ };
+ synthesizeNativeWheel(aElement, aX, aY, aDeltaX, aDeltaY, observer);
+ });
+}
+
+// Synthesizes a native mousewheel event and resolve the returned promise once the
+// wheel event is dispatched to |aTarget|'s containing window. If the event
+// targets content in a subdocument, |aTarget| should be inside the
+// subdocument (or the subdocument's window). See synthesizeNativeWheel for
+// details on the other parameters.
+function promiseNativeWheelAndWaitForWheelEvent(
+ aTarget,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY
+) {
+ return new Promise((resolve, reject) => {
+ var targetWindow = windowForTarget(aTarget);
+ targetWindow.addEventListener(
+ "wheel",
+ function (e) {
+ setTimeout(resolve, 0);
+ },
+ { once: true }
+ );
+ try {
+ synthesizeNativeWheel(aTarget, aX, aY, aDeltaX, aDeltaY);
+ } catch (e) {
+ reject(e);
+ }
+ });
+}
+
+// Synthesizes a native mousewheel event and resolves the returned promise once the
+// first resulting scroll event is dispatched to |aTarget|'s containing window.
+// If the event targets content in a subdocument, |aTarget| should be inside
+// the subdocument (or the subdocument's window). See synthesizeNativeWheel
+// for details on the other parameters.
+function promiseNativeWheelAndWaitForScrollEvent(
+ aTarget,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY
+) {
+ return new Promise((resolve, reject) => {
+ var targetWindow = windowForTarget(aTarget);
+ targetWindow.addEventListener(
+ "scroll",
+ function () {
+ setTimeout(resolve, 0);
+ },
+ { capture: true, once: true }
+ ); // scroll events don't always bubble
+ try {
+ synthesizeNativeWheel(aTarget, aX, aY, aDeltaX, aDeltaY);
+ } catch (e) {
+ reject(e);
+ }
+ });
+}
+
+async function synthesizeTouchpadPinch(scales, focusX, focusY, options) {
+ var scalesAndFoci = [];
+
+ for (let i = 0; i < scales.length; i++) {
+ scalesAndFoci.push([scales[i], focusX, focusY]);
+ }
+
+ await synthesizeTouchpadGesture(scalesAndFoci, options);
+}
+
+// scalesAndFoci is an array of [scale, focusX, focuxY] tuples.
+async function synthesizeTouchpadGesture(scalesAndFoci, options) {
+ // Check for options, fill in defaults if appropriate.
+ let waitForTransformEnd =
+ options.waitForTransformEnd !== undefined
+ ? options.waitForTransformEnd
+ : true;
+ let waitForFrames =
+ options.waitForFrames !== undefined ? options.waitForFrames : false;
+
+ // Register the listener for the TransformEnd observer topic
+ let transformEndPromise = promiseTransformEnd();
+
+ var modifierFlags = 0;
+ var utils = utilsForTarget(document.body);
+ for (let i = 0; i < scalesAndFoci.length; i++) {
+ var pt = await coordinatesRelativeToScreen({
+ offsetX: scalesAndFoci[i][1],
+ offsetY: scalesAndFoci[i][2],
+ target: document.body,
+ });
+ var phase;
+ if (i === 0) {
+ phase = SpecialPowers.DOMWindowUtils.PHASE_BEGIN;
+ } else if (i === scalesAndFoci.length - 1) {
+ phase = SpecialPowers.DOMWindowUtils.PHASE_END;
+ } else {
+ phase = SpecialPowers.DOMWindowUtils.PHASE_UPDATE;
+ }
+ utils.sendNativeTouchpadPinch(
+ phase,
+ scalesAndFoci[i][0],
+ pt.x,
+ pt.y,
+ modifierFlags
+ );
+ if (waitForFrames) {
+ await promiseFrame();
+ }
+ }
+
+ // Wait for TransformEnd to fire.
+ if (waitForTransformEnd) {
+ await transformEndPromise;
+ }
+}
+
+async function synthesizeTouchpadPan(
+ focusX,
+ focusY,
+ deltaXs,
+ deltaYs,
+ options
+) {
+ // Check for options, fill in defaults if appropriate.
+ let waitForTransformEnd =
+ options.waitForTransformEnd !== undefined
+ ? options.waitForTransformEnd
+ : true;
+ let waitForFrames =
+ options.waitForFrames !== undefined ? options.waitForFrames : false;
+
+ // Register the listener for the TransformEnd observer topic
+ let transformEndPromise = promiseTransformEnd();
+
+ var modifierFlags = 0;
+ var pt = await coordinatesRelativeToScreen({
+ offsetX: focusX,
+ offsetY: focusY,
+ target: document.body,
+ });
+ var utils = utilsForTarget(document.body);
+ for (let i = 0; i < deltaXs.length; i++) {
+ var phase;
+ if (i === 0) {
+ phase = SpecialPowers.DOMWindowUtils.PHASE_BEGIN;
+ } else if (i === deltaXs.length - 1) {
+ phase = SpecialPowers.DOMWindowUtils.PHASE_END;
+ } else {
+ phase = SpecialPowers.DOMWindowUtils.PHASE_UPDATE;
+ }
+ utils.sendNativeTouchpadPan(
+ phase,
+ pt.x,
+ pt.y,
+ deltaXs[i],
+ deltaYs[i],
+ modifierFlags
+ );
+ if (waitForFrames) {
+ await promiseFrame();
+ }
+ }
+
+ // Wait for TransformEnd to fire.
+ if (waitForTransformEnd) {
+ await transformEndPromise;
+ }
+}
+
+// Synthesizes a native touch event and dispatches it. aX and aY in CSS pixels
+// relative to the top-left of |aTarget|'s bounding rect.
+async function synthesizeNativeTouch(
+ aTarget,
+ aX,
+ aY,
+ aType,
+ aObserver = null,
+ aTouchId = 0
+) {
+ var pt = await coordinatesRelativeToScreen({
+ offsetX: aX,
+ offsetY: aY,
+ target: aTarget,
+ });
+ var utils = utilsForTarget(aTarget);
+ utils.sendNativeTouchPoint(aTouchId, aType, pt.x, pt.y, 1, 90, aObserver);
+ return true;
+}
+
+function sendBasicNativePointerInput(
+ utils,
+ aId,
+ aPointerType,
+ aState,
+ aX,
+ aY,
+ aObserver,
+ { pressure = 1, twist = 0, tiltX = 0, tiltY = 0, button = 0 } = {}
+) {
+ switch (aPointerType) {
+ case "touch":
+ utils.sendNativeTouchPoint(aId, aState, aX, aY, pressure, 90, aObserver);
+ break;
+ case "pen":
+ utils.sendNativePenInput(
+ aId,
+ aState,
+ aX,
+ aY,
+ pressure,
+ twist,
+ tiltX,
+ tiltY,
+ button,
+ aObserver
+ );
+ break;
+ default:
+ throw new Error(`Not supported: ${aPointerType}`);
+ }
+}
+
+async function promiseNativePointerInput(
+ aTarget,
+ aPointerType,
+ aState,
+ aX,
+ aY,
+ options
+) {
+ const pt = await coordinatesRelativeToScreen({
+ offsetX: aX,
+ offsetY: aY,
+ target: aTarget,
+ });
+ const utils = utilsForTarget(aTarget);
+ return new Promise(resolve => {
+ sendBasicNativePointerInput(
+ utils,
+ options?.pointerId ?? 0,
+ aPointerType,
+ aState,
+ pt.x,
+ pt.y,
+ resolve,
+ options
+ );
+ });
+}
+
+/**
+ * Function to generate native pointer events as a sequence.
+ * @param aTarget is the element or window whose bounding rect the coordinates are
+ * relative to.
+ * @param aPointerType "touch" or "pen".
+ * @param aPositions is a 2D array of position data. It is indexed as [row][column],
+ * where advancing the row counter moves forward in time, and each column
+ * represents a single pointer. Each row must have exactly
+ * the same number of columns, and the number of columns must match the length
+ * of the aPointerIds parameter.
+ * For each row, each entry is either an object with x and y fields,
+ * or a null. A null value indicates that the pointer should be "lifted"
+ * (i.e. send a touchend for that touch input). A non-null value therefore
+ * indicates the position of the pointer input.
+ * This function takes care of the state tracking necessary to send
+ * pointerup/pointerdown inputs as necessary as the pointers go up and down.
+ * @param aObserver is the observer that will get registered on the very last
+ * native pointer synthesis call this function makes.
+ * @param aPointerIds is an array holding the pointer ID values.
+ */
+async function synthesizeNativePointerSequences(
+ aTarget,
+ aPointerType,
+ aPositions,
+ aObserver = null,
+ aPointerIds = [0],
+ options
+) {
+ // We use lastNonNullValue to figure out which synthesizeNativeTouch call
+ // will be the last one we make, so that we can register aObserver on it.
+ var lastNonNullValue = -1;
+ for (let i = 0; i < aPositions.length; i++) {
+ if (aPositions[i] == null) {
+ throw new Error(`aPositions[${i}] was unexpectedly null`);
+ }
+ if (aPositions[i].length != aPointerIds.length) {
+ throw new Error(
+ `aPositions[${i}] did not have the expected number of positions; ` +
+ `expected ${aPointerIds.length} pointers but found ${aPositions[i].length}`
+ );
+ }
+ for (let j = 0; j < aPointerIds.length; j++) {
+ if (aPositions[i][j] != null) {
+ lastNonNullValue = i * aPointerIds.length + j;
+ // Do the conversion to screen space before actually synthesizing
+ // the events, otherwise the screen space may change as a result of
+ // the touch inputs and the conversion may not work as intended.
+ aPositions[i][j] = await coordinatesRelativeToScreen({
+ offsetX: aPositions[i][j].x,
+ offsetY: aPositions[i][j].y,
+ target: aTarget,
+ });
+ }
+ }
+ }
+ if (lastNonNullValue < 0) {
+ throw new Error("All values in positions array were null!");
+ }
+
+ // Insert a row of nulls at the end of aPositions, to ensure that all
+ // touches get removed. If the touches have already been removed this will
+ // just add an extra no-op iteration in the aPositions loop below.
+ var allNullRow = new Array(aPointerIds.length);
+ allNullRow.fill(null);
+ aPositions.push(allNullRow);
+
+ // The last sendNativeTouchPoint call will be the TOUCH_REMOVE which happens
+ // one iteration of aPosition after the last non-null value.
+ var lastSynthesizeCall = lastNonNullValue + aPointerIds.length;
+
+ // track which touches are down and which are up. start with all up
+ var currentPositions = new Array(aPointerIds.length);
+ currentPositions.fill(null);
+
+ var utils = utilsForTarget(aTarget);
+ // Iterate over the position data now, and generate the touches requested
+ for (let i = 0; i < aPositions.length; i++) {
+ for (let j = 0; j < aPointerIds.length; j++) {
+ if (aPositions[i][j] == null) {
+ // null means lift the finger
+ if (currentPositions[j] == null) {
+ // it's already lifted, do nothing
+ } else {
+ // synthesize the touch-up. If this is the last call we're going to
+ // make, pass the observer as well
+ var thisIndex = i * aPointerIds.length + j;
+ var observer = lastSynthesizeCall == thisIndex ? aObserver : null;
+ sendBasicNativePointerInput(
+ utils,
+ aPointerIds[j],
+ aPointerType,
+ SpecialPowers.DOMWindowUtils.TOUCH_REMOVE,
+ currentPositions[j].x,
+ currentPositions[j].y,
+ observer,
+ options
+ );
+ currentPositions[j] = null;
+ }
+ } else {
+ sendBasicNativePointerInput(
+ utils,
+ aPointerIds[j],
+ aPointerType,
+ SpecialPowers.DOMWindowUtils.TOUCH_CONTACT,
+ aPositions[i][j].x,
+ aPositions[i][j].y,
+ null,
+ options
+ );
+ currentPositions[j] = aPositions[i][j];
+ }
+ }
+ }
+ return true;
+}
+
+async function synthesizeNativeTouchSequences(
+ aTarget,
+ aPositions,
+ aObserver = null,
+ aTouchIds = [0]
+) {
+ await synthesizeNativePointerSequences(
+ aTarget,
+ "touch",
+ aPositions,
+ aObserver,
+ aTouchIds
+ );
+}
+
+async function synthesizeNativePointerDrag(
+ aTarget,
+ aPointerType,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ aObserver = null,
+ aPointerId = 0,
+ options
+) {
+ var steps = Math.max(Math.abs(aDeltaX), Math.abs(aDeltaY));
+ var positions = [[{ x: aX, y: aY }]];
+ for (var i = 1; i < steps; i++) {
+ var dx = i * (aDeltaX / steps);
+ var dy = i * (aDeltaY / steps);
+ var pos = { x: aX + dx, y: aY + dy };
+ positions.push([pos]);
+ }
+ positions.push([{ x: aX + aDeltaX, y: aY + aDeltaY }]);
+ return synthesizeNativePointerSequences(
+ aTarget,
+ aPointerType,
+ positions,
+ aObserver,
+ [aPointerId],
+ options
+ );
+}
+
+// Note that when calling this function you'll want to make sure that the pref
+// "apz.touch_start_tolerance" is set to 0, or some of the touchmove will get
+// consumed to overcome the panning threshold.
+async function synthesizeNativeTouchDrag(
+ aTarget,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ aObserver = null,
+ aTouchId = 0
+) {
+ return synthesizeNativePointerDrag(
+ aTarget,
+ "touch",
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ aObserver,
+ aTouchId
+ );
+}
+
+function promiseNativePointerDrag(
+ aTarget,
+ aPointerType,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ aPointerId = 0,
+ options
+) {
+ return new Promise(resolve => {
+ synthesizeNativePointerDrag(
+ aTarget,
+ aPointerType,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ resolve,
+ aPointerId,
+ options
+ );
+ });
+}
+
+// Promise-returning variant of synthesizeNativeTouchDrag
+function promiseNativeTouchDrag(
+ aTarget,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ aTouchId = 0
+) {
+ return new Promise(resolve => {
+ synthesizeNativeTouchDrag(
+ aTarget,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ resolve,
+ aTouchId
+ );
+ });
+}
+
+// Tapping is essentially a dragging with no move
+function promiseNativePointerTap(aTarget, aPointerType, aX, aY, options) {
+ return promiseNativePointerDrag(
+ aTarget,
+ aPointerType,
+ aX,
+ aY,
+ 0,
+ 0,
+ options?.pointerId ?? 0,
+ options
+ );
+}
+
+async function synthesizeNativeTap(aTarget, aX, aY, aObserver = null) {
+ var pt = await coordinatesRelativeToScreen({
+ offsetX: aX,
+ offsetY: aY,
+ target: aTarget,
+ });
+ let utils = utilsForTarget(aTarget);
+ utils.sendNativeTouchTap(pt.x, pt.y, false, aObserver);
+ return true;
+}
+
+// only currently implemented on macOS
+async function synthesizeNativeTouchpadDoubleTap(aTarget, aX, aY) {
+ ok(
+ getPlatform() == "mac",
+ "only implemented on mac. implement sendNativeTouchpadDoubleTap for this platform," +
+ " see bug 1696802 for how it was done on macOS"
+ );
+ let pt = await coordinatesRelativeToScreen({
+ offsetX: aX,
+ offsetY: aY,
+ target: aTarget,
+ });
+ let utils = utilsForTarget(aTarget);
+ utils.sendNativeTouchpadDoubleTap(pt.x, pt.y, 0);
+ return true;
+}
+
+// If the event targets content in a subdocument, |aTarget| should be inside the
+// subdocument (or the subdocument window).
+async function synthesizeNativeMouseEventWithAPZ(aParams, aObserver = null) {
+ if (aParams.win !== undefined) {
+ throw Error(
+ "Are you trying to use EventUtils' API? `win` won't be used with synthesizeNativeMouseClickWithAPZ."
+ );
+ }
+ if (aParams.scale !== undefined) {
+ throw Error(
+ "Are you trying to use EventUtils' API? `scale` won't be used with synthesizeNativeMouseClickWithAPZ."
+ );
+ }
+ if (aParams.elementOnWidget !== undefined) {
+ throw Error(
+ "Are you trying to use EventUtils' API? `elementOnWidget` won't be used with synthesizeNativeMouseClickWithAPZ."
+ );
+ }
+ const {
+ type, // "click", "mousedown", "mouseup" or "mousemove"
+ target, // Origin of offsetX and offsetY, must be an element
+ offsetX, // X offset in `target` in CSS Pixels
+ offsetY, // Y offset in `target` in CSS pixels
+ atCenter, // Instead of offsetX/Y, synthesize the event at center of `target`
+ screenX, // X offset in screen in device pixels, offsetX/Y nor atCenter must not be set if this is set
+ screenY, // Y offset in screen in device pixels, offsetX/Y nor atCenter must not be set if this is set
+ button = 0, // if "click", "mousedown", "mouseup", set same value as DOM MouseEvent.button
+ modifiers = {}, // Active modifiers, see `parseNativeModifiers`
+ } = aParams;
+ if (atCenter) {
+ if (offsetX != undefined || offsetY != undefined) {
+ throw Error(
+ `atCenter is specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified`
+ );
+ }
+ if (screenX != undefined || screenY != undefined) {
+ throw Error(
+ `atCenter is specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified`
+ );
+ }
+ } else if (offsetX != undefined && offsetY != undefined) {
+ if (screenX != undefined || screenY != undefined) {
+ throw Error(
+ `offsetX/Y are specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified`
+ );
+ }
+ } else if (screenX != undefined && screenY != undefined) {
+ if (offsetX != undefined || offsetY != undefined) {
+ throw Error(
+ `screenX/Y are specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified`
+ );
+ }
+ }
+ const pt = await (async () => {
+ if (screenX != undefined) {
+ return { x: screenX, y: screenY };
+ }
+ return coordinatesRelativeToScreen({
+ offsetX,
+ offsetY,
+ atCenter,
+ target,
+ });
+ })();
+ const utils = utilsForTarget(target);
+ const element = elementForTarget(target);
+ const modifierFlags = parseNativeModifiers(modifiers);
+ if (type === "click") {
+ utils.sendNativeMouseEvent(
+ pt.x,
+ pt.y,
+ utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN,
+ button,
+ modifierFlags,
+ element,
+ function () {
+ utils.sendNativeMouseEvent(
+ pt.x,
+ pt.y,
+ utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP,
+ button,
+ modifierFlags,
+ element,
+ aObserver
+ );
+ }
+ );
+ return;
+ }
+
+ utils.sendNativeMouseEvent(
+ pt.x,
+ pt.y,
+ (() => {
+ switch (type) {
+ case "mousedown":
+ return utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN;
+ case "mouseup":
+ return utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP;
+ case "mousemove":
+ return utils.NATIVE_MOUSE_MESSAGE_MOVE;
+ default:
+ throw Error(`Invalid type is specified: ${type}`);
+ }
+ })(),
+ button,
+ modifierFlags,
+ element,
+ aObserver
+ );
+}
+
+function promiseNativeMouseEventWithAPZ(aParams) {
+ return new Promise(resolve =>
+ synthesizeNativeMouseEventWithAPZ(aParams, resolve)
+ );
+}
+
+// See synthesizeNativeMouseEventWithAPZ for the detail of aParams.
+function promiseNativeMouseEventWithAPZAndWaitForEvent(aParams) {
+ return new Promise(resolve => {
+ const targetWindow = windowForTarget(aParams.target);
+ const eventType = aParams.eventTypeToWait || aParams.type;
+ targetWindow.addEventListener(eventType, resolve, {
+ once: true,
+ });
+ synthesizeNativeMouseEventWithAPZ(aParams);
+ });
+}
+
+// Move the mouse to (dx, dy) relative to |target|, and scroll the wheel
+// at that location.
+// Moving the mouse is necessary to avoid wheel events from two consecutive
+// promiseMoveMouseAndScrollWheelOver() calls on different elements being incorrectly
+// considered as part of the same wheel transaction.
+// We also wait for the mouse move event to be processed before sending the
+// wheel event, otherwise there is a chance they might get reordered, and
+// we have the transaction problem again.
+// This function returns a promise that is resolved when the resulting wheel
+// (if waitForScroll = false) or scroll (if waitForScroll = true) event is
+// received.
+function promiseMoveMouseAndScrollWheelOver(
+ target,
+ dx,
+ dy,
+ waitForScroll = true,
+ scrollDelta = 10
+) {
+ let p = promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "mousemove",
+ target,
+ offsetX: dx,
+ offsetY: dy,
+ });
+ if (waitForScroll) {
+ p = p.then(() => {
+ return promiseNativeWheelAndWaitForScrollEvent(
+ target,
+ dx,
+ dy,
+ 0,
+ -scrollDelta
+ );
+ });
+ } else {
+ p = p.then(() => {
+ return promiseNativeWheelAndWaitForWheelEvent(
+ target,
+ dx,
+ dy,
+ 0,
+ -scrollDelta
+ );
+ });
+ }
+ return p;
+}
+
+async function scrollbarDragStart(aTarget, aScaleFactor) {
+ var targetElement = elementForTarget(aTarget);
+ var w = {},
+ h = {};
+ utilsForTarget(aTarget).getScrollbarSizes(targetElement, w, h);
+ var verticalScrollbarWidth = w.value;
+ if (verticalScrollbarWidth == 0) {
+ return null;
+ }
+
+ var upArrowHeight = verticalScrollbarWidth; // assume square scrollbar buttons
+ var startX = targetElement.clientWidth + verticalScrollbarWidth / 2;
+ var startY = upArrowHeight + 5; // start dragging somewhere in the thumb
+ startX *= aScaleFactor;
+ startY *= aScaleFactor;
+
+ // targetElement.clientWidth is unaffected by the zoom, but if the target
+ // is the root content window, the distance from the window origin to the
+ // scrollbar in CSS pixels does decrease proportionally to the zoom,
+ // so the CSS coordinates we return need to be scaled accordingly.
+ if (targetIsTopWindow(aTarget)) {
+ var resolution = await getResolution();
+ startX /= resolution;
+ startY /= resolution;
+ }
+
+ return { x: startX, y: startY };
+}
+
+// Synthesizes events to drag |target|'s vertical scrollbar by the distance
+// specified, synthesizing a mousemove for each increment as specified.
+// Returns null if the element doesn't have a vertical scrollbar. Otherwise,
+// returns an async function that should be invoked after the mousemoves have been
+// processed by the widget code, to end the scrollbar drag. Mousemoves being
+// processed by the widget code can be detected by listening for the mousemove
+// events in the caller, or for some other event that is triggered by the
+// mousemove, such as the scroll event resulting from the scrollbar drag.
+// The aScaleFactor argument should be provided if the scrollframe has been
+// scaled by an enclosing CSS transform. (TODO: this is a workaround for the
+// fact that coordinatesRelativeToScreen is supposed to do this automatically
+// but it currently does not).
+// Note: helper_scrollbar_snap_bug1501062.html contains a copy of this code
+// with modifications. Fixes here should be copied there if appropriate.
+// |target| can be an element (for subframes) or a window (for root frames).
+async function promiseVerticalScrollbarDrag(
+ aTarget,
+ aDistance = 20,
+ aIncrement = 5,
+ aScaleFactor = 1
+) {
+ var startPoint = await scrollbarDragStart(aTarget, aScaleFactor);
+ var targetElement = elementForTarget(aTarget);
+ if (startPoint == null) {
+ return null;
+ }
+
+ dump(
+ "Starting drag at " +
+ startPoint.x +
+ ", " +
+ startPoint.y +
+ " from top-left of #" +
+ targetElement.id +
+ "\n"
+ );
+
+ // Move the mouse to the scrollbar thumb and drag it down
+ await promiseNativeMouseEventWithAPZ({
+ target: aTarget,
+ offsetX: startPoint.x,
+ offsetY: startPoint.y,
+ type: "mousemove",
+ });
+ // mouse down
+ await promiseNativeMouseEventWithAPZ({
+ target: aTarget,
+ offsetX: startPoint.x,
+ offsetY: startPoint.y,
+ type: "mousedown",
+ });
+ // drag vertically by |aIncrement| until we reach the specified distance
+ for (var y = aIncrement; y < aDistance; y += aIncrement) {
+ await promiseNativeMouseEventWithAPZ({
+ target: aTarget,
+ offsetX: startPoint.x,
+ offsetY: startPoint.y + y,
+ type: "mousemove",
+ });
+ }
+ await promiseNativeMouseEventWithAPZ({
+ target: aTarget,
+ offsetX: startPoint.x,
+ offsetY: startPoint.y + aDistance,
+ type: "mousemove",
+ });
+
+ // and return an async function to call afterwards to finish up the drag
+ return async function () {
+ dump("Finishing drag of #" + targetElement.id + "\n");
+ await promiseNativeMouseEventWithAPZ({
+ target: aTarget,
+ offsetX: startPoint.x,
+ offsetY: startPoint.y + aDistance,
+ type: "mouseup",
+ });
+ };
+}
+
+// This is similar to promiseVerticalScrollbarDrag except this triggers
+// the vertical scrollbar drag with a touch drag input. This function
+// returns true if a scrollbar was present and false if no scrollbar
+// was found for the given element.
+async function promiseVerticalScrollbarTouchDrag(
+ aTarget,
+ aDistance = 20,
+ aScaleFactor = 1
+) {
+ var startPoint = await scrollbarDragStart(aTarget, aScaleFactor);
+ var targetElement = elementForTarget(aTarget);
+ if (startPoint == null) {
+ return false;
+ }
+
+ dump(
+ "Starting touch drag at " +
+ startPoint.x +
+ ", " +
+ startPoint.y +
+ " from top-left of #" +
+ targetElement.id +
+ "\n"
+ );
+
+ await promiseNativeTouchDrag(
+ aTarget,
+ startPoint.x,
+ startPoint.y,
+ 0,
+ aDistance
+ );
+
+ return true;
+}
+
+// Synthesizes a native mouse drag, starting at offset (mouseX, mouseY) from
+// the given target. The drag occurs in the given number of steps, to a final
+// destination of (mouseX + distanceX, mouseY + distanceY) from the target.
+// Returns a promise (wrapped in a function, so it doesn't execute immediately)
+// that should be awaited after the mousemoves have been processed by the widget
+// code, to end the drag. This is important otherwise the OS can sometimes
+// reorder the events and the drag doesn't have the intended effect (see
+// bug 1368603).
+// Example usage:
+// let dragFinisher = await promiseNativeMouseDrag(myElement, 0, 0);
+// await myIndicationThatDragHadAnEffect;
+// await dragFinisher();
+async function promiseNativeMouseDrag(
+ target,
+ mouseX,
+ mouseY,
+ distanceX = 20,
+ distanceY = 20,
+ steps = 20
+) {
+ var targetElement = elementForTarget(target);
+ dump(
+ "Starting drag at " +
+ mouseX +
+ ", " +
+ mouseY +
+ " from top-left of #" +
+ targetElement.id +
+ "\n"
+ );
+
+ // Move the mouse to the target position
+ await promiseNativeMouseEventWithAPZ({
+ target,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ type: "mousemove",
+ });
+ // mouse down
+ await promiseNativeMouseEventWithAPZ({
+ target,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ type: "mousedown",
+ });
+ // drag vertically by |increment| until we reach the specified distance
+ for (var s = 1; s <= steps; s++) {
+ let dx = distanceX * (s / steps);
+ let dy = distanceY * (s / steps);
+ dump(`Dragging to ${mouseX + dx}, ${mouseY + dy} from target\n`);
+ await promiseNativeMouseEventWithAPZ({
+ target,
+ offsetX: mouseX + dx,
+ offsetY: mouseY + dy,
+ type: "mousemove",
+ });
+ }
+
+ // and return a function-wrapped promise to call afterwards to finish the drag
+ return function () {
+ return promiseNativeMouseEventWithAPZ({
+ target,
+ offsetX: mouseX + distanceX,
+ offsetY: mouseY + distanceY,
+ type: "mouseup",
+ });
+ };
+}
+
+// Synthesizes a native touch sequence of events corresponding to a pinch-zoom-in
+// at the given focus point. The focus point must be specified in CSS coordinates
+// relative to the document body.
+async function pinchZoomInTouchSequence(focusX, focusY) {
+ // prettier-ignore
+ var zoom_in = [
+ [ { x: focusX - 25, y: focusY - 50 }, { x: focusX + 25, y: focusY + 50 } ],
+ [ { x: focusX - 30, y: focusY - 80 }, { x: focusX + 30, y: focusY + 80 } ],
+ [ { x: focusX - 35, y: focusY - 110 }, { x: focusX + 40, y: focusY + 110 } ],
+ [ { x: focusX - 40, y: focusY - 140 }, { x: focusX + 45, y: focusY + 140 } ],
+ [ { x: focusX - 45, y: focusY - 170 }, { x: focusX + 50, y: focusY + 170 } ],
+ [ { x: focusX - 50, y: focusY - 200 }, { x: focusX + 55, y: focusY + 200 } ],
+ ];
+
+ var touchIds = [0, 1];
+ return synthesizeNativeTouchSequences(document.body, zoom_in, null, touchIds);
+}
+
+// Returns a promise that is resolved when the observer service dispatches a
+// message with the given topic.
+function promiseTopic(aTopic) {
+ return new Promise((resolve, reject) => {
+ SpecialPowers.Services.obs.addObserver(function observer(
+ subject,
+ topic,
+ data
+ ) {
+ try {
+ SpecialPowers.Services.obs.removeObserver(observer, topic);
+ resolve([subject, data]);
+ } catch (ex) {
+ SpecialPowers.Services.obs.removeObserver(observer, topic);
+ reject(ex);
+ }
+ },
+ aTopic);
+ });
+}
+
+// Returns a promise that is resolved when a APZ transform ends.
+function promiseTransformEnd() {
+ return promiseTopic("APZ:TransformEnd");
+}
+
+function promiseScrollend(aTarget = window) {
+ return promiseOneEvent(aTarget, "scrollend");
+}
+
+// Returns a promise that resolves after the indicated number
+// of touchend events have fired on the given target element.
+function promiseTouchEnd(element, count = 1) {
+ return new Promise(resolve => {
+ var eventCount = 0;
+ var counterFunction = function (e) {
+ eventCount++;
+ if (eventCount == count) {
+ element.removeEventListener("touchend", counterFunction, {
+ passive: true,
+ });
+ resolve();
+ }
+ };
+ element.addEventListener("touchend", counterFunction, { passive: true });
+ });
+}
+
+// This generates a touch-based pinch zoom-in gesture that is expected
+// to succeed. It returns after APZ has completed the zoom and reaches the end
+// of the transform. The focus point is expected to be in CSS coordinates
+// relative to the document body.
+async function pinchZoomInWithTouch(focusX, focusY) {
+ // Register the listener for the TransformEnd observer topic
+ let transformEndPromise = promiseTopic("APZ:TransformEnd");
+
+ // Dispatch all the touch events
+ await pinchZoomInTouchSequence(focusX, focusY);
+
+ // Wait for TransformEnd to fire.
+ await transformEndPromise;
+}
+// This generates a touchpad pinch zoom-in gesture that is expected
+// to succeed. It returns after APZ has completed the zoom and reaches the end
+// of the transform. The focus point is expected to be in CSS coordinates
+// relative to the document body.
+async function pinchZoomInWithTouchpad(focusX, focusY, options = {}) {
+ var zoomIn = [
+ 1.0, 1.019531, 1.035156, 1.037156, 1.039156, 1.054688, 1.056688, 1.070312,
+ 1.072312, 1.089844, 1.091844, 1.109375, 1.128906, 1.144531, 1.160156,
+ 1.175781, 1.191406, 1.207031, 1.222656, 1.234375, 1.246094, 1.261719,
+ 1.273438, 1.285156, 1.296875, 1.3125, 1.328125, 1.347656, 1.363281,
+ 1.382812, 1.402344, 1.421875, 1.0,
+ ];
+ await synthesizeTouchpadPinch(zoomIn, focusX, focusY, options);
+}
+
+async function pinchZoomInAndPanWithTouchpad(options = {}) {
+ var x = 584;
+ var y = 347;
+ var scalesAndFoci = [];
+ // Zoom
+ for (var scale = 1.0; scale <= 2.0; scale += 0.2) {
+ scalesAndFoci.push([scale, x, y]);
+ }
+ // Pan (due to a limitation of the current implementation, events
+ // for which the scale doesn't change are dropped, so vary the
+ // scale slightly as well).
+ for (var i = 1; i <= 20; i++) {
+ x -= 4;
+ y -= 5;
+ scalesAndFoci.push([scale + 0.01 * i, x, y]);
+ }
+ await synthesizeTouchpadGesture(scalesAndFoci, options);
+}
+
+async function pinchZoomOutWithTouchpad(focusX, focusY, options = {}) {
+ // The last item equal one to indicate scale end
+ var zoomOut = [
+ 1.0, 1.375, 1.359375, 1.339844, 1.316406, 1.296875, 1.277344, 1.257812,
+ 1.238281, 1.21875, 1.199219, 1.175781, 1.15625, 1.132812, 1.101562,
+ 1.078125, 1.054688, 1.03125, 1.011719, 0.992188, 0.972656, 0.953125,
+ 0.933594, 1.0,
+ ];
+ await synthesizeTouchpadPinch(zoomOut, focusX, focusY, options);
+}
+
+async function pinchZoomInOutWithTouchpad(focusX, focusY, options = {}) {
+ // Use the same scale for two events in a row to make sure the code handles this properly.
+ var zoomInOut = [
+ 1.0, 1.082031, 1.089844, 1.097656, 1.101562, 1.109375, 1.121094, 1.128906,
+ 1.128906, 1.125, 1.097656, 1.074219, 1.054688, 1.035156, 1.015625, 1.0, 1.0,
+ ];
+ await synthesizeTouchpadPinch(zoomInOut, focusX, focusY, options);
+}
+// This generates a touch-based pinch gesture that is expected to succeed
+// and trigger an APZ:TransformEnd observer notification.
+// It returns after that notification has been dispatched.
+// The coordinates of touch events in `touchSequence` are expected to be
+// in CSS coordinates relative to the document body.
+async function synthesizeNativeTouchAndWaitForTransformEnd(
+ touchSequence,
+ touchIds
+) {
+ // Register the listener for the TransformEnd observer topic
+ let transformEndPromise = promiseTopic("APZ:TransformEnd");
+
+ // Dispatch all the touch events
+ await synthesizeNativeTouchSequences(
+ document.body,
+ touchSequence,
+ null,
+ touchIds
+ );
+
+ // Wait for TransformEnd to fire.
+ await transformEndPromise;
+}
+
+// Returns a touch sequence for a pinch-zoom-out operation in the center
+// of the visual viewport. The touch sequence returned is in CSS coordinates
+// relative to the document body.
+function pinchZoomOutTouchSequenceAtCenter() {
+ // Divide the half of visual viewport size by 8, then cause touch events
+ // starting from the 7th furthest away from the center towards the center.
+ const deltaX = window.visualViewport.width / 16;
+ const deltaY = window.visualViewport.height / 16;
+ const centerX =
+ window.visualViewport.pageLeft + window.visualViewport.width / 2;
+ const centerY =
+ window.visualViewport.pageTop + window.visualViewport.height / 2;
+ // prettier-ignore
+ var zoom_out = [
+ [ { x: centerX - (deltaX * 6), y: centerY - (deltaY * 6) },
+ { x: centerX + (deltaX * 6), y: centerY + (deltaY * 6) } ],
+ [ { x: centerX - (deltaX * 5), y: centerY - (deltaY * 5) },
+ { x: centerX + (deltaX * 5), y: centerY + (deltaY * 5) } ],
+ [ { x: centerX - (deltaX * 4), y: centerY - (deltaY * 4) },
+ { x: centerX + (deltaX * 4), y: centerY + (deltaY * 4) } ],
+ [ { x: centerX - (deltaX * 3), y: centerY - (deltaY * 3) },
+ { x: centerX + (deltaX * 3), y: centerY + (deltaY * 3) } ],
+ [ { x: centerX - (deltaX * 2), y: centerY - (deltaY * 2) },
+ { x: centerX + (deltaX * 2), y: centerY + (deltaY * 2) } ],
+ [ { x: centerX - (deltaX * 1), y: centerY - (deltaY * 1) },
+ { x: centerX + (deltaX * 1), y: centerY + (deltaY * 1) } ],
+ ];
+ return zoom_out;
+}
+
+// This generates a touch-based pinch zoom-out gesture that is expected
+// to succeed. It returns after APZ has completed the zoom and reaches the end
+// of the transform. The touch inputs are directed to the center of the
+// current visual viewport.
+async function pinchZoomOutWithTouchAtCenter() {
+ var zoom_out = pinchZoomOutTouchSequenceAtCenter();
+ var touchIds = [0, 1];
+ await synthesizeNativeTouchAndWaitForTransformEnd(zoom_out, touchIds);
+}
+
+// useTouchpad is only currently implemented on macOS
+async function synthesizeDoubleTap(element, x, y, useTouchpad) {
+ if (useTouchpad) {
+ await synthesizeNativeTouchpadDoubleTap(element, x, y);
+ } else {
+ await synthesizeNativeTap(element, x, y);
+ await synthesizeNativeTap(element, x, y);
+ }
+}
+// useTouchpad is only currently implemented on macOS
+async function doubleTapOn(element, x, y, useTouchpad) {
+ let transformEndPromise = promiseTransformEnd();
+
+ await synthesizeDoubleTap(element, x, y, useTouchpad);
+
+ // Wait for the APZ:TransformEnd to fire
+ await transformEndPromise;
+
+ // Flush state so we can query an accurate resolution
+ await promiseApzFlushedRepaints();
+}
+
+const NativePanHandlerForLinux = {
+ beginPhase: SpecialPowers.DOMWindowUtils.PHASE_BEGIN,
+ updatePhase: SpecialPowers.DOMWindowUtils.PHASE_UPDATE,
+ endPhase: SpecialPowers.DOMWindowUtils.PHASE_END,
+ promiseNativePanEvent: promiseNativeTouchpadPanEventAndWaitForObserver,
+ delta: -50,
+};
+
+const NativePanHandlerForWindows = {
+ beginPhase: SpecialPowers.DOMWindowUtils.PHASE_BEGIN,
+ updatePhase: SpecialPowers.DOMWindowUtils.PHASE_UPDATE,
+ endPhase: SpecialPowers.DOMWindowUtils.PHASE_END,
+ promiseNativePanEvent: promiseNativeTouchpadPanEventAndWaitForObserver,
+ delta: 50,
+};
+
+const NativePanHandlerForMac = {
+ // From https://developer.apple.com/documentation/coregraphics/cgscrollphase/kcgscrollphasebegan?language=occ , etc.
+ beginPhase: 1, // kCGScrollPhaseBegan
+ updatePhase: 2, // kCGScrollPhaseChanged
+ endPhase: 4, // kCGScrollPhaseEnded
+ promiseNativePanEvent: promiseNativePanGestureEventAndWaitForObserver,
+ delta: -50,
+};
+
+const NativePanHandlerForHeadless = {
+ beginPhase: SpecialPowers.DOMWindowUtils.PHASE_BEGIN,
+ updatePhase: SpecialPowers.DOMWindowUtils.PHASE_UPDATE,
+ endPhase: SpecialPowers.DOMWindowUtils.PHASE_END,
+ promiseNativePanEvent: promiseNativeTouchpadPanEventAndWaitForObserver,
+ delta: 50,
+};
+
+function getPanHandler() {
+ if (SpecialPowers.isHeadless) {
+ return NativePanHandlerForHeadless;
+ }
+
+ switch (getPlatform()) {
+ case "linux":
+ return NativePanHandlerForLinux;
+ case "windows":
+ return NativePanHandlerForWindows;
+ case "mac":
+ return NativePanHandlerForMac;
+ default:
+ throw new Error(
+ "There's no native pan handler on platform " + getPlatform()
+ );
+ }
+}
+
+// Lazily get `NativePanHandler` to avoid an exception where we don't support
+// native pan events (e.g. Android).
+if (!window.hasOwnProperty("NativePanHandler")) {
+ Object.defineProperty(window, "NativePanHandler", {
+ get() {
+ return getPanHandler();
+ },
+ });
+}
+
+async function panRightToLeftBegin(aElement, aX, aY, aMultiplier) {
+ await NativePanHandler.promiseNativePanEvent(
+ aElement,
+ aX,
+ aY,
+ NativePanHandler.delta * aMultiplier,
+ 0,
+ NativePanHandler.beginPhase
+ );
+}
+
+async function panRightToLeftUpdate(aElement, aX, aY, aMultiplier) {
+ await NativePanHandler.promiseNativePanEvent(
+ aElement,
+ aX,
+ aY,
+ NativePanHandler.delta * aMultiplier,
+ 0,
+ NativePanHandler.updatePhase
+ );
+}
+
+async function panRightToLeftEnd(aElement, aX, aY, aMultiplier) {
+ await NativePanHandler.promiseNativePanEvent(
+ aElement,
+ aX,
+ aY,
+ 0,
+ 0,
+ NativePanHandler.endPhase
+ );
+}
+
+async function panRightToLeft(aElement, aX, aY, aMultiplier) {
+ await panRightToLeftBegin(aElement, aX, aY, aMultiplier);
+ await panRightToLeftUpdate(aElement, aX, aY, aMultiplier);
+ await panRightToLeftEnd(aElement, aX, aY, aMultiplier);
+}
+
+async function panLeftToRight(aElement, aX, aY, aMultiplier) {
+ await panLeftToRightBegin(aElement, aX, aY, aMultiplier);
+ await panLeftToRightUpdate(aElement, aX, aY, aMultiplier);
+ await panLeftToRightEnd(aElement, aX, aY, aMultiplier);
+}
+
+async function panLeftToRightBegin(aElement, aX, aY, aMultiplier) {
+ await NativePanHandler.promiseNativePanEvent(
+ aElement,
+ aX,
+ aY,
+ -NativePanHandler.delta * aMultiplier,
+ 0,
+ NativePanHandler.beginPhase
+ );
+}
+
+async function panLeftToRightUpdate(aElement, aX, aY, aMultiplier) {
+ await NativePanHandler.promiseNativePanEvent(
+ aElement,
+ aX,
+ aY,
+ -NativePanHandler.delta * aMultiplier,
+ 0,
+ NativePanHandler.updatePhase
+ );
+ await NativePanHandler.promiseNativePanEvent(
+ aElement,
+ aX,
+ aY,
+ -NativePanHandler.delta * aMultiplier,
+ 0,
+ NativePanHandler.updatePhase
+ );
+}
+
+async function panLeftToRightEnd(aElement, aX, aY, aMultiplier) {
+ await NativePanHandler.promiseNativePanEvent(
+ aElement,
+ aX,
+ aY,
+ 0,
+ 0,
+ NativePanHandler.endPhase
+ );
+}
+
+// Close the context menu on desktop platforms.
+// NOTE: This function doesn't work if the context menu isn't open.
+async function closeContextMenu() {
+ if (getPlatform() == "android") {
+ return;
+ }
+
+ const contextmenuClosedPromise = SpecialPowers.spawnChrome([], async () => {
+ const menu = this.browsingContext.topChromeWindow.document.getElementById(
+ "contentAreaContextMenu"
+ );
+ ok(
+ menu.state == "open" || menu.state == "showing",
+ "This function is supposed to work only if the context menu is open or showing"
+ );
+
+ return new Promise(resolve => {
+ menu.addEventListener(
+ "popuphidden",
+ () => {
+ resolve();
+ },
+ { once: true }
+ );
+ menu.hidePopup();
+ });
+ });
+
+ await contextmenuClosedPromise;
+}
+
+// Get a list of prefs which should be used for a subtest which wants to
+// generate a smooth scroll animation using an input event. The smooth
+// scroll animation is slowed down so the test can perform other actions
+// while it's still in progress.
+function getSmoothScrollPrefs(aInputType, aMsdPhysics) {
+ let result = [["apz.test.logging_enabled", true]];
+ // Some callers just want the default and don't pass in aMsdPhysics.
+ if (aMsdPhysics !== undefined) {
+ result.push(["general.smoothScroll.msdPhysics.enabled", aMsdPhysics]);
+ } else {
+ aMsdPhysics = SpecialPowers.getBoolPref(
+ "general.smoothScroll.msdPhysics.enabled"
+ );
+ }
+ if (aInputType == "wheel") {
+ // We want to test real wheel events rather than pan events.
+ result.push(["apz.test.mac.synth_wheel_input", true]);
+ } /* keyboard input */ else {
+ // The default verticalScrollDistance (which is 3) is too small for native
+ // keyboard scrolling, it sometimes produces same scroll offsets in the early
+ // stages of the smooth animation.
+ result.push(["toolkit.scrollbox.verticalScrollDistance", 5]);
+ }
+ // Use a longer animation duration to avoid the situation that the
+ // animation stops accidentally in between each arrow input event.
+ // If the situation happens, scroll offsets will not change at the moment.
+ if (aMsdPhysics) {
+ // Prefs for MSD physics (applicable to any input type).
+ result.push(
+ ...[
+ ["general.smoothScroll.msdPhysics.motionBeginSpringConstant", 20],
+ ["general.smoothScroll.msdPhysics.regularSpringConstant", 20],
+ ["general.smoothScroll.msdPhysics.slowdownMinDeltaRatio", 0.1],
+ ["general.smoothScroll.msdPhysics.slowdownSpringConstant", 20],
+ ]
+ );
+ } else if (aInputType == "wheel") {
+ // Prefs for Bezier physics with wheel input.
+ result.push(
+ ...[
+ ["general.smoothScroll.mouseWheel.durationMaxMS", 1500],
+ ["general.smoothScroll.mouseWheel.durationMinMS", 1500],
+ ]
+ );
+ } else {
+ // Prefs for Bezier physics with keyboard input.
+ result.push(
+ ...[
+ ["general.smoothScroll.lines.durationMaxMS", 1500],
+ ["general.smoothScroll.lines.durationMinMS", 1500],
+ ]
+ );
+ }
+ return result;
+}
+
+function buildRelativeScrollSmoothnessVariants(aInputType, aScrollMethods) {
+ let subtests = [];
+ for (let scrollMethod of aScrollMethods) {
+ subtests.push({
+ file: `helper_relative_scroll_smoothness.html?input-type=${aInputType}&scroll-method=${scrollMethod}&strict=true`,
+ prefs: getSmoothScrollPrefs(aInputType, /* Bezier physics */ false),
+ });
+ // For MSD physics, run the test with strict=false. The shape of the
+ // animation curve is highly timing dependent, and we can't guarantee
+ // that an animation will run long enough until the next input event
+ // arrives.
+ subtests.push({
+ file: `helper_relative_scroll_smoothness.html?input-type=${aInputType}&scroll-method=${scrollMethod}&strict=false`,
+ prefs: getSmoothScrollPrefs(aInputType, /* MSD physics */ true),
+ });
+ }
+ return subtests;
+}
diff --git a/gfx/layers/apz/test/mochitest/apz_test_utils.js b/gfx/layers/apz/test/mochitest/apz_test_utils.js
new file mode 100644
index 0000000000..821c66103d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/apz_test_utils.js
@@ -0,0 +1,1322 @@
+// Utilities for writing APZ tests using the framework added in bug 961289
+
+// ----------------------------------------------------------------------
+// Functions that convert the APZ test data into a more usable form.
+// Every place we have a WebIDL sequence whose elements are dictionaries
+// with two elements, a key, and a value, we convert this into a JS
+// object with a property for each key/value pair. (This is the structure
+// we really want, but we can't express in directly in WebIDL.)
+// ----------------------------------------------------------------------
+
+// getHitTestConfig() expects apz_test_native_event_utils.js to be loaded as well.
+/* import-globals-from apz_test_native_event_utils.js */
+
+function convertEntries(entries) {
+ var result = {};
+ for (var i = 0; i < entries.length; ++i) {
+ result[entries[i].key] = entries[i].value;
+ }
+ return result;
+}
+
+function parsePoint(str) {
+ var pieces = str.replace(/[()\s]+/g, "").split(",");
+ SimpleTest.is(pieces.length, 2, "expected string of form (x,y)");
+ for (var i = 0; i < 2; i++) {
+ var eq = pieces[i].indexOf("=");
+ if (eq >= 0) {
+ pieces[i] = pieces[i].substring(eq + 1);
+ }
+ }
+ return {
+ x: parseInt(pieces[0]),
+ y: parseInt(pieces[1]),
+ };
+}
+
+// Given a VisualViewport object, return the visual viewport
+// rect relative to the page.
+function getVisualViewportRect(vv) {
+ return {
+ x: vv.pageLeft,
+ y: vv.pageTop,
+ width: vv.width,
+ height: vv.height,
+ };
+}
+
+// Return the offset of the visual viewport relative to the layout viewport.
+function getRelativeViewportOffset(window) {
+ const offsetX = {};
+ const offsetY = {};
+ const utils = SpecialPowers.getDOMWindowUtils(window);
+ utils.getVisualViewportOffsetRelativeToLayoutViewport(offsetX, offsetY);
+ return {
+ x: offsetX.value,
+ y: offsetY.value,
+ };
+}
+
+function parseRect(str) {
+ var pieces = str.replace(/[()\s]+/g, "").split(",");
+ SimpleTest.is(pieces.length, 4, "expected string of form (x,y,w,h)");
+ for (var i = 0; i < 4; i++) {
+ var eq = pieces[i].indexOf("=");
+ if (eq >= 0) {
+ pieces[i] = pieces[i].substring(eq + 1);
+ }
+ }
+ return {
+ x: parseInt(pieces[0]),
+ y: parseInt(pieces[1]),
+ width: parseInt(pieces[2]),
+ height: parseInt(pieces[3]),
+ };
+}
+
+// These functions expect rects with fields named x/y/width/height, such as
+// that returned by parseRect().
+function rectContains(haystack, needle) {
+ return (
+ haystack.x <= needle.x &&
+ haystack.y <= needle.y &&
+ haystack.x + haystack.width >= needle.x + needle.width &&
+ haystack.y + haystack.height >= needle.y + needle.height
+ );
+}
+function rectToString(rect) {
+ return (
+ "(" + rect.x + "," + rect.y + "," + rect.width + "," + rect.height + ")"
+ );
+}
+function assertRectContainment(
+ haystackRect,
+ haystackDesc,
+ needleRect,
+ needleDesc
+) {
+ SimpleTest.ok(
+ rectContains(haystackRect, needleRect),
+ haystackDesc +
+ " " +
+ rectToString(haystackRect) +
+ " should contain " +
+ needleDesc +
+ " " +
+ rectToString(needleRect)
+ );
+}
+
+function getPropertyAsRect(scrollFrames, scrollId, prop) {
+ SimpleTest.ok(
+ scrollId in scrollFrames,
+ "expected scroll frame data for scroll id " + scrollId
+ );
+ var scrollFrameData = scrollFrames[scrollId];
+ SimpleTest.ok(
+ "displayport" in scrollFrameData,
+ "expected a " + prop + " for scroll id " + scrollId
+ );
+ var value = scrollFrameData[prop];
+ return parseRect(value);
+}
+
+function convertScrollFrameData(scrollFrames) {
+ var result = {};
+ for (var i = 0; i < scrollFrames.length; ++i) {
+ result[scrollFrames[i].scrollId] = convertEntries(scrollFrames[i].entries);
+ }
+ return result;
+}
+
+function convertBuckets(buckets) {
+ var result = {};
+ for (var i = 0; i < buckets.length; ++i) {
+ result[buckets[i].sequenceNumber] = convertScrollFrameData(
+ buckets[i].scrollFrames
+ );
+ }
+ return result;
+}
+
+function convertTestData(testData) {
+ var result = {};
+ result.paints = convertBuckets(testData.paints);
+ result.repaintRequests = convertBuckets(testData.repaintRequests);
+ return result;
+}
+
+// Returns the last bucket that has at least one scrollframe. This
+// is useful for skipping over buckets that are from empty transactions,
+// because those don't contain any useful data.
+function getLastNonemptyBucket(buckets) {
+ for (var i = buckets.length - 1; i >= 0; --i) {
+ if (buckets[i].scrollFrames.length) {
+ return buckets[i];
+ }
+ }
+ return null;
+}
+
+// Takes something like "matrix(1, 0, 0, 1, 234.024, 528.29023)"" and returns a number array
+function parseTransform(transform) {
+ return /matrix\((.*),(.*),(.*),(.*),(.*),(.*)\)/
+ .exec(transform)
+ .slice(1)
+ .map(parseFloat);
+}
+
+function isTransformClose(a, b, name) {
+ is(
+ a.length,
+ b.length,
+ `expected transforms ${a} and ${b} to be the same length`
+ );
+ for (let i = 0; i < a.length; i++) {
+ ok(Math.abs(a[i] - b[i]) < 0.01, name);
+ }
+}
+
+// Given APZ test data for a single paint on the compositor side,
+// reconstruct the APZC tree structure from the 'parentScrollId'
+// entries that were logged. More specifically, the subset of the
+// APZC tree structure corresponding to the layer subtree for the
+// content process that triggered the paint, is reconstructed (as
+// the APZ test data only contains information abot this subtree).
+function buildApzcTree(paint) {
+ // The APZC tree can potentially have multiple root nodes,
+ // so we invent a node that is the parent of all roots.
+ // This 'root' does not correspond to an APZC.
+ var root = { scrollId: -1, children: [] };
+ for (let scrollId in paint) {
+ paint[scrollId].children = [];
+ paint[scrollId].scrollId = scrollId;
+ }
+ for (let scrollId in paint) {
+ var parentNode = null;
+ if ("hasNoParentWithSameLayersId" in paint[scrollId]) {
+ parentNode = root;
+ } else if ("parentScrollId" in paint[scrollId]) {
+ parentNode = paint[paint[scrollId].parentScrollId];
+ }
+ parentNode.children.push(paint[scrollId]);
+ }
+ return root;
+}
+
+// Given an APZC tree produced by buildApzcTree, return the RCD node in
+// the tree, or null if there was none.
+function findRcdNode(apzcTree) {
+ // isRootContent will be undefined or "1"
+ if (apzcTree.isRootContent) {
+ return apzcTree;
+ }
+ for (var i = 0; i < apzcTree.children.length; i++) {
+ var rcd = findRcdNode(apzcTree.children[i]);
+ if (rcd != null) {
+ return rcd;
+ }
+ }
+ return null;
+}
+
+// Return whether an element whose id includes |elementId| has been layerized.
+// Assumes |elementId| will be present in the content description for the
+// element, and not in the content descriptions of other elements.
+function isLayerized(elementId) {
+ var contentTestData =
+ SpecialPowers.getDOMWindowUtils(window).getContentAPZTestData();
+ var nonEmptyBucket = getLastNonemptyBucket(contentTestData.paints);
+ ok(nonEmptyBucket != null, "expected at least one nonempty paint");
+ var seqno = nonEmptyBucket.sequenceNumber;
+ contentTestData = convertTestData(contentTestData);
+ var paint = contentTestData.paints[seqno];
+ for (var scrollId in paint) {
+ if ("contentDescription" in paint[scrollId]) {
+ if (paint[scrollId].contentDescription.includes(elementId)) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+// Return a rect (or null) that holds the last known content-side displayport
+// for a given element. (The element selection works the same way, and with
+// the same assumptions as the isLayerized function above).
+function getLastContentDisplayportFor(elementId, expectPainted = true) {
+ var contentTestData =
+ SpecialPowers.getDOMWindowUtils(window).getContentAPZTestData();
+ if (contentTestData == undefined) {
+ ok(!expectPainted, "expected to have apz test data (1)");
+ return null;
+ }
+ var nonEmptyBucket = getLastNonemptyBucket(contentTestData.paints);
+ if (nonEmptyBucket == null) {
+ ok(!expectPainted, "expected to have apz test data (2)");
+ return null;
+ }
+ var seqno = nonEmptyBucket.sequenceNumber;
+ contentTestData = convertTestData(contentTestData);
+ var paint = contentTestData.paints[seqno];
+ for (var scrollId in paint) {
+ if ("contentDescription" in paint[scrollId]) {
+ if (paint[scrollId].contentDescription.includes(elementId)) {
+ if ("displayport" in paint[scrollId]) {
+ return parseRect(paint[scrollId].displayport);
+ }
+ }
+ }
+ }
+ return null;
+}
+
+// Return the APZC tree (as produced by buildApzcTree) for the last
+// non-empty paint received by the compositor.
+function getLastApzcTree() {
+ let data = SpecialPowers.getDOMWindowUtils(window).getCompositorAPZTestData();
+ if (data == undefined) {
+ ok(false, "expected to have compositor apz test data");
+ return null;
+ }
+ if (!data.paints.length) {
+ ok(false, "expected to have at least one compositor paint bucket");
+ return null;
+ }
+ var seqno = data.paints[data.paints.length - 1].sequenceNumber;
+ data = convertTestData(data);
+ return buildApzcTree(data.paints[seqno]);
+}
+
+// Return a promise that is resolved on the next rAF callback
+function promiseFrame(aWindow = window) {
+ return new Promise(resolve => {
+ aWindow.requestAnimationFrame(resolve);
+ });
+}
+
+// Return a promise that is resolved on the next MozAfterPaint event
+function promiseAfterPaint() {
+ return new Promise(resolve => {
+ window.addEventListener("MozAfterPaint", resolve, { once: true });
+ });
+}
+
+// This waits until any pending events on the APZ controller thread are
+// processed, and any resulting repaint requests are received by the main
+// thread. Note that while the repaint requests do get processed by the
+// APZ handler on the main thread, the repaints themselves may not have
+// occurred by the the returned promise resolves. If you want to wait
+// for those repaints, consider using promiseApzFlushedRepaints instead.
+function promiseOnlyApzControllerFlushedWithoutSetTimeout(aWindow = window) {
+ return new Promise(function (resolve, reject) {
+ var repaintDone = function () {
+ dump("PromiseApzRepaintsFlushed: APZ flush done\n");
+ SpecialPowers.Services.obs.removeObserver(
+ repaintDone,
+ "apz-repaints-flushed"
+ );
+ resolve();
+ };
+ SpecialPowers.Services.obs.addObserver(repaintDone, "apz-repaints-flushed");
+ if (SpecialPowers.getDOMWindowUtils(aWindow).flushApzRepaints()) {
+ dump(
+ "PromiseApzRepaintsFlushed: Flushed APZ repaints, waiting for callback...\n"
+ );
+ } else {
+ dump(
+ "PromiseApzRepaintsFlushed: Flushing APZ repaints was a no-op, triggering callback directly...\n"
+ );
+ repaintDone();
+ }
+ });
+}
+
+// Another variant of the above promiseOnlyApzControllerFlushedWithoutSetTimeout
+// but with a setTimeout(0) callback.
+function promiseOnlyApzControllerFlushed(aWindow = window) {
+ return new Promise(resolve => {
+ promiseOnlyApzControllerFlushedWithoutSetTimeout(aWindow).then(() => {
+ setTimeout(resolve, 0);
+ });
+ });
+}
+
+// Flush repaints, APZ pending repaints, and any repaints resulting from that
+// flush. This is particularly useful if the test needs to reach some sort of
+// "idle" state in terms of repaints. Usually just waiting for all paints
+// followed by flushApzRepaints is sufficient to flush all APZ state back to
+// the main thread, but it can leave a paint scheduled which will get triggered
+// at some later time. For tests that specifically test for painting at
+// specific times, this method is the way to go. Even if in doubt, this is the
+// preferred method as the extra step is "safe" and shouldn't interfere with
+// most tests.
+async function promiseApzFlushedRepaints() {
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ await promiseAllPaintsDone();
+}
+
+// This function takes a set of subtests to run one at a time in new top-level
+// windows, and returns a Promise that is resolved once all the subtests are
+// done running.
+//
+// The aSubtests array is an array of objects with the following keys:
+// file: required, the filename of the subtest.
+// prefs: optional, an array of arrays containing key-value prefs to set.
+// dp_suppression: optional, a boolean on whether or not to respect displayport
+// suppression during the test.
+// onload: optional, a function that will be registered as a load event listener
+// for the child window that will hold the subtest. the function will be
+// passed exactly one argument, which will be the child window.
+// An example of an array is:
+// aSubtests = [
+// { 'file': 'test_file_name.html' },
+// { 'file': 'test_file_2.html', 'prefs': [['pref.name', true], ['other.pref', 1000]], 'dp_suppression': false }
+// { 'file': 'file_3.html', 'onload': function(w) { w.subtestDone(); } }
+// ];
+//
+// Each subtest should call one of the subtestDone() or subtestFailed()
+// functions when it is done, to indicate that the window should be torn
+// down and the next test should run.
+// These functions are injected into the subtest's window by this
+// function prior to loading the subtest. For convenience, the |is| and |ok|
+// functions provided by SimpleTest are also mapped into the subtest's window.
+// For other things from the parent, the subtest can use window.opener.<whatever>
+// to access objects.
+function runSubtestsSeriallyInFreshWindows(aSubtests) {
+ return new Promise(function (resolve, reject) {
+ var testIndex = -1;
+ var w = null;
+
+ // If the "apz.subtest" pref has been set, only a single subtest whose name matches
+ // the pref's value (if any) will be run.
+ var onlyOneSubtest = SpecialPowers.getCharPref(
+ "apz.subtest",
+ /* default = */ ""
+ );
+
+ function advanceSubtestExecutionWithFailure(msg) {
+ SimpleTest.ok(false, msg);
+ advanceSubtestExecution();
+ }
+
+ async function advanceSubtestExecution() {
+ var test = aSubtests[testIndex];
+ if (w) {
+ // Run any cleanup functions registered in the subtest
+ // Guard against the subtest not loading apz_test_utils.js
+ if (w.ApzCleanup) {
+ w.ApzCleanup.execute();
+ }
+ if (typeof test.dp_suppression != "undefined") {
+ // We modified the suppression when starting the test, so now undo that.
+ SpecialPowers.getDOMWindowUtils(window).respectDisplayPortSuppression(
+ !test.dp_suppression
+ );
+ }
+
+ if (test.prefs) {
+ // We pushed some prefs for this test, pop them, and re-invoke
+ // advanceSubtestExecution() after that's been processed
+ SpecialPowers.popPrefEnv(function () {
+ w.close();
+ w = null;
+ advanceSubtestExecution();
+ });
+ return;
+ }
+
+ w.close();
+ }
+
+ testIndex++;
+ if (testIndex >= aSubtests.length) {
+ resolve();
+ return;
+ }
+
+ await SimpleTest.promiseFocus(window);
+
+ test = aSubtests[testIndex];
+
+ let recognizedProps = ["file", "prefs", "dp_suppression", "onload"];
+ for (let prop in test) {
+ if (!recognizedProps.includes(prop)) {
+ SimpleTest.ok(
+ false,
+ "Subtest " + test.file + " has unrecognized property '" + prop + "'"
+ );
+ setTimeout(function () {
+ advanceSubtestExecution();
+ }, 0);
+ return;
+ }
+ }
+
+ if (onlyOneSubtest && onlyOneSubtest != test.file) {
+ SimpleTest.ok(
+ true,
+ "Skipping " +
+ test.file +
+ " because only " +
+ onlyOneSubtest +
+ " is being run"
+ );
+ setTimeout(function () {
+ advanceSubtestExecution();
+ }, 0);
+ return;
+ }
+
+ SimpleTest.ok(true, "Starting subtest " + test.file);
+
+ if (typeof test.dp_suppression != "undefined") {
+ // Normally during a test, the displayport will get suppressed during page
+ // load, and unsuppressed at a non-deterministic time during the test. The
+ // unsuppression can trigger a repaint which interferes with the test, so
+ // to avoid that we can force the displayport to be unsuppressed for the
+ // entire test which is more deterministic.
+ SpecialPowers.getDOMWindowUtils(window).respectDisplayPortSuppression(
+ test.dp_suppression
+ );
+ }
+
+ function spawnTest(aFile) {
+ w = window.open("", "_blank");
+ w.subtestDone = advanceSubtestExecution;
+ w.subtestFailed = advanceSubtestExecutionWithFailure;
+ w.isApzSubtest = true;
+ w.SimpleTest = SimpleTest;
+ w.dump = function (msg) {
+ return dump(aFile + " | " + msg);
+ };
+ w.info = function (msg) {
+ return info(aFile + " | " + msg);
+ };
+ w.is = function (a, b, msg) {
+ return is(a, b, aFile + " | " + msg);
+ };
+ w.isnot = function (a, b, msg) {
+ return isnot(a, b, aFile + " | " + msg);
+ };
+ w.isfuzzy = function (a, b, eps, msg) {
+ return isfuzzy(a, b, eps, aFile + " | " + msg);
+ };
+ w.ok = function (cond, msg) {
+ arguments[1] = aFile + " | " + msg;
+ // Forward all arguments to SimpleTest.ok where we will check that ok() was
+ // called with at most 2 arguments.
+ return SimpleTest.ok.apply(SimpleTest, arguments);
+ };
+ w.todo_is = function (a, b, msg) {
+ return todo_is(a, b, aFile + " | " + msg);
+ };
+ w.todo = function (cond, msg) {
+ return todo(cond, aFile + " | " + msg);
+ };
+ if (test.onload) {
+ w.addEventListener(
+ "load",
+ function (e) {
+ test.onload(w);
+ },
+ { once: true }
+ );
+ }
+ var subtestUrl =
+ location.href.substring(0, location.href.lastIndexOf("/") + 1) +
+ aFile;
+ function urlResolves(url) {
+ var request = new XMLHttpRequest();
+ request.open("GET", url, false);
+ request.send();
+ return request.status !== 404;
+ }
+ if (!urlResolves(subtestUrl)) {
+ SimpleTest.ok(
+ false,
+ "Subtest URL " +
+ subtestUrl +
+ " does not resolve. " +
+ "Be sure it's present in the support-files section of mochitest.ini."
+ );
+ reject();
+ return undefined;
+ }
+ w.location = subtestUrl;
+ return w;
+ }
+
+ if (test.prefs) {
+ // Got some prefs for this subtest, push them
+ await SpecialPowers.pushPrefEnv({ set: test.prefs });
+ }
+ w = spawnTest(test.file);
+ }
+
+ advanceSubtestExecution();
+ }).catch(function (e) {
+ SimpleTest.ok(false, "Error occurred while running subtests: " + e);
+ });
+}
+
+function pushPrefs(prefs) {
+ return SpecialPowers.pushPrefEnv({ set: prefs });
+}
+
+async function waitUntilApzStable() {
+ if (!SpecialPowers.isMainProcess()) {
+ // We use this waitUntilApzStable function during test initialization
+ // and for those scenarios we want to flush the parent-process layer
+ // tree to the compositor and wait for that as well. That way we know
+ // that not only is the content-process layer tree ready in the compositor,
+ // the parent-process layer tree in the compositor has the appropriate
+ // RefLayer pointing to the content-process layer tree.
+
+ // Sadly this helper function cannot reuse any code from other places because
+ // it must be totally self-contained to be shipped over to the parent process.
+ function parentProcessFlush() {
+ /* eslint-env mozilla/chrome-script */
+ function apzFlush() {
+ var topWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!topWin) {
+ topWin = Services.wm.getMostRecentWindow("navigator:geckoview");
+ }
+ var topUtils = topWin.windowUtils;
+
+ var repaintDone = function () {
+ dump("WaitUntilApzStable: APZ flush done in parent proc\n");
+ Services.obs.removeObserver(repaintDone, "apz-repaints-flushed");
+ // send message back to content process
+ sendAsyncMessage("apz-flush-done", null);
+ };
+ var flushRepaint = function () {
+ if (topUtils.isMozAfterPaintPending) {
+ topWin.addEventListener("MozAfterPaint", flushRepaint, {
+ once: true,
+ });
+ return;
+ }
+
+ Services.obs.addObserver(repaintDone, "apz-repaints-flushed");
+ if (topUtils.flushApzRepaints()) {
+ dump(
+ "WaitUntilApzStable: flushed APZ repaints in parent proc, waiting for callback...\n"
+ );
+ } else {
+ dump(
+ "WaitUntilApzStable: flushing APZ repaints in parent proc was a no-op, triggering callback directly...\n"
+ );
+ repaintDone();
+ }
+ };
+
+ // Flush APZ repaints, but wait until all the pending paints have been
+ // sent.
+ flushRepaint();
+ }
+ function cleanup() {
+ removeMessageListener("apz-flush", apzFlush);
+ removeMessageListener("cleanup", cleanup);
+ }
+ addMessageListener("apz-flush", apzFlush);
+ addMessageListener("cleanup", cleanup);
+ }
+
+ // This is the first time waitUntilApzStable is being called, do initialization
+ if (typeof waitUntilApzStable.chromeHelper == "undefined") {
+ waitUntilApzStable.chromeHelper =
+ SpecialPowers.loadChromeScript(parentProcessFlush);
+ ApzCleanup.register(() => {
+ waitUntilApzStable.chromeHelper.sendAsyncMessage("cleanup", null);
+ waitUntilApzStable.chromeHelper.destroy();
+ delete waitUntilApzStable.chromeHelper;
+ });
+ }
+
+ // Actually trigger the parent-process flush and wait for it to finish
+ waitUntilApzStable.chromeHelper.sendAsyncMessage("apz-flush", null);
+ await waitUntilApzStable.chromeHelper.promiseOneMessage("apz-flush-done");
+ dump("WaitUntilApzStable: got apz-flush-done in child proc\n");
+ }
+
+ await SimpleTest.promiseFocus(window);
+ dump("WaitUntilApzStable: done promiseFocus\n");
+ await promiseAllPaintsDone();
+ dump("WaitUntilApzStable: done promiseAllPaintsDone\n");
+ await promiseOnlyApzControllerFlushed();
+ dump("WaitUntilApzStable: all done\n");
+}
+
+// This function returns a promise that is resolved after at least one paint
+// has been sent and processed by the compositor. This function can force
+// such a paint to happen if none are pending. This is useful to run after
+// the waitUntilApzStable() but before reading the compositor-side APZ test
+// data, because the test data for the content layers id only gets populated
+// on content layer tree updates *after* the root layer tree has a RefLayer
+// pointing to the contnet layer tree. waitUntilApzStable itself guarantees
+// that the root layer tree is pointing to the content layer tree, but does
+// not guarantee the subsequent paint; this function does that job.
+async function forceLayerTreeToCompositor() {
+ // Modify a style property to force a layout flush
+ document.body.style.boxSizing = "border-box";
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ if (!utils.isMozAfterPaintPending) {
+ dump("Forcing a paint since none was pending already...\n");
+ var testMode = utils.isTestControllingRefreshes;
+ utils.advanceTimeAndRefresh(0);
+ if (!testMode) {
+ utils.restoreNormalRefresh();
+ }
+ }
+ await promiseAllPaintsDone(null, true);
+ await promiseOnlyApzControllerFlushed();
+}
+
+function isApzEnabled() {
+ var enabled = SpecialPowers.getDOMWindowUtils(window).asyncPanZoomEnabled;
+ if (!enabled) {
+ // All tests are required to have at least one assertion. Since APZ is
+ // disabled, and the main test is presumably not going to run, we stick in
+ // a dummy assertion here to keep the test passing.
+ SimpleTest.ok(true, "APZ is not enabled; this test will be skipped");
+ }
+ return enabled;
+}
+
+function isKeyApzEnabled() {
+ return isApzEnabled() && SpecialPowers.getBoolPref("apz.keyboard.enabled");
+}
+
+// Take a snapshot of the given rect, *including compositor transforms* (i.e.
+// includes async scroll transforms applied by APZ). If you don't need the
+// compositor transforms, you can probably get away with using
+// SpecialPowers.snapshotWindowWithOptions or one of the friendlier wrappers.
+// The rect provided is expected to be relative to the screen, for example as
+// returned by rectRelativeToScreen in apz_test_native_event_utils.js.
+// Example usage:
+// var snapshot = getSnapshot(rectRelativeToScreen(myDiv));
+// which will take a snapshot of the 'myDiv' element. Note that if part of the
+// element is obscured by other things on top, the snapshot will include those
+// things. If it is clipped by a scroll container, the snapshot will include
+// that area anyway, so you will probably get parts of the scroll container in
+// the snapshot. If the rect extends outside the browser window then the
+// results are undefined.
+// The snapshot is returned in the form of a data URL.
+function getSnapshot(rect) {
+ function parentProcessSnapshot() {
+ /* eslint-env mozilla/chrome-script */
+ addMessageListener("snapshot", function (parentRect) {
+ var topWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!topWin) {
+ topWin = Services.wm.getMostRecentWindow("navigator:geckoview");
+ }
+
+ // reposition the rect relative to the top-level browser window
+ parentRect = JSON.parse(parentRect);
+ parentRect.x -= topWin.mozInnerScreenX;
+ parentRect.y -= topWin.mozInnerScreenY;
+
+ // take the snapshot
+ var canvas = topWin.document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "canvas"
+ );
+ canvas.width = parentRect.width;
+ canvas.height = parentRect.height;
+ var ctx = canvas.getContext("2d");
+ ctx.drawWindow(
+ topWin,
+ parentRect.x,
+ parentRect.y,
+ parentRect.width,
+ parentRect.height,
+ "rgb(255,255,255)",
+ ctx.DRAWWINDOW_DRAW_VIEW |
+ ctx.DRAWWINDOW_USE_WIDGET_LAYERS |
+ ctx.DRAWWINDOW_DRAW_CARET
+ );
+ return canvas.toDataURL();
+ });
+ }
+
+ if (typeof getSnapshot.chromeHelper == "undefined") {
+ // This is the first time getSnapshot is being called; do initialization
+ getSnapshot.chromeHelper = SpecialPowers.loadChromeScript(
+ parentProcessSnapshot
+ );
+ ApzCleanup.register(function () {
+ getSnapshot.chromeHelper.destroy();
+ });
+ }
+
+ return getSnapshot.chromeHelper.sendQuery("snapshot", JSON.stringify(rect));
+}
+
+// Takes the document's query string and parses it, assuming the query string
+// is composed of key-value pairs where the value is in JSON format. The object
+// returned contains the various values indexed by their respective keys. In
+// case of duplicate keys, the last value be used.
+// Examples:
+// ?key="value"&key2=false&key3=500
+// produces { "key": "value", "key2": false, "key3": 500 }
+// ?key={"x":0,"y":50}&key2=[1,2,true]
+// produces { "key": { "x": 0, "y": 0 }, "key2": [1, 2, true] }
+function getQueryArgs() {
+ var args = {};
+ if (location.search.length) {
+ var params = location.search.substr(1).split("&");
+ for (var p of params) {
+ var [k, v] = p.split("=");
+ args[k] = JSON.parse(v);
+ }
+ }
+ return args;
+}
+
+// An async function that inserts a script element with the given URI into
+// the head of the document of the given window. This function returns when
+// the load or error event fires on the script element, indicating completion.
+async function injectScript(aScript, aWindow = window) {
+ var e = aWindow.document.createElement("script");
+ e.type = "text/javascript";
+ let loadPromise = new Promise((resolve, reject) => {
+ e.onload = function () {
+ resolve();
+ };
+ e.onerror = function () {
+ dump("Script [" + aScript + "] errored out\n");
+ reject();
+ };
+ });
+ e.src = aScript;
+ aWindow.document.getElementsByTagName("head")[0].appendChild(e);
+ await loadPromise;
+}
+
+// Compute some configuration information used for hit testing.
+// The computed information is cached to avoid recomputing it
+// each time this function is called.
+// The computed information is an object with three fields:
+// utils: the nsIDOMWindowUtils instance for this window
+// isWindow: true if the platform is Windows
+// activateAllScrollFrames: true if prefs indicate all scroll frames are
+// activated with at least a minimal display port
+function getHitTestConfig() {
+ if (!("hitTestConfig" in window)) {
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ var isWindows = getPlatform() == "windows";
+ let activateAllScrollFrames =
+ SpecialPowers.getBoolPref("apz.wr.activate_all_scroll_frames") ||
+ (SpecialPowers.getBoolPref(
+ "apz.wr.activate_all_scroll_frames_when_fission"
+ ) &&
+ SpecialPowers.Services.appinfo.fissionAutostart);
+
+ window.hitTestConfig = {
+ utils,
+ isWindows,
+ activateAllScrollFrames,
+ };
+ }
+ return window.hitTestConfig;
+}
+
+// Compute the coordinates of the center of the given element. The argument
+// can either be a string (the id of the element desired) or the element
+// itself.
+function centerOf(element) {
+ if (typeof element === "string") {
+ element = document.getElementById(element);
+ }
+ var bounds = element.getBoundingClientRect();
+ return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
+}
+
+// Peform a compositor hit test at the given point and return the result.
+// |point| is expected to be in CSS coordinates relative to the layout
+// viewport, since this is what sendMouseEvent() expects. (Note that this
+// is different from sendNativeMouseEvent() which expects screen coordinates
+// relative to the screen.)
+// The returned object has two fields:
+// hitInfo: a combination of APZHitResultFlags
+// scrollId: the view-id of the scroll frame that was hit
+function hitTest(point) {
+ var utils = getHitTestConfig().utils;
+ dump("Hit-testing point (" + point.x + ", " + point.y + ")\n");
+ utils.sendMouseEvent(
+ "MozMouseHittest",
+ point.x,
+ point.y,
+ 0,
+ 0,
+ 0,
+ true,
+ 0,
+ 0,
+ true,
+ true
+ );
+ var data = utils.getCompositorAPZTestData();
+ ok(
+ data.hitResults.length >= 1,
+ "Expected at least one hit result in the APZTestData"
+ );
+ var result = data.hitResults[data.hitResults.length - 1];
+ return {
+ hitInfo: result.hitResult,
+ scrollId: result.scrollId,
+ layersId: result.layersId,
+ };
+}
+
+// Returns a canonical stringification of the hitInfo bitfield.
+function hitInfoToString(hitInfo) {
+ var strs = [];
+ for (var flag in APZHitResultFlags) {
+ if ((hitInfo & APZHitResultFlags[flag]) != 0) {
+ strs.push(flag);
+ }
+ }
+ if (!strs.length) {
+ return "INVISIBLE";
+ }
+ strs.sort(function (a, b) {
+ return APZHitResultFlags[a] - APZHitResultFlags[b];
+ });
+ return strs.join(" | ");
+}
+
+// Takes an object returned by hitTest, along with the expected values, and
+// asserts that they match. Notably, it uses hitInfoToString to provide a
+// more useful message for the case that the hit info doesn't match
+function checkHitResult(
+ hitResult,
+ expectedHitInfo,
+ expectedScrollId,
+ expectedLayersId,
+ desc
+) {
+ is(
+ hitInfoToString(hitResult.hitInfo),
+ hitInfoToString(expectedHitInfo),
+ desc + " hit info"
+ );
+ is(hitResult.scrollId, expectedScrollId, desc + " scrollid");
+ is(hitResult.layersId, expectedLayersId, desc + " layersid");
+}
+
+// Symbolic constants used by hitTestScrollbar().
+var ScrollbarTrackLocation = {
+ START: 1,
+ END: 2,
+};
+var LayerState = {
+ ACTIVE: 1,
+ INACTIVE: 2,
+};
+
+// Perform a hit test on the scrollbar(s) of a scroll frame.
+// This function takes a single argument which is expected to be
+// an object with the following fields:
+// element: The scroll frame to perform the hit test on.
+// directions: The direction(s) of scrollbars to test.
+// If directions.vertical is true, the vertical scrollbar will be tested.
+// If directions.horizontal is true, the horizontal scrollbar will be tested.
+// Both may be true in a single call (in which case two tests are performed).
+// expectedScrollId: The scroll id that is expected to be hit, if activateAllScrollFrames is false.
+// expectedLayersId: The layers id that is expected to be hit.
+// trackLocation: One of ScrollbarTrackLocation.{START, END}.
+// Determines which end of the scrollbar track is targeted.
+// expectThumb: Whether the scrollbar thumb is expected to be present
+// at the targeted end of the scrollbar track.
+// layerState: Whether the scroll frame is active or inactive.
+// The function performs the hit tests and asserts that the returned
+// hit test information is consistent with the passed parameters.
+// There is no return value.
+// Tests that use this function must set the pref
+// "layout.scrollbars.always-layerize-track".
+function hitTestScrollbar(params) {
+ var config = getHitTestConfig();
+
+ var elem = params.element;
+
+ var boundingClientRect = elem.getBoundingClientRect();
+
+ var verticalScrollbarWidth = boundingClientRect.width - elem.clientWidth;
+ var horizontalScrollbarHeight = boundingClientRect.height - elem.clientHeight;
+
+ // On windows, the scrollbar tracks have buttons on the end. When computing
+ // coordinates for hit-testing we need to account for this. We assume the
+ // buttons are square, and so can use the scrollbar width/height to estimate
+ // the size of the buttons
+ var scrollbarArrowButtonHeight = config.isWindows
+ ? verticalScrollbarWidth
+ : 0;
+ var scrollbarArrowButtonWidth = config.isWindows
+ ? horizontalScrollbarHeight
+ : 0;
+
+ // Compute the expected hit result flags.
+ // The direction flag (APZHitResultFlags.SCROLLBAR_VERTICAL) is added in
+ // later, for the vertical test only.
+ // The APZHitResultFlags.SCROLLBAR flag will be present regardless of whether
+ // the layer is active or inactive because we force layerization of scrollbar
+ // tracks. Unfortunately not forcing the layerization results in different
+ // behaviour on different platforms which makes testing harder.
+ var expectedHitInfo = APZHitResultFlags.VISIBLE | APZHitResultFlags.SCROLLBAR;
+ if (params.expectThumb) {
+ // The thumb has listeners which are APZ-aware.
+ expectedHitInfo |= APZHitResultFlags.APZ_AWARE_LISTENERS;
+ var expectActive =
+ config.activateAllScrollFrames || params.layerState == LayerState.ACTIVE;
+ if (!expectActive) {
+ expectedHitInfo |= APZHitResultFlags.INACTIVE_SCROLLFRAME;
+ }
+ // We do not generate the layers for thumbs on inactive scrollframes.
+ if (expectActive) {
+ expectedHitInfo |= APZHitResultFlags.SCROLLBAR_THUMB;
+ }
+ }
+
+ var expectedScrollId = params.expectedScrollId;
+ if (config.activateAllScrollFrames) {
+ expectedScrollId = config.utils.getViewId(params.element);
+ if (params.layerState == LayerState.ACTIVE) {
+ is(
+ expectedScrollId,
+ params.expectedScrollId,
+ "Expected scrollId for active scrollframe should match"
+ );
+ }
+ }
+
+ var scrollframeMsg =
+ params.layerState == LayerState.ACTIVE
+ ? "active scrollframe"
+ : "inactive scrollframe";
+
+ // Hit-test the targeted areas, assuming we don't have overlay scrollbars
+ // with zero dimensions.
+ if (params.directions.vertical && verticalScrollbarWidth > 0) {
+ var verticalScrollbarPoint = {
+ x: boundingClientRect.right - verticalScrollbarWidth / 2,
+ y:
+ params.trackLocation == ScrollbarTrackLocation.START
+ ? boundingClientRect.y + scrollbarArrowButtonHeight + 5
+ : boundingClientRect.bottom -
+ horizontalScrollbarHeight -
+ scrollbarArrowButtonHeight -
+ 5,
+ };
+ checkHitResult(
+ hitTest(verticalScrollbarPoint),
+ expectedHitInfo | APZHitResultFlags.SCROLLBAR_VERTICAL,
+ expectedScrollId,
+ params.expectedLayersId,
+ scrollframeMsg + " - vertical scrollbar"
+ );
+ }
+
+ if (params.directions.horizontal && horizontalScrollbarHeight > 0) {
+ var horizontalScrollbarPoint = {
+ x:
+ params.trackLocation == ScrollbarTrackLocation.START
+ ? boundingClientRect.x + scrollbarArrowButtonWidth + 5
+ : boundingClientRect.right -
+ verticalScrollbarWidth -
+ scrollbarArrowButtonWidth -
+ 5,
+ y: boundingClientRect.bottom - horizontalScrollbarHeight / 2,
+ };
+ checkHitResult(
+ hitTest(horizontalScrollbarPoint),
+ expectedHitInfo,
+ expectedScrollId,
+ params.expectedLayersId,
+ scrollframeMsg + " - horizontal scrollbar"
+ );
+ }
+}
+
+// Return a list of prefs for the given test identifier.
+function getPrefs(ident) {
+ switch (ident) {
+ case "TOUCH_EVENTS:PAN":
+ return [
+ // Dropping the touch slop to 0 makes the tests easier to write because
+ // we can just do a one-pixel drag to get over the pan threshold rather
+ // than having to hard-code some larger value.
+ ["apz.touch_start_tolerance", "0.0"],
+ // The touchstart from the drag can turn into a long-tap if the touch-move
+ // events get held up. Try to prevent that by making long-taps require
+ // a 10 second hold. Note that we also cannot enable chaos mode on this
+ // test for this reason, since chaos mode can cause the long-press timer
+ // to fire sooner than the pref dictates.
+ ["ui.click_hold_context_menus.delay", 10000],
+ // The subtests in this test do touch-drags to pan the page, but we don't
+ // want those pans to turn into fling animations, so we increase the
+ // fling min velocity requirement absurdly high.
+ ["apz.fling_min_velocity_threshold", "10000"],
+ // The helper_div_pan's div gets a displayport on scroll, but if the
+ // test takes too long the displayport can expire before the new scroll
+ // position is synced back to the main thread. So we disable displayport
+ // expiry for these tests.
+ ["apz.displayport_expiry_ms", 0],
+ // We need to disable touch resampling during these tests because we
+ // rely on touch move events being processed without delay. Touch
+ // resampling only processes them once vsync fires.
+ ["android.touch_resampling.enabled", false],
+ ];
+ case "TOUCH_ACTION":
+ return [
+ ...getPrefs("TOUCH_EVENTS:PAN"),
+ ["apz.test.fails_with_native_injection", getPlatform() == "windows"],
+ ];
+ default:
+ return [];
+ }
+}
+
+var ApzCleanup = {
+ _cleanups: [],
+
+ register(func) {
+ if (!this._cleanups.length) {
+ if (!window.isApzSubtest) {
+ SimpleTest.registerCleanupFunction(this.execute.bind(this));
+ } // else ApzCleanup.execute is called from runSubtestsSeriallyInFreshWindows
+ }
+ this._cleanups.push(func);
+ },
+
+ execute() {
+ while (this._cleanups.length) {
+ var func = this._cleanups.pop();
+ try {
+ func();
+ } catch (ex) {
+ SimpleTest.ok(
+ false,
+ "Subtest cleanup function [" +
+ func.toString() +
+ "] threw exception [" +
+ ex +
+ "] on page [" +
+ location.href +
+ "]"
+ );
+ }
+ }
+ },
+};
+
+/**
+ * Returns a promise that will resolve if `eventTarget` receives an event of the
+ * given type that passes the given filter. Only the first matching message is
+ * used. The filter must be a function (or null); it is called with the event
+ * object and the call must return true to resolve the promise.
+ */
+function promiseOneEvent(eventTarget, eventType, filter) {
+ return new Promise((resolve, reject) => {
+ eventTarget.addEventListener(eventType, function listener(e) {
+ let success = false;
+ if (filter == null) {
+ success = true;
+ } else if (typeof filter == "function") {
+ try {
+ success = filter(e);
+ } catch (ex) {
+ dump(
+ `ERROR: Filter passed to promiseOneEvent threw exception: ${ex}\n`
+ );
+ reject();
+ return;
+ }
+ } else {
+ dump(
+ "ERROR: Filter passed to promiseOneEvent was neither null nor a function\n"
+ );
+ reject();
+ return;
+ }
+ if (success) {
+ eventTarget.removeEventListener(eventType, listener);
+ resolve(e);
+ }
+ });
+ });
+}
+
+function visualViewportAsZoomedRect() {
+ let vv = window.visualViewport;
+ return {
+ x: vv.pageLeft,
+ y: vv.pageTop,
+ w: vv.width,
+ h: vv.height,
+ z: vv.scale,
+ };
+}
+
+// Pulls the latest compositor APZ test data and checks to see if the
+// scroller with id `scrollerId` was checkerboarding. It also ensures that
+// a scroller with id `scrollerId` was actually found in the test data.
+// This function requires that "apz.test.logging_enabled" be set to true,
+// in order for the test data to be logged.
+function assertNotCheckerboarded(utils, scrollerId, msgPrefix) {
+ utils.advanceTimeAndRefresh(0);
+ var data = utils.getCompositorAPZTestData();
+ //dump(JSON.stringify(data, null, 4));
+ var found = false;
+ for (apzcData of data.additionalData) {
+ if (apzcData.key == scrollerId) {
+ var checkerboarding = apzcData.value
+ .split(",")
+ .includes("checkerboarding");
+ ok(!checkerboarding, `${msgPrefix}: scroller is not checkerboarding`);
+ found = true;
+ }
+ }
+ ok(found, `${msgPrefix}: Found the scroller in the APZ data`);
+ utils.restoreNormalRefresh();
+}
+
+async function waitToClearOutAnyPotentialScrolls(aWindow) {
+ await promiseFrame(aWindow);
+ await promiseFrame(aWindow);
+ await promiseOnlyApzControllerFlushed(aWindow);
+ await promiseFrame(aWindow);
+ await promiseFrame(aWindow);
+}
+
+function waitForScrollEvent(target) {
+ return new Promise(resolve => {
+ target.addEventListener("scroll", resolve, { once: true });
+ });
+}
+
+// This is another variant of promiseApzFlushedRepaints.
+// We need this function because, unfortunately, there is no easy way to use
+// paint_listeners.js' functions and apz_test_utils.js' functions in popup
+// contents opened by extensions either as scripts in the popup contents or
+// scripts inside SpecialPowers.spawn because we can't use privileged functions
+// in the popup contents' script, we can't use functions basically as it as in
+// the sandboxed context either.
+async function promiseApzFlushedRepaintsInPopup(popup) {
+ // Flush APZ repaints and waits for MozAfterPaint.
+ await SpecialPowers.spawn(popup, [], async () => {
+ const utils = SpecialPowers.getDOMWindowUtils(content.window);
+
+ async function promiseAllPaintsDone() {
+ return new Promise(resolve => {
+ function waitForPaints() {
+ if (utils.isMozAfterPaintPending) {
+ dump("Waits for a MozAfterPaint event\n");
+ content.window.addEventListener(
+ "MozAfterPaint",
+ () => {
+ dump("Got a MozAfterPaint event\n");
+ waitForPaints();
+ },
+ { once: true }
+ );
+ } else {
+ dump("No more pending MozAfterPaint\n");
+ content.window.setTimeout(resolve, 0);
+ }
+ }
+ waitForPaints();
+ });
+ }
+ await promiseAllPaintsDone();
+
+ await new Promise(resolve => {
+ var repaintDone = function () {
+ dump("APZ flush done\n");
+ SpecialPowers.Services.obs.removeObserver(
+ repaintDone,
+ "apz-repaints-flushed"
+ );
+ content.window.setTimeout(resolve, 0);
+ };
+ SpecialPowers.Services.obs.addObserver(
+ repaintDone,
+ "apz-repaints-flushed"
+ );
+ if (utils.flushApzRepaints()) {
+ dump("Flushed APZ repaints, waiting for callback...\n");
+ } else {
+ dump(
+ "Flushing APZ repaints was a no-op, triggering callback directly...\n"
+ );
+ repaintDone();
+ }
+ });
+
+ await promiseAllPaintsDone();
+ });
+}
+
+// A utility function to make sure there's no scroll animation on the given
+// |aElement|.
+async function cancelScrollAnimation(aElement, aWindow = window) {
+ // In fact there's no good way to directly cancel the active animation on the
+ // element, so we destroy the corresponding scrollable frame then reconstruct
+ // a new scrollable frame so that it clobbers the animation.
+ const originalStyle = aElement.style.display;
+ aElement.style.display = "none";
+ await aWindow.promiseApzFlushedRepaints();
+ aElement.style.display = originalStyle;
+ await aWindow.promiseApzFlushedRepaints();
+}
+
+function collectSampledScrollOffsets(aElement) {
+ let data = SpecialPowers.DOMWindowUtils.getCompositorAPZTestData();
+ let sampledResults = data.sampledResults;
+
+ const layersId = SpecialPowers.DOMWindowUtils.getLayersId();
+ const scrollId = SpecialPowers.DOMWindowUtils.getViewId(aElement);
+
+ return sampledResults.filter(
+ result =>
+ SpecialPowers.wrap(result).layersId == layersId &&
+ SpecialPowers.wrap(result).scrollId == scrollId
+ );
+}
+
+function cloneVisualViewport() {
+ return {
+ offsetLeft: visualViewport.offsetLeft,
+ offsetTop: visualViewport.offsetTop,
+ pageLeft: visualViewport.pageLeft,
+ pageTop: visualViewport.pageTop,
+ width: visualViewport.width,
+ height: visualViewport.height,
+ scale: visualViewport.scale,
+ };
+}
+
+function compareVisualViewport(
+ aVisualViewportValue1,
+ aVisualViewportValue2,
+ aMessage
+) {
+ for (let p in aVisualViewportValue1) {
+ // Due to the method difference of the calculation for double-tap-zoom in
+ // OOP iframes, we allow 1.0 difference in each visualViewport value.
+ // NOTE: Because of our layer pixel snapping (bug 1774315 and bug 1852884)
+ // the visual viewport metrics can have one more pixel difference so we
+ // allow it here.
+ const tolerance = 1.0 + 1.0;
+ isfuzzy(
+ aVisualViewportValue1[p],
+ aVisualViewportValue2[p],
+ aVisualViewportValue1.scale > 1.0
+ ? tolerance
+ : tolerance / aVisualViewportValue1.scale,
+ `${p} should be same on ${aMessage}`
+ );
+ }
+}
diff --git a/gfx/layers/apz/test/mochitest/browser.toml b/gfx/layers/apz/test/mochitest/browser.toml
new file mode 100644
index 0000000000..5432aa0ae1
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser.toml
@@ -0,0 +1,80 @@
+[DEFAULT]
+support-files = [
+ "apz_test_native_event_utils.js",
+ "apz_test_utils.js",
+ "helper_browser_test_utils.js",
+ "!/browser/base/content/test/forms/head.js",
+ "!/browser/components/extensions/test/browser/head.js",
+ "!/browser/components/extensions/test/browser/head_browserAction.js"
+]
+
+["browser_test_animations_without_apz_sampler.js"]
+
+["browser_test_autoscrolling_in_extension_popup_window.js"]
+
+["browser_test_autoscrolling_in_oop_frame.js"]
+support-files = ["helper_test_autoscrolling_in_oop_frame.html"]
+
+["browser_test_background_tab_load_scroll.js"]
+support-files = ["helper_background_tab_load_scroll.html"]
+
+["browser_test_background_tab_scroll.js"]
+skip-if = ["toolkit == 'android'"] # wheel events not supported on mobile
+support-files = ["helper_background_tab_scroll.html"]
+
+["browser_test_content_response_timeout.js"]
+support-files = ["helper_content_response_timeout.html"]
+
+["browser_test_group_fission.js"]
+skip-if = [
+ "win11_2009 && bits == 32", # intermittent failures on on win11/32
+ "os == 'linux' && bits == 64", # Bug 1773830
+]
+support-files = [
+ "FissionTestHelperParent.sys.mjs",
+ "FissionTestHelperChild.sys.mjs",
+ "helper_fission_*.*",
+ "!/dom/animation/test/testcommon.js"
+]
+
+["browser_test_position_sticky.js"]
+support-files = ["helper_position_sticky_flicker.html"]
+
+["browser_test_reset_scaling_zoom.js"]
+support-files = ["helper_test_reset_scaling_zoom.html"]
+
+["browser_test_scroll_thumb_dragging.js"]
+support-files = ["helper_scroll_thumb_dragging.html"]
+
+["browser_test_scrollbar_in_extension_popup_window.js"]
+skip-if = [
+ "verify",
+ "os == 'linux'" # Bug 1713052
+]
+
+["browser_test_scrolling_in_extension_popup_window.js"]
+skip-if = ["os == 'mac'"] # Bug 1784759
+
+["browser_test_scrolling_on_inactive_scroller_in_extension_popup_window.js"]
+run-if = ["os == 'mac'"] # bug 1700805
+
+["browser_test_select_popup_position.js"]
+support-files = [
+ "helper_test_select_popup_position.html",
+ "helper_test_select_popup_position_transformed_in_parent.html",
+ "helper_test_select_popup_position_zoomed.html"
+]
+
+["browser_test_select_zoom.js"]
+skip-if = ["os == 'win'"] # bug 1495580
+support-files = ["helper_test_select_zoom.html"]
+
+["browser_test_tab_drag_event_counts.js"]
+skip-if = ["os == 'linux'"] # No native key event support on Linux at this time (bug 1770143)
+support-files = [
+ "helper_test_tab_drag_event_counts.html"
+]
+
+["browser_test_tab_drag_zoom.js"]
+skip-if = ["os == 'win'"] # Our Windows touch injection test code doesn't support pinch gestures (bug 1495580)
+support-files = ["helper_test_tab_drag_zoom.html"]
diff --git a/gfx/layers/apz/test/mochitest/browser_test_animations_without_apz_sampler.js b/gfx/layers/apz/test/mochitest/browser_test_animations_without_apz_sampler.js
new file mode 100644
index 0000000000..8fdd20887a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_animations_without_apz_sampler.js
@@ -0,0 +1,134 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head_browserAction.js",
+ this
+);
+
+add_task(async () => {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "popup.html": `
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ #target {
+ width: 100px;
+ height: 50px;
+ background: green;
+ }
+ #target2 {
+ width: 100px;
+ height: 50px;
+ background: green;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="target"></div>
+ <div id="target2"></div>
+ </body>
+ </html>`,
+ },
+ });
+
+ await extension.startup();
+
+ async function takeSnapshot(browserWin, callback) {
+ let browser = await openBrowserActionPanel(extension, browserWin, true);
+
+ if (callback) {
+ await SpecialPowers.spawn(browser, [], callback);
+ }
+
+ // Ensure there's no pending paint requests.
+ // The below code is a simplified version of promiseAllPaintsDone in
+ // paint_listener.js.
+ await SpecialPowers.spawn(browser, [], async () => {
+ return new Promise(resolve => {
+ function waitForPaints() {
+ // Wait until paint suppression has ended
+ if (SpecialPowers.DOMWindowUtils.paintingSuppressed) {
+ dump`waiting for paint suppression to end...`;
+ content.window.setTimeout(waitForPaints, 0);
+ return;
+ }
+
+ if (SpecialPowers.DOMWindowUtils.isMozAfterPaintPending) {
+ dump`waiting for paint...`;
+ content.window.addEventListener("MozAfterPaint", waitForPaints, {
+ once: true,
+ });
+ return;
+ }
+ resolve();
+ }
+ waitForPaints();
+ });
+ });
+
+ const snapshot = await SpecialPowers.spawn(browser, [], async () => {
+ return SpecialPowers.snapshotWindowWithOptions(
+ content.window,
+ undefined /* use the default rect */,
+ undefined /* use the default bgcolor */,
+ { DRAWWINDOW_DRAW_VIEW: true } /* to capture scrollbars */
+ )
+ .toDataURL()
+ .toString();
+ });
+
+ const popup = getBrowserActionPopup(extension, browserWin);
+ await closeBrowserAction(extension, browserWin);
+ is(popup.state, "closed", "browserAction popup has been closed");
+
+ return snapshot;
+ }
+
+ // Test without apz sampler.
+ await SpecialPowers.pushPrefEnv({ set: [["apz.popups.enabled", false]] });
+
+ // Reference
+ const newWin = await BrowserTestUtils.openNewBrowserWindow();
+ const reference = await takeSnapshot(newWin);
+ await BrowserTestUtils.closeWindow(newWin);
+
+ // Test target
+ const testWin = await BrowserTestUtils.openNewBrowserWindow();
+ const result = await takeSnapshot(testWin, async () => {
+ let div = content.window.document.getElementById("target");
+ const anim = div.animate({ opacity: [1, 0.5] }, 10);
+ await anim.finished;
+ const anim2 = div.animate(
+ { transform: ["translateX(10px)", "translateX(20px)"] },
+ 10
+ );
+ await anim2.finished;
+
+ let div2 = content.window.document.getElementById("target2");
+ const anim3 = div2.animate(
+ { transform: ["translateX(10px)", "translateX(20px)"] },
+ 10
+ );
+ await anim3.finished;
+ });
+ await BrowserTestUtils.closeWindow(testWin);
+
+ is(result, reference, "The omta property value should be reset");
+
+ await extension.unload();
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_extension_popup_window.js b/gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_extension_popup_window.js
new file mode 100644
index 0000000000..911af7548d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_extension_popup_window.js
@@ -0,0 +1,189 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head_browserAction.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
+ this
+);
+
+add_task(async () => {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "popup.html": `
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script src="popup.js"></script>
+ <style>
+ * {
+ padding: 0;
+ margin: 0;
+ }
+ body {
+ height: 400px;
+ width: 200px;
+ overflow-y: auto;
+ overflow-x: hidden;
+ }
+ li {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 30vh;
+ font-size: 200%;
+ }
+ li:nth-child(even){
+ background-color: #ccc;
+ }
+ </style>
+ </head>
+ <body>
+ <ul>
+ <li>1</li>
+ <li>2</li>
+ <li>3</li>
+ <li>4</li>
+ <li>5</li>
+ <li>6</li>
+ <li>7</li>
+ <li>8</li>
+ <li>9</li>
+ <li>10</li>
+ </ul>
+ </body>
+ </html>`,
+ "popup.js": function () {
+ window.addEventListener(
+ "mousemove",
+ () => {
+ dump("Got a mousemove event in the popup content document\n");
+ browser.test.sendMessage("received-mousemove");
+ },
+ { once: true }
+ );
+ window.addEventListener(
+ "scroll",
+ () => {
+ dump("Got a scroll event in the popup content document\n");
+ browser.test.sendMessage("received-scroll");
+ },
+ { once: true }
+ );
+ },
+ },
+ });
+
+ await extension.startup();
+
+ await SpecialPowers.pushPrefEnv({ set: [["apz.popups.enabled", true]] });
+
+ // Open the popup window of the extension.
+ const browserForPopup = await openBrowserActionPanel(
+ extension,
+ undefined,
+ true
+ );
+
+ if (!browserForPopup.isRemoteBrowser) {
+ await closeBrowserAction(extension);
+ await extension.unload();
+ ok(
+ true,
+ "Skipping this test since the popup window doesn't have remote contents"
+ );
+ return;
+ }
+
+ // Flush APZ repaints and waits for MozAfterPaint to make sure APZ state is
+ // stable.
+ await promiseApzFlushedRepaintsInPopup(browserForPopup);
+
+ const { screenX, screenY, viewId, presShellId } = await SpecialPowers.spawn(
+ browserForPopup,
+ [],
+ () => {
+ const winUtils = SpecialPowers.getDOMWindowUtils(content.window);
+ return {
+ screenX: content.window.mozInnerScreenX * content.devicePixelRatio,
+ screenY: content.window.mozInnerScreenY * content.devicePixelRatio,
+ viewId: winUtils.getViewId(content.document.documentElement),
+ presShellId: winUtils.getPresShellId(),
+ };
+ }
+ );
+
+ // Before starting autoscroll we need to make sure a mousemove event has been
+ // processed in the popup content so that subsequent mousemoves for autoscroll
+ // will be properly processed in autoscroll animation.
+ const mousemoveEventPromise = extension.awaitMessage("received-mousemove");
+
+ const nativeMouseEventPromise = promiseNativeMouseEventWithAPZ({
+ type: "mousemove",
+ target: browserForPopup,
+ offsetX: 100,
+ offsetY: 50,
+ });
+
+ await Promise.all([nativeMouseEventPromise, mousemoveEventPromise]);
+
+ const scrollEventPromise = extension.awaitMessage("received-scroll");
+
+ // Start autoscrolling.
+ ok(
+ browserForPopup.browsingContext.startApzAutoscroll(
+ screenX + 100,
+ screenY + 50,
+ viewId,
+ presShellId
+ )
+ );
+
+ // Send sequential mousemove events to cause autoscrolling.
+ for (let i = 0; i < 10; i++) {
+ await promiseNativeMouseEventWithAPZ({
+ type: "mousemove",
+ target: browserForPopup,
+ offsetX: 100,
+ offsetY: 50 + i * 10,
+ });
+ }
+
+ // Flush APZ repaints and waits for MozAfterPaint to make sure the scroll has
+ // been reflected on the main thread.
+ const apzPromise = promiseApzFlushedRepaintsInPopup(browserForPopup);
+
+ await Promise.all([apzPromise, scrollEventPromise]);
+
+ const scrollY = await SpecialPowers.spawn(browserForPopup, [], () => {
+ return content.window.scrollY;
+ });
+ ok(scrollY > 0, "Autoscrolling works in the popup window");
+
+ browserForPopup.browsingContext.stopApzAutoscroll(viewId, presShellId);
+
+ await closeBrowserAction(extension);
+
+ await extension.unload();
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_oop_frame.js b/gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_oop_frame.js
new file mode 100644
index 0000000000..26d0ff6109
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_oop_frame.js
@@ -0,0 +1,120 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
+ this
+);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["general.autoScroll", true],
+ ["middlemouse.contentLoadURL", false],
+ ["test.events.async.enabled", true],
+ ],
+ });
+});
+
+async function doTest() {
+ function httpURL(filename) {
+ const chromeURL = getRootDirectory(gTestPath) + filename;
+ return chromeURL.replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+ }
+
+ function getScrollY(context) {
+ return SpecialPowers.spawn(context, [], () => content.scrollY);
+ }
+
+ const pageUrl = httpURL("helper_test_autoscrolling_in_oop_frame.html");
+
+ await BrowserTestUtils.withNewTab(pageUrl, async function (browser) {
+ await promiseApzFlushedRepaintsInPopup(browser);
+
+ const iframeContext = browser.browsingContext.children[0];
+ await promiseApzFlushedRepaintsInPopup(iframeContext);
+
+ const { screenX, screenY, viewId, presShellId } = await SpecialPowers.spawn(
+ iframeContext,
+ [],
+ () => {
+ const winUtils = SpecialPowers.getDOMWindowUtils(content);
+ return {
+ screenX: content.mozInnerScreenX * content.devicePixelRatio,
+ screenY: content.mozInnerScreenY * content.devicePixelRatio,
+ viewId: winUtils.getViewId(content.document.documentElement),
+ presShellId: winUtils.getPresShellId(),
+ };
+ }
+ );
+
+ ok(
+ iframeContext.startApzAutoscroll(
+ screenX + 100,
+ screenY + 50,
+ viewId,
+ presShellId
+ ),
+ "Started autscroll"
+ );
+
+ const scrollEventPromise = SpecialPowers.spawn(
+ iframeContext,
+ [],
+ async () => {
+ return new Promise(resolve => {
+ content.addEventListener(
+ "scroll",
+ event => {
+ dump("Got a scroll event in the iframe\n");
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ }
+ );
+
+ // Send sequential mousemove events to cause autoscrolling.
+ for (let i = 0; i < 10; i++) {
+ await promiseNativeMouseEventWithAPZ({
+ type: "mousemove",
+ target: browser,
+ offsetX: 100,
+ offsetY: 50 + i * 10,
+ });
+ }
+
+ // Flush APZ repaints and waits for MozAfterPaint to make sure the scroll has
+ // been reflected on the main thread.
+ const apzPromise = promiseApzFlushedRepaintsInPopup(browser);
+
+ await Promise.all([apzPromise, scrollEventPromise]);
+
+ const frameScrollY = await getScrollY(iframeContext);
+ ok(frameScrollY > 0, "Autoscrolled the iframe");
+
+ const rootScrollY = await getScrollY(browser);
+ ok(rootScrollY == 0, "Didn't scroll the root document");
+
+ iframeContext.stopApzAutoscroll(viewId, presShellId);
+ });
+}
+
+add_task(async function test_autoscroll_in_oop_iframe() {
+ await doTest();
+});
+
+add_task(async function test_autoscroll_in_oop_iframe_with_os_zoom() {
+ await SpecialPowers.pushPrefEnv({ set: [["ui.textScaleFactor", 200]] });
+ await doTest();
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_background_tab_load_scroll.js b/gfx/layers/apz/test/mochitest/browser_test_background_tab_load_scroll.js
new file mode 100644
index 0000000000..9878907603
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_background_tab_load_scroll.js
@@ -0,0 +1,117 @@
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
+ this
+);
+
+add_task(async function test_main() {
+ // Open a specific page in a background tab, then switch to the tab, check if
+ // the visual and layout scroll offsets have diverged.
+ // Then change to another tab so it's background again. Then reload it. Then
+ // change back to it and check again if the visual and layout scroll offsets
+ // have diverged.
+ // The page has a couple important properties to trigger the bug. We need to
+ // be restoring a non-zero scroll position so that we call ScrollToImpl with
+ // origin restore so that we do not set the visual viewport offset. We then
+ // need to call ScrollToImpl with a origin that does not get clobber by apz
+ // so that we (wrongly) set the visual viewport offset.
+
+ requestLongerTimeout(2);
+
+ async function twoRafsInContent(browser) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ await new Promise(r =>
+ content.requestAnimationFrame(() => content.requestAnimationFrame(r))
+ );
+ });
+ }
+
+ async function waitForApzInContent(browser) {
+ await SpecialPowers.spawn(browser, [], async () => {
+ await content.wrappedJSObject.waitUntilApzStable();
+ await content.wrappedJSObject.promiseApzFlushedRepaints();
+ });
+ }
+
+ async function checkScrollPosInContent(browser, iter, num) {
+ let visualScrollPos = await SpecialPowers.spawn(browser, [], function () {
+ const offsetX = {};
+ const offsetY = {};
+ SpecialPowers.getDOMWindowUtils(content).getVisualViewportOffset(
+ offsetX,
+ offsetY
+ );
+ return offsetY.value;
+ });
+
+ let scrollPos = await SpecialPowers.spawn(browser, [], function () {
+ return content.window.scrollY;
+ });
+
+ // When this fails the difference is at least 10000.
+ ok(
+ Math.abs(scrollPos - visualScrollPos) < 2,
+ "expect scroll position and visual scroll position to be the same: visual " +
+ visualScrollPos +
+ " scroll " +
+ scrollPos +
+ " (" +
+ iter +
+ "," +
+ num +
+ ")"
+ );
+ }
+
+ for (let i = 0; i < 5; i++) {
+ let blankurl = "about:blank";
+ let blankTab = BrowserTestUtils.addTab(gBrowser, blankurl);
+ let blankbrowser = blankTab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(blankbrowser, false, blankurl);
+
+ let url =
+ "http://mochi.test:8888/browser/gfx/layers/apz/test/mochitest/helper_background_tab_load_scroll.html";
+ let backgroundTab = BrowserTestUtils.addTab(gBrowser, url);
+ let browser = backgroundTab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser, false, url);
+ dump("Done loading background tab\n");
+
+ await twoRafsInContent(browser);
+
+ // Switch to the foreground.
+ await BrowserTestUtils.switchTab(gBrowser, backgroundTab);
+ dump("Switched background tab to foreground\n");
+
+ await waitForApzInContent(browser);
+
+ await checkScrollPosInContent(browser, i, 1);
+
+ await BrowserTestUtils.switchTab(gBrowser, blankTab);
+
+ browser.reload();
+ await BrowserTestUtils.browserLoaded(browser, false, url);
+
+ await twoRafsInContent(browser);
+
+ // Switch to the foreground.
+ await BrowserTestUtils.switchTab(gBrowser, backgroundTab);
+ dump("Switched background tab to foreground\n");
+
+ await waitForApzInContent(browser);
+
+ await checkScrollPosInContent(browser, i, 2);
+
+ // Cleanup
+ let tabClosed = BrowserTestUtils.waitForTabClosing(backgroundTab);
+ BrowserTestUtils.removeTab(backgroundTab);
+ await tabClosed;
+
+ let blanktabClosed = BrowserTestUtils.waitForTabClosing(blankTab);
+ BrowserTestUtils.removeTab(blankTab);
+ await blanktabClosed;
+ }
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_background_tab_scroll.js b/gfx/layers/apz/test/mochitest/browser_test_background_tab_scroll.js
new file mode 100644
index 0000000000..4ce8200199
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_background_tab_scroll.js
@@ -0,0 +1,66 @@
+add_task(async function test_main() {
+ // Load page in the background. This will cause the first-paint of the
+ // tab (which has ScrollPositionUpdate instances) to get sent to the
+ // compositor, but the parent process RefLayer won't be pointing to this
+ // tab so APZ never sees the ScrollPositionUpdate instances.
+
+ let url =
+ "http://mochi.test:8888/browser/gfx/layers/apz/test/mochitest/helper_background_tab_scroll.html#scrolltarget";
+ let backgroundTab = BrowserTestUtils.addTab(gBrowser, url);
+ let browser = backgroundTab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser, false, url);
+ dump("Done loading background tab\n");
+
+ // Switch to the foreground, to let the APZ tree get built.
+ await BrowserTestUtils.switchTab(gBrowser, backgroundTab);
+ dump("Switched background tab to foreground\n");
+
+ // Verify main-thread scroll position is where we expect
+ let scrollPos = await ContentTask.spawn(browser, null, function () {
+ return content.window.scrollY;
+ });
+ is(scrollPos, 5000, "Expected background tab to be at scroll pos 5000");
+
+ // Trigger an APZ-side scroll via native wheel event, followed by some code
+ // to ensure APZ's repaint requests to arrive at the main-thread. If
+ // things are working properly, the main thread will accept the repaint
+ // requests and update the main-thread scroll position. If the APZ side
+ // is sending incorrect scroll generations in the repaint request, then
+ // the main thread will fail to clear the main-thread scroll origin (which
+ // was set by the scroll to the #scrolltarget anchor), and so will not
+ // accept APZ's scroll position updates.
+ let contentScrollFunction = async function () {
+ await content.window.wrappedJSObject.promiseNativeWheelAndWaitForWheelEvent(
+ content.window,
+ 100,
+ 100,
+ 0,
+ 200
+ );
+
+ // Advance some/all frames of the APZ wheel animation
+ let utils = content.window.SpecialPowers.getDOMWindowUtils(content.window);
+ for (var i = 0; i < 10; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ utils.restoreNormalRefresh();
+ // Flush pending APZ repaints, then read the main-thread scroll
+ // position
+ await content.window.wrappedJSObject.promiseOnlyApzControllerFlushed(
+ content.window
+ );
+ return content.window.scrollY;
+ };
+ scrollPos = await ContentTask.spawn(browser, null, contentScrollFunction);
+
+ // Verify main-thread scroll position has changed
+ ok(
+ scrollPos < 5000,
+ `Expected background tab to have scrolled up, is at ${scrollPos}`
+ );
+
+ // Cleanup
+ let tabClosed = BrowserTestUtils.waitForTabClosing(backgroundTab);
+ BrowserTestUtils.removeTab(backgroundTab);
+ await tabClosed;
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_content_response_timeout.js b/gfx/layers/apz/test/mochitest/browser_test_content_response_timeout.js
new file mode 100644
index 0000000000..a80fd77c17
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_content_response_timeout.js
@@ -0,0 +1,88 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
+ this
+);
+
+add_task(async () => {
+ // Use pan gesture events for Mac.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Set a relatively shorter timeout value.
+ ["apz.content_response_timeout", 100],
+ ],
+ });
+
+ const URL_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+ // Load a content having an APZ-aware listener causing 500ms busy state and
+ // a scroll event listener changing the background color of an element.
+ // The reason why we change the background color in a scroll listener rather
+ // than setting up a Promise resolved in a scroll event handler and waiting
+ // for the Promise is SpecialPowers.spawn doesn't allow it (bug 1743857).
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ URL_ROOT + "helper_content_response_timeout.html"
+ );
+
+ let scrollPromise = BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "scroll"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.promiseApzFlushedRepaints();
+ await content.wrappedJSObject.waitUntilApzStable();
+ });
+
+ // Note that below function uses `WaitForObserver` version of sending a
+ // pan-start event function so that the notification can be sent in the parent
+ // process, thus we can get the notification even if the content process is
+ // busy.
+ await NativePanHandler.promiseNativePanEvent(
+ tab.linkedBrowser,
+ 100,
+ 100,
+ 0,
+ NativePanHandler.delta,
+ NativePanHandler.beginPhase
+ );
+
+ await new Promise(resolve => {
+ setTimeout(resolve, 200);
+ });
+
+ await NativePanHandler.promiseNativePanEvent(
+ tab.linkedBrowser,
+ 100,
+ 100,
+ 0,
+ NativePanHandler.delta,
+ NativePanHandler.updatePhase
+ );
+ await NativePanHandler.promiseNativePanEvent(
+ tab.linkedBrowser,
+ 100,
+ 100,
+ 0,
+ 0,
+ NativePanHandler.endPhase
+ );
+
+ await scrollPromise;
+ ok(true, "We got at least one scroll event");
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_group_fission.js b/gfx/layers/apz/test/mochitest/browser_test_group_fission.js
new file mode 100644
index 0000000000..c6b838dd75
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_group_fission.js
@@ -0,0 +1,149 @@
+add_task(async function setup_pref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // To avoid throttling requestAnimationFrame callbacks in invisible
+ // iframes
+ ["layout.throttled_frame_rate", 60],
+ ["dom.animations-api.timelines.enabled", true],
+ // Next two prefs are needed for hit-testing to work
+ ["test.events.async.enabled", true],
+ ["apz.test.logging_enabled", true],
+ ],
+ });
+});
+
+add_task(async function test_main() {
+ function httpURL(filename) {
+ let chromeURL = getRootDirectory(gTestPath) + filename;
+ return chromeURL.replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+ }
+
+ // Each of these subtests is a dictionary that contains:
+ // file (required): filename of the subtest that will get opened in a new tab
+ // in the top-level fission-enabled browser window.
+ // setup (optional): function that takes the top-level fission window and is
+ // run once after the subtest is loaded but before it is started.
+ var subtests = [
+ { file: "helper_fission_basic.html" },
+ { file: "helper_fission_transforms.html" },
+ { file: "helper_fission_scroll_oopif.html" },
+ {
+ file: "helper_fission_event_region_override.html",
+ setup(win) {
+ win.document.addEventListener("wheel", e => e.preventDefault(), {
+ once: true,
+ passive: false,
+ });
+ },
+ },
+ { file: "helper_fission_animation_styling_in_oopif.html" },
+ { file: "helper_fission_force_empty_hit_region.html" },
+ { file: "helper_fission_touch.html" },
+ {
+ file: "helper_fission_tap.html",
+ prefs: [["apz.max_tap_time", 10000]],
+ },
+ { file: "helper_fission_inactivescroller_under_oopif.html" },
+ {
+ file: "helper_fission_tap_on_zoomed.html",
+ prefs: [["apz.max_tap_time", 10000]],
+ },
+ {
+ file: "helper_fission_tap_in_nested_iframe_on_zoomed.html",
+ prefs: [["apz.max_tap_time", 10000]],
+ },
+ { file: "helper_fission_scroll_handoff.html" },
+ { file: "helper_fission_large_subframe.html" },
+ { file: "helper_fission_initial_displayport.html" },
+ { file: "helper_fission_checkerboard_severity.html" },
+ { file: "helper_fission_setResolution.html" },
+ { file: "helper_fission_inactivescroller_positionedcontent.html" },
+ { file: "helper_fission_irregular_areas.html" },
+ { file: "helper_fission_animation_styling_in_transformed_oopif.html" },
+ // add additional tests here
+ ];
+
+ // ccov builds run slower and need longer, so let's scale up the timeout
+ // by the number of tests we're running.
+ requestLongerTimeout(subtests.length);
+
+ let fissionWindow = await BrowserTestUtils.openNewBrowserWindow({
+ fission: true,
+ });
+
+ // We import the ESM here so that we can install functions on the class
+ // below.
+ const { FissionTestHelperParent } = ChromeUtils.importESModule(
+ getRootDirectory(gTestPath) + "FissionTestHelperParent.sys.mjs"
+ );
+ FissionTestHelperParent.SimpleTest = SimpleTest;
+
+ ChromeUtils.registerWindowActor("FissionTestHelper", {
+ parent: {
+ esModuleURI:
+ getRootDirectory(gTestPath) + "FissionTestHelperParent.sys.mjs",
+ },
+ child: {
+ esModuleURI:
+ getRootDirectory(gTestPath) + "FissionTestHelperChild.sys.mjs",
+ events: {
+ "FissionTestHelper:Init": { capture: true, wantUntrusted: true },
+ },
+ },
+ allFrames: true,
+ });
+
+ try {
+ var onlyOneSubtest = SpecialPowers.getCharPref(
+ "apz.subtest",
+ /*default = */ ""
+ );
+
+ for (var subtest of subtests) {
+ if (onlyOneSubtest && onlyOneSubtest != subtest.file) {
+ SimpleTest.ok(
+ true,
+ "Skipping " +
+ subtest.file +
+ " because only " +
+ onlyOneSubtest +
+ " is being run"
+ );
+ continue;
+ }
+ let url = httpURL(subtest.file);
+ dump(`Starting test ${url}\n`);
+
+ // Load the test URL and tell it to get started, and wait until it reports
+ // completion.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: fissionWindow.gBrowser, url },
+ async browser => {
+ let tabActor =
+ browser.browsingContext.currentWindowGlobal.getActor(
+ "FissionTestHelper"
+ );
+ let donePromise = tabActor.getTestCompletePromise();
+ if (subtest.setup) {
+ subtest.setup(fissionWindow);
+ }
+ tabActor.startTest();
+ await donePromise;
+ }
+ );
+
+ dump(`Finished test ${url}\n`);
+ }
+ } finally {
+ // Delete stuff we added to FissionTestHelperParent, beacuse the object will
+ // outlive this test, and leaving stuff on it may leak the things reachable
+ // from it.
+ delete FissionTestHelperParent.SimpleTest;
+ // Teardown
+ ChromeUtils.unregisterWindowActor("FissionTestHelper");
+ await BrowserTestUtils.closeWindow(fissionWindow);
+ }
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_position_sticky.js b/gfx/layers/apz/test/mochitest/browser_test_position_sticky.js
new file mode 100644
index 0000000000..ce6b093a80
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_position_sticky.js
@@ -0,0 +1,105 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
+ this
+);
+
+add_task(async () => {
+ function httpURL(filename) {
+ let chromeURL = getRootDirectory(gTestPath) + filename;
+ return chromeURL.replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+ }
+
+ const url = httpURL("helper_position_sticky_flicker.html");
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ const { rect, scrollbarWidth } = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async () => {
+ const sticky = content.document.getElementById("sticky");
+
+ // Get the area in the screen coords where the position:sticky element is.
+ let stickyRect = sticky.getBoundingClientRect();
+ stickyRect.x += content.window.mozInnerScreenX;
+ stickyRect.y += content.window.mozInnerScreenY;
+
+ // generate some DIVs to make the page complex enough.
+ for (let i = 1; i <= 120000; i++) {
+ const div = content.document.createElement("div");
+ div.innerText = `${i}`;
+ content.document.body.appendChild(div);
+ }
+
+ await content.wrappedJSObject.promiseApzFlushedRepaints();
+ await content.wrappedJSObject.waitUntilApzStable();
+
+ let w = {},
+ h = {};
+ SpecialPowers.DOMWindowUtils.getScrollbarSizes(
+ content.document.documentElement,
+ w,
+ h
+ );
+
+ // Reduce the scrollbar width from the sticky area.
+ stickyRect.width -= w.value;
+ return {
+ rect: stickyRect,
+ scrollbarWidth: w.value,
+ };
+ }
+ );
+
+ // Take a snapshot where the position:sticky element is initially painted.
+ const reference = await getSnapshot(rect);
+
+ let mouseX = window.innerWidth - scrollbarWidth / 2;
+ let mouseY = tab.linkedBrowser.getBoundingClientRect().y + 5;
+
+ // Scroll fast to cause checkerboarding multiple times.
+ const dragFinisher = await promiseNativeMouseDrag(
+ window,
+ mouseX,
+ mouseY,
+ 0,
+ window.innerHeight,
+ 100
+ );
+
+ // On debug builds there seems to be no chance that the content process gets
+ // painted during above promiseNativeMouseDrag call, wait two frames to make
+ // sure it happens so that this test is likely able to fail without proper
+ // fix.
+ if (AppConstants.DEBUG) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.promiseFrame(content.window);
+ await content.wrappedJSObject.promiseFrame(content.window);
+ });
+ }
+
+ // Take a snapshot again where the position:sticky element should be painted.
+ const snapshot = await getSnapshot(rect);
+
+ await dragFinisher();
+
+ is(
+ snapshot,
+ reference,
+ "The position:sticky element should stay at the " +
+ "same place after scrolling on heavy load"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_reset_scaling_zoom.js b/gfx/layers/apz/test/mochitest/browser_test_reset_scaling_zoom.js
new file mode 100644
index 0000000000..168d358fcb
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_reset_scaling_zoom.js
@@ -0,0 +1,44 @@
+add_task(async function setup_pref() {
+ let isWindows = navigator.platform.indexOf("Win") == 0;
+ await SpecialPowers.pushPrefEnv({
+ set: [["apz.test.fails_with_native_injection", isWindows]],
+ });
+});
+
+add_task(async function test_main() {
+ function httpURL(filename) {
+ let chromeURL = getRootDirectory(gTestPath) + filename;
+ return chromeURL.replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+ }
+
+ const pageUrl = httpURL("helper_test_reset_scaling_zoom.html");
+
+ await BrowserTestUtils.withNewTab(pageUrl, async function (browser) {
+ let getResolution = function () {
+ return content.window.SpecialPowers.getDOMWindowUtils(
+ content.window
+ ).getResolution();
+ };
+
+ let doZoomIn = async function () {
+ await content.window.wrappedJSObject.doZoomIn();
+ };
+
+ let resolution = await ContentTask.spawn(browser, null, getResolution);
+ is(resolution, 1.0, "Initial page resolution should be 1.0");
+
+ await ContentTask.spawn(browser, null, doZoomIn);
+ resolution = await ContentTask.spawn(browser, null, getResolution);
+ isnot(resolution, 1.0, "Expected resolution to be bigger than 1.0");
+
+ document.getElementById("cmd_fullZoomReset").doCommand();
+ // Spin the event loop once just to make sure the message gets through
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ resolution = await ContentTask.spawn(browser, null, getResolution);
+ is(resolution, 1.0, "Expected resolution to be reset to 1.0");
+ });
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_scroll_thumb_dragging.js b/gfx/layers/apz/test/mochitest/browser_test_scroll_thumb_dragging.js
new file mode 100644
index 0000000000..bd2d733a32
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_scroll_thumb_dragging.js
@@ -0,0 +1,77 @@
+add_task(async function () {
+ function httpURL(filename) {
+ let chromeURL = getRootDirectory(gTestPath) + filename;
+ return chromeURL.replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+ }
+
+ const newWin = await BrowserTestUtils.openNewBrowserWindow();
+
+ const pageUrl = httpURL("helper_scroll_thumb_dragging.html");
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ newWin.gBrowser,
+ pageUrl
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.promiseApzFlushedRepaints();
+ await content.wrappedJSObject.waitUntilApzStable();
+ });
+
+ // Send an explicit click event to make sure the new window accidentally
+ // doesn't get an "enter-notify-event" on Linux during dragging, the event
+ // forcibly cancels the dragging state.
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ // Creating an object in this content privilege so that the object
+ // properties can be accessed in below
+ // promiseNativeMouseEventWithAPZAndWaitForEvent function.
+ const moveParams = content.window.eval(`({
+ target: window,
+ type: "mousemove",
+ offsetX: 10,
+ offsetY: 10
+ })`);
+ const clickParams = content.window.eval(`({
+ target: window,
+ type: "click",
+ offsetX: 10,
+ offsetY: 10
+ })`);
+ // Send a mouse move event first to make sure the "enter-notify-event"
+ // happens.
+ await content.wrappedJSObject.promiseNativeMouseEventWithAPZAndWaitForEvent(
+ moveParams
+ );
+ await content.wrappedJSObject.promiseNativeMouseEventWithAPZAndWaitForEvent(
+ clickParams
+ );
+ });
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ const scrollPromise = new Promise(resolve => {
+ content.window.addEventListener("scroll", resolve, { once: true });
+ });
+ const dragFinisher =
+ await content.wrappedJSObject.promiseVerticalScrollbarDrag(
+ content.window,
+ 10,
+ 10
+ );
+
+ await scrollPromise;
+ await dragFinisher();
+
+ await content.wrappedJSObject.promiseApzFlushedRepaints();
+ });
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ ok(
+ content.window.scrollY < 100,
+ "The root scrollable content shouldn't be scrolled too much"
+ );
+ });
+
+ await BrowserTestUtils.closeWindow(newWin);
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_scrollbar_in_extension_popup_window.js b/gfx/layers/apz/test/mochitest/browser_test_scrollbar_in_extension_popup_window.js
new file mode 100644
index 0000000000..6e18129845
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_scrollbar_in_extension_popup_window.js
@@ -0,0 +1,138 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head_browserAction.js",
+ this
+);
+
+add_task(async () => {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "popup.html": `
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ * {
+ padding: 0;
+ margin: 0;
+ }
+ body {
+ height: 400px;
+ width: 200px;
+ overflow-y: auto;
+ overflow-x: hidden;
+ }
+ li {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 30vh;
+ font-size: 200%;
+ }
+ li:nth-child(even){
+ background-color: #ccc;
+ }
+ </style>
+ </head>
+ <body>
+ <ul>
+ <li>1</li>
+ <li>2</li>
+ <li>3</li>
+ <li>4</li>
+ <li>5</li>
+ <li>6</li>
+ <li>7</li>
+ <li>8</li>
+ <li>9</li>
+ <li>10</li>
+ </ul>
+ </body>
+ </html>`,
+ },
+ });
+
+ await extension.startup();
+
+ async function takeSnapshot(browserWin) {
+ let browser = await openBrowserActionPanel(extension, browserWin, true);
+
+ // Ensure there's no pending paint requests.
+ // The below code is a simplified version of promiseAllPaintsDone in
+ // paint_listener.js.
+ await SpecialPowers.spawn(browser, [], async () => {
+ return new Promise(resolve => {
+ function waitForPaints() {
+ // Wait until paint suppression has ended
+ if (SpecialPowers.DOMWindowUtils.paintingSuppressed) {
+ dump`waiting for paint suppression to end...`;
+ content.window.setTimeout(waitForPaints, 0);
+ return;
+ }
+
+ if (SpecialPowers.DOMWindowUtils.isMozAfterPaintPending) {
+ dump`waiting for paint...`;
+ content.window.addEventListener("MozAfterPaint", waitForPaints, {
+ once: true,
+ });
+ return;
+ }
+ resolve();
+ }
+ waitForPaints();
+ });
+ });
+
+ const snapshot = await SpecialPowers.spawn(browser, [], async () => {
+ return SpecialPowers.snapshotWindowWithOptions(
+ content.window,
+ undefined /* use the default rect */,
+ undefined /* use the default bgcolor */,
+ { DRAWWINDOW_DRAW_VIEW: true } /* to capture scrollbars */
+ )
+ .toDataURL()
+ .toString();
+ });
+
+ const popup = getBrowserActionPopup(extension, browserWin);
+ await closeBrowserAction(extension, browserWin);
+ is(popup.state, "closed", "browserAction popup has been closed");
+
+ return snapshot;
+ }
+
+ // First, take a snapshot with disabling APZ in the popup window, we assume
+ // scrollbars are rendered properly there.
+ await SpecialPowers.pushPrefEnv({ set: [["apz.popups.enabled", false]] });
+ const newWin = await BrowserTestUtils.openNewBrowserWindow();
+ const reference = await takeSnapshot(newWin);
+ await BrowserTestUtils.closeWindow(newWin);
+
+ // Then take a snapshot with enabling APZ.
+ await SpecialPowers.pushPrefEnv({ set: [["apz.popups.enabled", true]] });
+ const anotherWin = await BrowserTestUtils.openNewBrowserWindow();
+ const test = await takeSnapshot(anotherWin);
+ await BrowserTestUtils.closeWindow(anotherWin);
+
+ is(
+ test,
+ reference,
+ "Contents in popup window opened by extension should be same regardless of the APZ state in the window"
+ );
+
+ await extension.unload();
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_scrolling_in_extension_popup_window.js b/gfx/layers/apz/test/mochitest/browser_test_scrolling_in_extension_popup_window.js
new file mode 100644
index 0000000000..6da3f3311b
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_scrolling_in_extension_popup_window.js
@@ -0,0 +1,128 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head_browserAction.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
+ this
+);
+
+add_task(async () => {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "popup.html": `
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ * {
+ padding: 0;
+ margin: 0;
+ }
+ body {
+ height: 400px;
+ width: 200px;
+ overflow-y: auto;
+ overflow-x: hidden;
+ }
+ li {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 30vh;
+ font-size: 200%;
+ }
+ li:nth-child(even){
+ background-color: #ccc;
+ }
+ </style>
+ </head>
+ <body>
+ <ul>
+ <li>1</li>
+ <li>2</li>
+ <li>3</li>
+ <li>4</li>
+ <li>5</li>
+ <li>6</li>
+ <li>7</li>
+ <li>8</li>
+ <li>9</li>
+ <li>10</li>
+ </ul>
+ </body>
+ </html>`,
+ },
+ });
+
+ await extension.startup();
+
+ await SpecialPowers.pushPrefEnv({ set: [["apz.popups.enabled", true]] });
+
+ // Open the popup window of the extension.
+ const browserForPopup = await openBrowserActionPanel(
+ extension,
+ undefined,
+ true
+ );
+
+ // Flush APZ repaints and waits for MozAfterPaint to make sure APZ state is
+ // stable.
+ await promiseApzFlushedRepaintsInPopup(browserForPopup);
+
+ const scrollEventPromise = SpecialPowers.spawn(
+ browserForPopup,
+ [],
+ async () => {
+ return new Promise(resolve => {
+ content.window.addEventListener(
+ "scroll",
+ event => {
+ dump("Got a scroll event in the popup content document\n");
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ }
+ );
+
+ // Send native mouse wheel to scroll the content in the popup.
+ await promiseNativeWheelAndWaitForObserver(browserForPopup, 50, 50, 0, -100);
+
+ // Flush APZ repaints and waits for MozAfterPaint to make sure the scroll has
+ // been reflected on the main thread.
+ const apzPromise = promiseApzFlushedRepaintsInPopup(browserForPopup);
+
+ await Promise.all([apzPromise, scrollEventPromise]);
+
+ const scrollY = await SpecialPowers.spawn(browserForPopup, [], () => {
+ return content.window.scrollY;
+ });
+ ok(scrollY > 0, "Mouse wheel scrolling works in the popup window");
+
+ await closeBrowserAction(extension);
+
+ await extension.unload();
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_scrolling_on_inactive_scroller_in_extension_popup_window.js b/gfx/layers/apz/test/mochitest/browser_test_scrolling_on_inactive_scroller_in_extension_popup_window.js
new file mode 100644
index 0000000000..99dccd458c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_scrolling_on_inactive_scroller_in_extension_popup_window.js
@@ -0,0 +1,137 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head_browserAction.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
+ this
+);
+
+add_task(async () => {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "popup.html": `
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ * {
+ padding: 0;
+ margin: 0;
+ }
+ body {
+ height: 400px;
+ width: 200px;
+ }
+ .flex {
+ width: 100vw;
+ display: flex;
+ flex-direction: row;
+ height: 100vh;
+ }
+ .overflow {
+ flex: 1;
+ overflow: auto;
+ height: 100vh;
+ }
+ .overflow div {
+ height: 400vh;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="flex">
+ <div class="overflow"><div>123</div></div>
+ <div class="overflow"><div>123</div></div>
+ </div>
+ </body>
+ </html>`,
+ },
+ });
+
+ await extension.startup();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["apz.popups.enabled", true],
+ ["apz.wr.activate_all_scroll_frames", false],
+ ["apz.wr.activate_all_scroll_frames_when_fission", false],
+ ],
+ });
+
+ // Open the popup window of the extension.
+ const browserForPopup = await openBrowserActionPanel(
+ extension,
+ undefined,
+ true
+ );
+
+ // Flush APZ repaints and waits for MozAfterPaint to make sure APZ state is
+ // stable.
+ await promiseApzFlushedRepaintsInPopup(browserForPopup);
+
+ // A Promise to wait for one scroll event for each scrollable element.
+ const scrollEventsPromise = SpecialPowers.spawn(browserForPopup, [], () => {
+ let promises = [];
+ content.document.querySelectorAll(".overflow").forEach(element => {
+ let promise = new Promise(resolve => {
+ element.addEventListener(
+ "scroll",
+ () => {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ promises.push(promise);
+ });
+ return Promise.all(promises);
+ });
+
+ // Send two native mouse wheel events to scroll each scrollable element in the
+ // popup.
+ await promiseNativeWheelAndWaitForObserver(browserForPopup, 50, 50, 0, -100);
+ await promiseNativeWheelAndWaitForObserver(browserForPopup, 150, 50, 0, -100);
+
+ // Flush APZ repaints and waits for MozAfterPaint to make sure the scroll has
+ // been reflected on the main thread.
+ const apzPromise = promiseApzFlushedRepaintsInPopup(browserForPopup);
+
+ await Promise.all([apzPromise, scrollEventsPromise]);
+
+ const scrollTops = await SpecialPowers.spawn(browserForPopup, [], () => {
+ let result = [];
+ content.document.querySelectorAll(".overflow").forEach(element => {
+ result.push(element.scrollTop);
+ });
+ return result;
+ });
+
+ ok(scrollTops[0] > 0, "Mouse wheel scrolling works in the popup window");
+ ok(scrollTops[1] > 0, "Mouse wheel scrolling works in the popup window");
+
+ await closeBrowserAction(extension);
+
+ await extension.unload();
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_select_popup_position.js b/gfx/layers/apz/test/mochitest/browser_test_select_popup_position.js
new file mode 100644
index 0000000000..08a6ec9b93
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_select_popup_position.js
@@ -0,0 +1,130 @@
+/* This test is a a mash up of
+ https://searchfox.org/mozilla-central/rev/559b25eb41c1cbffcb90a34e008b8288312fcd25/gfx/layers/apz/test/mochitest/browser_test_group_fission.js
+ https://searchfox.org/mozilla-central/rev/559b25eb41c1cbffcb90a34e008b8288312fcd25/gfx/layers/apz/test/mochitest/helper_basic_zoom.html
+ https://searchfox.org/mozilla-central/rev/559b25eb41c1cbffcb90a34e008b8288312fcd25/browser/base/content/test/forms/browser_selectpopup.js
+*/
+
+/* import-globals-from helper_browser_test_utils.js */
+Services.scriptloader.loadSubScript(
+ new URL("helper_browser_test_utils.js", gTestPath).href,
+ this
+);
+
+async function runPopupPositionTest(parentDocumentFileName) {
+ function httpURL(filename) {
+ let chromeURL = getRootDirectory(gTestPath) + filename;
+ return chromeURL.replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+ }
+
+ function httpCrossOriginURL(filename) {
+ let chromeURL = getRootDirectory(gTestPath) + filename;
+ return chromeURL.replace(
+ "chrome://mochitests/content/",
+ "http://example.com/"
+ );
+ }
+
+ const pageUrl = httpURL(parentDocumentFileName);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ // Load the OOP iframe.
+ const iframeUrl = httpCrossOriginURL(
+ "helper_test_select_popup_position.html"
+ );
+ const iframe = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [iframeUrl],
+ async url => {
+ const target = content.document.querySelector("iframe");
+ target.src = url;
+ await new Promise(resolve => {
+ target.addEventListener("load", resolve, { once: true });
+ });
+ return target.browsingContext;
+ }
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.promiseApzFlushedRepaints();
+ await content.wrappedJSObject.waitUntilApzStable();
+ });
+
+ const selectRect = await SpecialPowers.spawn(iframe, [], () => {
+ return content.document.querySelector("select").getBoundingClientRect();
+ });
+
+ // Get focus on the select element.
+ await SpecialPowers.spawn(iframe, [], async () => {
+ const select = content.document.querySelector("select");
+ const focusPromise = new Promise(resolve => {
+ select.addEventListener("focus", resolve, { once: true });
+ });
+ select.focus();
+ await focusPromise;
+ });
+
+ const selectPopup = await openSelectPopup();
+
+ const popupRect = selectPopup.getBoundingClientRect();
+ const popupMarginTop = parseFloat(getComputedStyle(selectPopup).marginTop);
+ const popupMarginLeft = parseFloat(getComputedStyle(selectPopup).marginLeft);
+
+ info(
+ `popup rect: (${popupRect.x}, ${popupRect.y}) ${popupRect.width}x${popupRect.height}`
+ );
+ info(`popup margins: ${popupMarginTop} / ${popupMarginLeft}`);
+ info(
+ `select rect: (${selectRect.x}, ${selectRect.y}) ${selectRect.width}x${selectRect.height}`
+ );
+
+ is(
+ popupRect.left - popupMarginLeft,
+ selectRect.x * 2.0,
+ "select popup position x should be scaled by the desktop zoom"
+ );
+
+ // On platforms other than MaxOSX the popup menu is positioned below the
+ // option element.
+ if (!navigator.platform.includes("Mac")) {
+ is(
+ popupRect.top - popupMarginTop,
+ tab.linkedBrowser.getBoundingClientRect().top +
+ (selectRect.y + selectRect.height) * 2.0,
+ "select popup position y should be scaled by the desktop zoom"
+ );
+ } else {
+ // On mac it's aligned to the selected menulist option.
+ const offsetToSelectedItem =
+ selectPopup.querySelector("menuitem[selected]").getBoundingClientRect()
+ .top - popupRect.top;
+ is(
+ popupRect.top - popupMarginTop + offsetToSelectedItem,
+ tab.linkedBrowser.getBoundingClientRect().top + selectRect.y * 2.0,
+ "select popup position y should be scaled by the desktop zoom"
+ );
+ }
+
+ await hideSelectPopup();
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function () {
+ if (!SpecialPowers.useRemoteSubframes) {
+ ok(
+ true,
+ "popup window position in non OOP iframe will be fixed by bug 1691346"
+ );
+ return;
+ }
+ await runPopupPositionTest(
+ "helper_test_select_popup_position_transformed_in_parent.html"
+ );
+});
+
+add_task(async function () {
+ await runPopupPositionTest("helper_test_select_popup_position_zoomed.html");
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_select_zoom.js b/gfx/layers/apz/test/mochitest/browser_test_select_zoom.js
new file mode 100644
index 0000000000..84baf8e5ac
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_select_zoom.js
@@ -0,0 +1,195 @@
+/* This test is a a mash up of
+ https://searchfox.org/mozilla-central/rev/559b25eb41c1cbffcb90a34e008b8288312fcd25/gfx/layers/apz/test/mochitest/browser_test_group_fission.js
+ https://searchfox.org/mozilla-central/rev/559b25eb41c1cbffcb90a34e008b8288312fcd25/gfx/layers/apz/test/mochitest/helper_basic_zoom.html
+ https://searchfox.org/mozilla-central/rev/559b25eb41c1cbffcb90a34e008b8288312fcd25/browser/base/content/test/forms/browser_selectpopup.js
+*/
+
+/* import-globals-from helper_browser_test_utils.js */
+Services.scriptloader.loadSubScript(
+ new URL("helper_browser_test_utils.js", gTestPath).href,
+ this
+);
+
+add_task(async function setup_pref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Dropping the touch slop to 0 makes the tests easier to write because
+ // we can just do a one-pixel drag to get over the pan threshold rather
+ // than having to hard-code some larger value.
+ ["apz.touch_start_tolerance", "0.0"],
+ // The subtests in this test do touch-drags to pan the page, but we don't
+ // want those pans to turn into fling animations, so we increase the
+ // fling-min threshold velocity to an arbitrarily large value.
+ ["apz.fling_min_velocity_threshold", "10000"],
+ ],
+ });
+});
+
+// This test opens a select popup after pinch (apz) zooming has happened.
+add_task(async function () {
+ function httpURL(filename) {
+ let chromeURL = getRootDirectory(gTestPath) + filename;
+ //return chromeURL;
+ return chromeURL.replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+ }
+
+ const pageUrl = httpURL("helper_test_select_zoom.html");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ const input = content.document.getElementById("select");
+ const focusPromise = new Promise(resolve => {
+ input.addEventListener("focus", resolve, { once: true });
+ });
+ input.focus();
+ await focusPromise;
+ });
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.waitUntilApzStable();
+ });
+
+ const initial_resolution = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => {
+ return content.window.windowUtils.getResolution();
+ }
+ );
+
+ const initial_rect = await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ return content.wrappedJSObject.getSelectRect();
+ });
+
+ ok(
+ initial_resolution > 0,
+ "The initial_resolution is " +
+ initial_resolution +
+ ", which is some sane value"
+ );
+
+ // First, get the position of the select popup when no translations have been applied.
+ const selectPopup = await openSelectPopup();
+
+ let popup_initial_rect = selectPopup.getBoundingClientRect();
+ let popupInitialX = popup_initial_rect.left;
+ let popupInitialY = popup_initial_rect.top;
+
+ await hideSelectPopup();
+
+ ok(popupInitialX > 0, "select position before zooming (x) " + popupInitialX);
+ ok(popupInitialY > 0, "select position before zooming (y) " + popupInitialY);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.pinchZoomInWithTouch(150, 300);
+ });
+
+ // Flush state and get the resolution we're at now
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.promiseApzFlushedRepaints();
+ });
+
+ const final_resolution = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => {
+ return content.window.windowUtils.getResolution();
+ }
+ );
+
+ ok(
+ final_resolution > initial_resolution,
+ "The final resolution (" +
+ final_resolution +
+ ") is greater after zooming in"
+ );
+
+ const final_rect = await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ return content.wrappedJSObject.getSelectRect();
+ });
+
+ await openSelectPopup();
+
+ let popupRect = selectPopup.getBoundingClientRect();
+ ok(
+ Math.abs(popupRect.left - popupInitialX) > 1,
+ "popup should have moved by more than one pixel (x) " +
+ popupRect.left +
+ " " +
+ popupInitialX
+ );
+ ok(
+ Math.abs(popupRect.top - popupInitialY) > 1,
+ "popup should have moved by more than one pixel (y) " +
+ popupRect.top +
+ " " +
+ popupInitialY
+ );
+
+ ok(
+ Math.abs(
+ final_rect.left - initial_rect.left - (popupRect.left - popupInitialX)
+ ) < 1,
+ "popup should have moved approximately the same as the element (x)"
+ );
+ let tolerance = navigator.platform.includes("Linux") ? final_rect.height : 1;
+ ok(
+ Math.abs(
+ final_rect.top - initial_rect.top - (popupRect.top - popupInitialY)
+ ) < tolerance,
+ "popup should have moved approximately the same as the element (y)"
+ );
+
+ ok(
+ true,
+ "initial " +
+ initial_rect.left +
+ " " +
+ initial_rect.top +
+ " " +
+ initial_rect.width +
+ " " +
+ initial_rect.height
+ );
+ ok(
+ true,
+ "final " +
+ final_rect.left +
+ " " +
+ final_rect.top +
+ " " +
+ final_rect.width +
+ " " +
+ final_rect.height
+ );
+
+ ok(
+ true,
+ "initial popup " +
+ popup_initial_rect.left +
+ " " +
+ popup_initial_rect.top +
+ " " +
+ popup_initial_rect.width +
+ " " +
+ popup_initial_rect.height
+ );
+ ok(
+ true,
+ "final popup " +
+ popupRect.left +
+ " " +
+ popupRect.top +
+ " " +
+ popupRect.width +
+ " " +
+ popupRect.height
+ );
+
+ await hideSelectPopup();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_tab_drag_event_counts.js b/gfx/layers/apz/test/mochitest/browser_test_tab_drag_event_counts.js
new file mode 100644
index 0000000000..5dbd02ffe7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_tab_drag_event_counts.js
@@ -0,0 +1,105 @@
+/*
+ Test for https://bugzilla.mozilla.org/show_bug.cgi?id=1683776
+*/
+
+const EVENTUTILS_URL =
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js";
+var EventUtils = {};
+
+const NATIVEKEYCODES_URL =
+ "chrome://mochikit/content/tests/SimpleTest/NativeKeyCodes.js";
+
+const APZNATIVEEVENTUTILS_URL =
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js";
+
+Services.scriptloader.loadSubScript(EVENTUTILS_URL, EventUtils);
+Services.scriptloader.loadSubScript(NATIVEKEYCODES_URL, this);
+Services.scriptloader.loadSubScript(APZNATIVEEVENTUTILS_URL, this);
+
+add_task(async function test_dragging_tabs_event_counts() {
+ function httpURL(filename) {
+ let chromeURL = getRootDirectory(gTestPath) + filename;
+ return chromeURL.replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+ }
+
+ async function sendKeyEvent(key) {
+ await new Promise(resolve => {
+ EventUtils.synthesizeNativeKey(
+ EventUtils.KEYBOARD_LAYOUT_EN_US,
+ key,
+ {},
+ "",
+ "",
+ resolve
+ );
+ });
+ }
+
+ const pageUrl = httpURL("helper_test_tab_drag_event_counts.html");
+
+ // Open two windows:
+ // window 1 with 1 tab
+ // window 2 with 2 tabs
+ let win1 = await BrowserTestUtils.openNewBrowserWindow();
+ let win2 = await BrowserTestUtils.openNewBrowserWindow();
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ win1.gBrowser,
+ pageUrl
+ );
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ win2.gBrowser,
+ pageUrl
+ );
+ let tab3 = await BrowserTestUtils.openNewForegroundTab(
+ win2.gBrowser,
+ pageUrl
+ );
+
+ await SpecialPowers.spawn(tab1.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.waitUntilApzStable();
+ });
+ await SpecialPowers.spawn(tab2.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.waitUntilApzStable();
+ });
+ await SpecialPowers.spawn(tab3.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.waitUntilApzStable();
+ });
+
+ // send numerous key events
+ for (let i = 0; i < 100; i++) {
+ await sendKeyEvent(nativeArrowUpKey());
+ await sendKeyEvent(nativeArrowDownKey());
+ }
+
+ // drag and drop tab 3 from window 2 to window 1
+ let dropPromise = BrowserTestUtils.waitForEvent(
+ win1.gBrowser.tabContainer,
+ "drop"
+ );
+ let effect = EventUtils.synthesizeDrop(
+ tab3,
+ tab1,
+ [[{ type: TAB_DROP_TYPE, data: tab3 }]],
+ null,
+ win2,
+ win1
+ );
+ is(effect, "move", "Tab should be moved from win2 to win1.");
+ await dropPromise;
+
+ // focus window 1
+ await SimpleTest.promiseFocus(win1);
+
+ // send another key event
+ // when the bug occurs, an assertion is triggered when processing this event
+ await sendKeyEvent(nativeArrowUpKey());
+
+ await BrowserTestUtils.closeWindow(win1);
+ await BrowserTestUtils.closeWindow(win2);
+
+ ok(true);
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_tab_drag_zoom.js b/gfx/layers/apz/test/mochitest/browser_test_tab_drag_zoom.js
new file mode 100644
index 0000000000..e421e7bd3c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_tab_drag_zoom.js
@@ -0,0 +1,103 @@
+/* This test is a a mash up of
+ https://searchfox.org/mozilla-central/rev/016925857e2f81a9425de9e03021dcf4251cafcc/gfx/layers/apz/test/mochitest/browser_test_select_zoom.js
+ https://searchfox.org/mozilla-central/rev/016925857e2f81a9425de9e03021dcf4251cafcc/browser/base/content/test/general/browser_tab_drag_drop_perwindow.js
+*/
+
+const EVENTUTILS_URL =
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js";
+var EventUtils = {};
+
+Services.scriptloader.loadSubScript(EVENTUTILS_URL, EventUtils);
+
+add_task(async function test_dragging_zoom_handling() {
+ function httpURL(filename) {
+ let chromeURL = getRootDirectory(gTestPath) + filename;
+ //return chromeURL;
+ return chromeURL.replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+ }
+
+ const pageUrl = httpURL("helper_test_tab_drag_zoom.html");
+
+ let win1 = await BrowserTestUtils.openNewBrowserWindow();
+ let win2 = await BrowserTestUtils.openNewBrowserWindow();
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(win1.gBrowser);
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ win2.gBrowser,
+ pageUrl
+ );
+
+ await SpecialPowers.spawn(tab2.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.waitUntilApzStable();
+ });
+
+ const initial_resolution = await SpecialPowers.spawn(
+ tab2.linkedBrowser,
+ [],
+ () => {
+ return content.window.windowUtils.getResolution();
+ }
+ );
+
+ ok(
+ initial_resolution > 0,
+ "The initial_resolution is " +
+ initial_resolution +
+ ", which is some sane value"
+ );
+
+ let effect = EventUtils.synthesizeDrop(
+ tab2,
+ tab1,
+ [[{ type: TAB_DROP_TYPE, data: tab2 }]],
+ null,
+ win2,
+ win1
+ );
+ is(effect, "move", "Tab should be moved from win2 to win1.");
+
+ await SpecialPowers.spawn(win1.gBrowser.selectedBrowser, [], async () => {
+ await content.wrappedJSObject.waitUntilApzStable();
+ });
+
+ let resolution = await SpecialPowers.spawn(
+ win1.gBrowser.selectedBrowser,
+ [],
+ () => {
+ return content.window.windowUtils.getResolution();
+ }
+ );
+
+ ok(
+ resolution == initial_resolution,
+ "The resolution (" + resolution + ") is the same after tab dragging"
+ );
+
+ await SpecialPowers.spawn(win1.gBrowser.selectedBrowser, [], async () => {
+ await content.wrappedJSObject.pinchZoomInWithTouch(150, 300);
+ });
+
+ // Flush state and get the resolution we're at now
+ await SpecialPowers.spawn(win1.gBrowser.selectedBrowser, [], async () => {
+ await content.wrappedJSObject.promiseApzFlushedRepaints();
+ });
+
+ resolution = await SpecialPowers.spawn(
+ win1.gBrowser.selectedBrowser,
+ [],
+ () => {
+ return content.window.windowUtils.getResolution();
+ }
+ );
+
+ ok(
+ resolution > initial_resolution,
+ "The resolution (" + resolution + ") is greater after zooming in"
+ );
+
+ await BrowserTestUtils.closeWindow(win1);
+ await BrowserTestUtils.closeWindow(win2);
+});
diff --git a/gfx/layers/apz/test/mochitest/green100x100.png b/gfx/layers/apz/test/mochitest/green100x100.png
new file mode 100644
index 0000000000..7df25f33bd
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/green100x100.png
Binary files differ
diff --git a/gfx/layers/apz/test/mochitest/helper_background_tab_load_scroll.html b/gfx/layers/apz/test/mochitest/helper_background_tab_load_scroll.html
new file mode 100644
index 0000000000..4769861b2a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_background_tab_load_scroll.html
@@ -0,0 +1,147 @@
+<!DOCTYPE html>
+<html>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+
+<script>
+function addTextToLastDiv() {
+ let alldivs = document.getElementsByTagName('div');
+ let lastdiv = alldivs[alldivs.length-1];
+ for (let i = 0; i < 225; i++) {
+ lastdiv.appendChild(document.createTextNode("Text text text text text text text text text text text text text text text text "));
+ }
+}
+
+function doload() {
+ window.scrollBy(0,10000);
+ document.documentElement.offsetLeft;
+}
+</script>
+<body onload="doload()">
+<div>
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<!-- We use display none and then toggle to regular display in an inline script
+ at the end of our content to try to make sure we generate some reflows
+ which will generate some ScrollToImpl calls with origin restore which are
+ necessary to reproduce the bug. -->
+<div style="display: none;">
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div>
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div style="display: none;">
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div>
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div style="display: none;">
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div>
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div style="display: none;">
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div>
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div style="display: none;">
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div>
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div style="display: none;">
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div>
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div style="display: none;">
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div>
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div style="display: none;">
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div>
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div style="display: none;">
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div>
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div style="display: none;">
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div>
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div style="display: none;">
+</div>
+<script>
+addTextToLastDiv();
+</script>
+
+
+<script>
+let alldivs = document.getElementsByTagName('div');
+for (let i = 0 ; i < alldivs.length; i++) {
+ alldivs[i].style.display = "";
+ document.documentElement.offsetLeft;
+}
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_background_tab_scroll.html b/gfx/layers/apz/test/mochitest/helper_background_tab_scroll.html
new file mode 100644
index 0000000000..f55a55f0fc
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_background_tab_scroll.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="apz_test_utils.js"></script>
+<style>
+body, html {
+ margin: 0;
+}
+</style>
+<div id="scrolltarget" style="margin-top: 5000px; height: 5000px">#scrolltarget</div>
diff --git a/gfx/layers/apz/test/mochitest/helper_basic_onetouchpinch.html b/gfx/layers/apz/test/mochitest/helper_basic_onetouchpinch.html
new file mode 100644
index 0000000000..1eb1d3dd03
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_basic_onetouchpinch.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Sanity check for one-touch pinch zooming</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let visResEvt = new EventCounter(window.visualViewport, "resize");
+ let visScrEvt = new EventCounter(window.visualViewport, "scroll");
+ // Our internal visual viewport events aren't restricted to the visual view-
+ // port itself, so we can listen on the window itself, however the event
+ // listener needs to be in the system group.
+ let visResEvtInternal = new EventCounter(window, "mozvisualresize",
+ { mozSystemGroup: true });
+ let visScrEvtInternal = new EventCounter(window, "mozvisualscroll",
+ { mozSystemGroup: true });
+ let visResEvtContent = new EventCounter(window, "mozvisualresize");
+ let visScrEvtContent = new EventCounter(window, "mozvisualscroll");
+
+ var initial_resolution = await getResolution();
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+
+ // This listener will trigger the test to continue once APZ is done with
+ // processing the scroll.
+ let transformEndPromise = promiseTransformEnd();
+
+ var zoom_in = [
+ [ { x: 150, y: 300 } ],
+ [ null ],
+ [ { x: 150, y: 300 } ],
+ [ { x: 150, y: 305 } ],
+ [ { x: 150, y: 310 } ],
+ [ { x: 150, y: 315 } ],
+ [ { x: 150, y: 320 } ],
+ [ { x: 150, y: 325 } ],
+ ];
+
+ var touchIds = [0];
+ await synthesizeNativeTouchSequences(document.body, zoom_in, null, touchIds);
+
+ // Wait for the APZ:TransformEnd to be fired after touch events are processed.
+ await transformEndPromise;
+
+ // Flush state and get the resolution we're at now
+ await promiseApzFlushedRepaints();
+ let final_resolution = await getResolution();
+ ok(final_resolution > initial_resolution, "The final resolution (" + final_resolution + ") is greater after zooming in");
+
+ // Check we've got the expected events.
+ // Zooming the page should fire visual viewport resize events:
+ visResEvt.unregister();
+ ok(visResEvt.count > 0, "Got some visual viewport resize events");
+ visResEvtInternal.unregister();
+ ok(visResEvtInternal.count > 0, "Got some mozvisualresize events");
+
+ // We're zooming somewhere in the middle of the page, so the visual
+ // viewport's coordinates change, too.
+ // This is true both relative to the page (mozvisualscroll), as well as
+ // relative to the layout viewport (visual viewport "scroll" event).
+ visScrEvt.unregister();
+ ok(visScrEvt.count > 0, "Got some visual viewport scroll events");
+ visScrEvtInternal.unregister();
+ ok(visScrEvtInternal.count > 0, "Got some mozvisualscroll events");
+
+ // Our internal events shouldn't leak to normal content.
+ visResEvtContent.unregister();
+ is(visResEvtContent.count, 0, "Got no mozvisualresize events in content");
+ visScrEvtContent.unregister();
+ is(visScrEvtContent.count, 0, "Got no mozvisualscroll events in content");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ Here is some text to stare at as the test runs. It serves no functional
+ purpose, but gives you an idea of the zoom level. It's harder to tell what
+ the zoom level is when the page is just solid white.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_basic_pan.html b/gfx/layers/apz/test/mochitest/helper_basic_pan.html
new file mode 100644
index 0000000000..db96e34a70
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_basic_pan.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity panning test</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let scrEvt = new EventCounter(window, "scroll");
+ let visScrEvt = new EventCounter(window.visualViewport, "scroll");
+ // Our internal visual viewport events aren't restricted to the visual view-
+ // port itself, so we can listen on the window itself, however the event
+ // listener needs to be in the system group.
+ let visScrEvtInternal = new EventCounter(window, "mozvisualscroll",
+ { mozSystemGroup: true });
+
+ // This listener will trigger the test to continue once APZ is done with
+ // processing the scroll.
+ let transformEndPromise = promiseTransformEnd();
+
+ await synthesizeNativeTouchDrag(document.body, 10, 100, 0, -50);
+ dump("Finished native drag, waiting for transform-end observer...\n");
+
+ // Wait for the APZ:TransformEnd to be fired after touch events are processed.
+ await transformEndPromise;
+
+ // Flush state.
+ await promiseApzFlushedRepaints();
+
+ is(window.scrollY, 50, "check that the window scrolled");
+
+ // Check we've got the expected events.
+ // This page is using "width=device-width; initial-scale=1.0" and we haven't
+ // pinch-zoomed any further, so layout and visual viewports have the same
+ // size and will scroll together. Therefore we should be getting layout
+ // viewport "scroll" events as well.
+ scrEvt.unregister();
+ ok(scrEvt.count > 0, "Got some layout viewport scroll events");
+ // This one is a bit tricky: Visual viewport "scroll" events are supposed to
+ // fire only when the relative offset between layout and visual viewport
+ // changes. Even when they're both scrolling together, we may update their
+ // positions independently, though, leading to some jitter in the offset and
+ // triggering the event after all.
+ // At least for the case here, where both viewports are the same size and we
+ // have a freshly loaded page, we should however be able to keep the offset at
+ // a constant zero and therefore not cause any visual viewport scroll events
+ // to fire.
+ visScrEvt.unregister();
+ is(visScrEvt.count, 0, "Got no visual viewport scroll events");
+ visScrEvtInternal.unregister();
+ // Our internal visual viewport scroll event on the other hand only cares
+ // about the absolute offset of the visual viewport and should therefore
+ // definitively fire.
+ ok(visScrEvtInternal.count > 0, "Got some mozvisualscroll events");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div style="height: 5000px; background-color: lightgreen;">
+ This div makes the page scrollable.
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_basic_scrollend.html b/gfx/layers/apz/test/mochitest/helper_basic_scrollend.html
new file mode 100644
index 0000000000..9d71fe6251
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_basic_scrollend.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html, body { margin: 0; }
+
+ body {
+ height: 10000px;
+ }
+ </style>
+ <script>
+const searchParams = new URLSearchParams(location.search);
+
+async function test() {
+ var scrollendCount = 0;
+
+ function onScrollend(e) {
+ scrollendCount += 1;
+ }
+
+ switch (searchParams.get("chrome-only")) {
+ case "true":
+ // Add chrome-only event listener.
+ SpecialPowers.addChromeEventListener("scrollend", onScrollend, true);
+ break;
+ case "false":
+ // Add document event listener.
+ document.addEventListener("scrollend", onScrollend);
+ break;
+ default:
+ ok(false, "Unsupported chrome-only value: " + searchParams.get("chrome-only"));
+ break;
+ }
+
+ is(scrollendCount, 0, "A scrollend event should not be triggered yet");
+
+ await promiseFrame();
+
+ let wheelScrollTransformEndPromise = promiseTransformEnd();
+
+ await promiseMoveMouseAndScrollWheelOver(document.scrollingElement, 100, 100);
+
+ await wheelScrollTransformEndPromise;
+
+ await promiseFrame();
+
+ is(scrollendCount, 1, "A scrollend event should be triggered after user scroll");
+
+ scrollendCount = 0;
+
+ // Call the scrollTo function without behavior: smooth to trigger an instant
+ // programatic scroll.
+ scrollTo({ top: 500, left: 0 });
+
+ // Ensure the refresh driver has ticked.
+ await promiseFrame();
+
+ // A scrollend event should be posted after the refresh driver has ticked.
+ is(scrollendCount, 1, "A scrollend event should be triggered after instant scroll");
+
+ // If smooth scrolls are enabled, repeat the test with a smooth scroll.
+ if (SpecialPowers.getBoolPref("general.smoothScroll")) {
+
+ scrollendCount = 0;
+
+ let smoothScrollTransformEndPromise = promiseTransformEnd();
+
+ // Call the scrollTo function with behavior: smooth to trigger a programmatic
+ // scroll that should require some form of async transform.
+ scrollTo({ top: 1000, left: 0, behavior: "smooth" });
+
+ // Ensure the smooth scroll transform has finished.
+ await smoothScrollTransformEndPromise;
+
+ await promiseFrame();
+
+ is(scrollendCount, 1, "A scrollend event should be triggered after smooth scroll");
+ }
+}
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_basic_zoom.html b/gfx/layers/apz/test/mochitest/helper_basic_zoom.html
new file mode 100644
index 0000000000..55717b31a4
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_basic_zoom.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Sanity check for zooming</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let visResEvt = new EventCounter(window.visualViewport, "resize");
+ let visScrEvt = new EventCounter(window.visualViewport, "scroll");
+ // Our internal visual viewport events aren't restricted to the visual view-
+ // port itself, so we can listen on the window itself, however the event
+ // listener needs to be in the system group.
+ let visResEvtInternal = new EventCounter(window, "mozvisualresize",
+ { mozSystemGroup: true });
+ let visScrEvtInternal = new EventCounter(window, "mozvisualscroll",
+ { mozSystemGroup: true });
+ let visResEvtContent = new EventCounter(window, "mozvisualresize");
+ let visScrEvtContent = new EventCounter(window, "mozvisualscroll");
+
+ var initial_resolution = await getResolution();
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+
+ await pinchZoomInWithTouch(150, 300);
+
+ // Flush state and get the resolution we're at now
+ await promiseApzFlushedRepaints();
+ let final_resolution = await getResolution();
+ ok(final_resolution > initial_resolution, "The final resolution (" + final_resolution + ") is greater after zooming in");
+
+ // Check we've got the expected events.
+ // Pinch-zooming the page should fire visual viewport resize events:
+ visResEvt.unregister();
+ ok(visResEvt.count > 0, "Got some visual viewport resize events");
+ visResEvtInternal.unregister();
+ ok(visResEvtInternal.count > 0, "Got some mozvisualresize events");
+
+ // We're pinch-zooming somewhere in the middle of the page, so the visual
+ // viewport's coordinates change, too.
+ // This is true both relative to the page (mozvisualscroll), as well as
+ // relative to the layout viewport (visual viewport "scroll" event).
+ visScrEvt.unregister();
+ ok(visScrEvt.count > 0, "Got some visual viewport scroll events");
+ visScrEvtInternal.unregister();
+ ok(visScrEvtInternal.count > 0, "Got some mozvisualscroll events");
+
+ // Our internal events shouldn't leak to normal content.
+ visResEvtContent.unregister();
+ is(visResEvtContent.count, 0, "Got no mozvisualresize events in content");
+ visScrEvtContent.unregister();
+ is(visScrEvtContent.count, 0, "Got no mozvisualscroll events in content");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ Here is some text to stare at as the test runs. It serves no functional
+ purpose, but gives you an idea of the zoom level. It's harder to tell what
+ the zoom level is when the page is just solid white.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_browser_test_utils.js b/gfx/layers/apz/test/mochitest/helper_browser_test_utils.js
new file mode 100644
index 0000000000..ac68c9b1d4
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_browser_test_utils.js
@@ -0,0 +1,11 @@
+// For hideSelectPopup.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/base/content/test/forms/head.js",
+ this
+);
+
+function openSelectPopup(selector = "select", win = window) {
+ let popupShownPromise = BrowserTestUtils.waitForSelectPopupShown(win);
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }, win);
+ return popupShownPromise;
+}
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1162771.html b/gfx/layers/apz/test/mochitest/helper_bug1162771.html
new file mode 100644
index 0000000000..3503341d41
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1162771.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for touchend on media elements</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function handleTouchStart() {
+ let v = document.getElementById("video");
+ let d = document.getElementById("div");
+
+ let e = await new Promise(resolve => {
+ document.body.addEventListener("touchstart", resolve, {once: true});
+ });
+
+ if (e.target === v || e.target === d) {
+ e.target.style.display = "none";
+ ok(true, "Set display to none on #" + e.target.id);
+ } else {
+ ok(false, "Got unexpected touchstart on " + e.target);
+ }
+ await promiseAllPaintsDone();
+}
+
+async function handleTouchEnd() {
+ let v = document.getElementById("video");
+ let d = document.getElementById("div");
+
+ let e = await new Promise(resolve => {
+ document.body.addEventListener("touchend", resolve, {once: true});
+ });
+
+ if (e.target === v || e.target === d) {
+ e.target._gotTouchend = true;
+ ok(true, "Got touchend event on #" + e.target.id);
+ }
+}
+
+async function test() {
+ var v = document.getElementById("video");
+ var d = document.getElementById("div");
+
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+
+ let startHandledPromise = handleTouchStart();
+ let endHandledPromise = handleTouchEnd();
+ var pt = await coordinatesRelativeToScreen({ offsetX: 25, offsetY: 5, target: v });
+ utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, pt.x, pt.y, 1, 90, null);
+ await startHandledPromise;
+ utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, pt.x, pt.y, 1, 90, null);
+ await endHandledPromise;
+ ok(v._gotTouchend, "Touchend was received on video element");
+
+ startHandledPromise = handleTouchStart();
+ endHandledPromise = handleTouchEnd();
+ pt = await coordinatesRelativeToScreen({ offsetX: 25, offsetY: 5, target: d });
+ utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, pt.x, pt.y, 1, 90, null);
+ await startHandledPromise;
+ utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, pt.x, pt.y, 1, 90, null);
+ await endHandledPromise;
+ ok(d._gotTouchend, "Touchend was received on div element");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ * {
+ font-size: 24px;
+ box-sizing: border-box;
+ }
+
+ #video {
+ display:block;
+ position:absolute;
+ top: 100px;
+ left:0;
+ width: 50%;
+ height: 100px;
+ border:solid black 1px;
+ background-color: #8a8;
+ }
+
+ #div {
+ display:block;
+ position:absolute;
+ top: 100px;
+ left: 50%;
+ width: 50%;
+ height: 100px;
+ border:solid black 1px;
+ background-color: #88a;
+ }
+ </style>
+</head>
+<body>
+ <p>Tap on the colored boxes to hide them.</p>
+ <video id="video"></video>
+ <div id="div"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1271432.html b/gfx/layers/apz/test/mochitest/helper_bug1271432.html
new file mode 100644
index 0000000000..1e40421a8f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1271432.html
@@ -0,0 +1,573 @@
+<head>
+ <title>Ensure that the hit region doesn't get unexpectedly expanded</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+<script type="application/javascript">
+async function test() {
+ var scroller = document.getElementById("scroller");
+ var scrollerPos = scroller.scrollTop;
+ var dx = 100, dy = 50;
+
+ is(window.scrollY, 0, "Initial page scroll position should be 0");
+ is(scrollerPos, 0, "Initial scroller position should be 0");
+
+ await promiseMoveMouseAndScrollWheelOver(scroller, dx, dy);
+
+ is(window.scrollY, 0, "Page scroll position should still be 0");
+ ok(scroller.scrollTop > scrollerPos, "Scroller should have scrolled");
+
+ // wait for it to layerize fully and then try again
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ scrollerPos = scroller.scrollTop;
+
+ await promiseMoveMouseAndScrollWheelOver(scroller, dx, dy);
+ is(window.scrollY, 0, "Page scroll position should still be 0 after layerization");
+ ok(scroller.scrollTop > scrollerPos, "Scroller should have continued scrolling");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+<style>
+a#with_after_content {
+ background-color: #F16725;
+ opacity: 0.8;
+ display: inline-block;
+ margin-top: 40px;
+ margin-left: 40px;
+}
+a#with_after_content::after {
+ content: " ";
+ position: absolute;
+ width: 0px;
+ height: 0px;
+ bottom: 40px;
+ z-index: -1;
+ right: 40px;
+ background-color: transparent;
+ border-style: solid;
+ border-width: 15px 15px 15px 0;
+ border-color: #d54e0e transparent transparent transparent;
+ box-shadow: none;
+ box-sizing: border-box;
+}
+div#scroller {
+ overflow-y: scroll;
+ width: 50%;
+ height: 50%;
+}
+</style>
+</head>
+<body>
+<a id="with_after_content">Some text</a>
+
+<div id="scroller">
+Scrolling on the very left edge of this div will work.
+Scrolling on the right side of this div (starting with the left edge of the orange box above) should work, but doesn't.<br/>
+0<br>
+1<br>
+2<br>
+3<br>
+4<br>
+5<br>
+6<br>
+7<br>
+8<br>
+9<br>
+10<br>
+11<br>
+12<br>
+13<br>
+14<br>
+15<br>
+16<br>
+17<br>
+18<br>
+19<br>
+20<br>
+21<br>
+22<br>
+23<br>
+24<br>
+25<br>
+26<br>
+27<br>
+28<br>
+29<br>
+30<br>
+31<br>
+32<br>
+33<br>
+34<br>
+35<br>
+36<br>
+37<br>
+38<br>
+39<br>
+40<br>
+41<br>
+42<br>
+43<br>
+44<br>
+45<br>
+46<br>
+47<br>
+48<br>
+49<br>
+50<br>
+51<br>
+52<br>
+53<br>
+54<br>
+55<br>
+56<br>
+57<br>
+58<br>
+59<br>
+60<br>
+61<br>
+62<br>
+63<br>
+64<br>
+65<br>
+66<br>
+67<br>
+68<br>
+69<br>
+70<br>
+71<br>
+72<br>
+73<br>
+74<br>
+75<br>
+76<br>
+77<br>
+78<br>
+79<br>
+80<br>
+81<br>
+82<br>
+83<br>
+84<br>
+85<br>
+86<br>
+87<br>
+88<br>
+89<br>
+90<br>
+91<br>
+92<br>
+93<br>
+94<br>
+95<br>
+96<br>
+97<br>
+98<br>
+99<br>
+100<br>
+101<br>
+102<br>
+103<br>
+104<br>
+105<br>
+106<br>
+107<br>
+108<br>
+109<br>
+110<br>
+111<br>
+112<br>
+113<br>
+114<br>
+115<br>
+116<br>
+117<br>
+118<br>
+119<br>
+120<br>
+121<br>
+122<br>
+123<br>
+124<br>
+125<br>
+126<br>
+127<br>
+128<br>
+129<br>
+130<br>
+131<br>
+132<br>
+133<br>
+134<br>
+135<br>
+136<br>
+137<br>
+138<br>
+139<br>
+140<br>
+141<br>
+142<br>
+143<br>
+144<br>
+145<br>
+146<br>
+147<br>
+148<br>
+149<br>
+150<br>
+151<br>
+152<br>
+153<br>
+154<br>
+155<br>
+156<br>
+157<br>
+158<br>
+159<br>
+160<br>
+161<br>
+162<br>
+163<br>
+164<br>
+165<br>
+166<br>
+167<br>
+168<br>
+169<br>
+170<br>
+171<br>
+172<br>
+173<br>
+174<br>
+175<br>
+176<br>
+177<br>
+178<br>
+179<br>
+180<br>
+181<br>
+182<br>
+183<br>
+184<br>
+185<br>
+186<br>
+187<br>
+188<br>
+189<br>
+190<br>
+191<br>
+192<br>
+193<br>
+194<br>
+195<br>
+196<br>
+197<br>
+198<br>
+199<br>
+200<br>
+201<br>
+202<br>
+203<br>
+204<br>
+205<br>
+206<br>
+207<br>
+208<br>
+209<br>
+210<br>
+211<br>
+212<br>
+213<br>
+214<br>
+215<br>
+216<br>
+217<br>
+218<br>
+219<br>
+220<br>
+221<br>
+222<br>
+223<br>
+224<br>
+225<br>
+226<br>
+227<br>
+228<br>
+229<br>
+230<br>
+231<br>
+232<br>
+233<br>
+234<br>
+235<br>
+236<br>
+237<br>
+238<br>
+239<br>
+240<br>
+241<br>
+242<br>
+243<br>
+244<br>
+245<br>
+246<br>
+247<br>
+248<br>
+249<br>
+250<br>
+251<br>
+252<br>
+253<br>
+254<br>
+255<br>
+256<br>
+257<br>
+258<br>
+259<br>
+260<br>
+261<br>
+262<br>
+263<br>
+264<br>
+265<br>
+266<br>
+267<br>
+268<br>
+269<br>
+270<br>
+271<br>
+272<br>
+273<br>
+274<br>
+275<br>
+276<br>
+277<br>
+278<br>
+279<br>
+280<br>
+281<br>
+282<br>
+283<br>
+284<br>
+285<br>
+286<br>
+287<br>
+288<br>
+289<br>
+290<br>
+291<br>
+292<br>
+293<br>
+294<br>
+295<br>
+296<br>
+297<br>
+298<br>
+299<br>
+300<br>
+301<br>
+302<br>
+303<br>
+304<br>
+305<br>
+306<br>
+307<br>
+308<br>
+309<br>
+310<br>
+311<br>
+312<br>
+313<br>
+314<br>
+315<br>
+316<br>
+317<br>
+318<br>
+319<br>
+320<br>
+321<br>
+322<br>
+323<br>
+324<br>
+325<br>
+326<br>
+327<br>
+328<br>
+329<br>
+330<br>
+331<br>
+332<br>
+333<br>
+334<br>
+335<br>
+336<br>
+337<br>
+338<br>
+339<br>
+340<br>
+341<br>
+342<br>
+343<br>
+344<br>
+345<br>
+346<br>
+347<br>
+348<br>
+349<br>
+350<br>
+351<br>
+352<br>
+353<br>
+354<br>
+355<br>
+356<br>
+357<br>
+358<br>
+359<br>
+360<br>
+361<br>
+362<br>
+363<br>
+364<br>
+365<br>
+366<br>
+367<br>
+368<br>
+369<br>
+370<br>
+371<br>
+372<br>
+373<br>
+374<br>
+375<br>
+376<br>
+377<br>
+378<br>
+379<br>
+380<br>
+381<br>
+382<br>
+383<br>
+384<br>
+385<br>
+386<br>
+387<br>
+388<br>
+389<br>
+390<br>
+391<br>
+392<br>
+393<br>
+394<br>
+395<br>
+396<br>
+397<br>
+398<br>
+399<br>
+400<br>
+401<br>
+402<br>
+403<br>
+404<br>
+405<br>
+406<br>
+407<br>
+408<br>
+409<br>
+410<br>
+411<br>
+412<br>
+413<br>
+414<br>
+415<br>
+416<br>
+417<br>
+418<br>
+419<br>
+420<br>
+421<br>
+422<br>
+423<br>
+424<br>
+425<br>
+426<br>
+427<br>
+428<br>
+429<br>
+430<br>
+431<br>
+432<br>
+433<br>
+434<br>
+435<br>
+436<br>
+437<br>
+438<br>
+439<br>
+440<br>
+441<br>
+442<br>
+443<br>
+444<br>
+445<br>
+446<br>
+447<br>
+448<br>
+449<br>
+450<br>
+451<br>
+452<br>
+453<br>
+454<br>
+455<br>
+456<br>
+457<br>
+458<br>
+459<br>
+460<br>
+461<br>
+462<br>
+463<br>
+464<br>
+465<br>
+466<br>
+467<br>
+468<br>
+469<br>
+470<br>
+471<br>
+472<br>
+473<br>
+474<br>
+475<br>
+476<br>
+477<br>
+478<br>
+479<br>
+480<br>
+481<br>
+482<br>
+483<br>
+484<br>
+485<br>
+486<br>
+487<br>
+488<br>
+489<br>
+490<br>
+491<br>
+492<br>
+493<br>
+494<br>
+495<br>
+496<br>
+497<br>
+498<br>
+499<br>
+</div>
+<div style="height: 1000px">this div makes the page scrollable</div>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1280013.html b/gfx/layers/apz/test/mochitest/helper_bug1280013.html
new file mode 100644
index 0000000000..6b7d5cf4c3
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1280013.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html style="overflow:hidden">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=width-device; initial-scale=1.0">
+ <title>Test for bug 1280013</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+async function test() {
+ ok(screen.height > 500, "Screen height must be at least 500 pixels for this test to work");
+
+ // Scroll down to the iframe. Do it in two drags instead of one in case the
+ // device screen is short.
+ let transformEnd = promiseTransformEnd();
+ await synthesizeNativeTouchDrag(window, 10, 200, 0, -175);
+ await transformEnd;
+
+ transformEnd = promiseTransformEnd();
+ await synthesizeNativeTouchDrag(window, 10, 200, 0, -175);
+ await transformEnd;
+
+ // Now the top of the visible area should be at y=350 of the top-level page,
+ // so if the screen is >= 500px tall, the entire iframe should be visible, at
+ // least vertically.
+
+ // However, because of the overflow:hidden on the root elements, all this
+ // scrolling is happening in APZ and is not reflected in the main-thread
+ // scroll position (it is stored in the callback transform instead). We check
+ // this by checking the scroll offset.
+ await promiseOnlyApzControllerFlushed();
+ is(window.scrollY, 0, "Main-thread scroll position is still at 0");
+
+ // Scroll the iframe by 150px.
+ var subframe = document.getElementById("subframe");
+ transformEnd = promiseTransformEnd();
+ await synthesizeNativeTouchDrag(subframe, 10, 100, 0, -150);
+ await transformEnd;
+
+ // Flush any pending paints on the APZ side, and wait for the main thread
+ // to process them all so that we get the correct test data
+ await promiseApzFlushedRepaints();
+
+ // get the displayport for the subframe
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ var contentPaints = utils.getContentAPZTestData().paints;
+ var lastPaint = convertScrollFrameData(getLastNonemptyBucket(contentPaints).scrollFrames);
+ var foundIt = 0;
+ for (var scrollId in lastPaint) {
+ if (("contentDescription" in lastPaint[scrollId]) &&
+ (lastPaint[scrollId].contentDescription.includes("tall_html"))) {
+ var dp = getPropertyAsRect(lastPaint, scrollId, "displayport");
+ ok(dp.y <= 0, "The displayport top should be less than or equal to zero to cover the visible part of the subframe; it is " + dp.y);
+ ok(dp.y + dp.height >= subframe.clientHeight, "The displayport bottom should be greater than the clientHeight; it is " + (dp.y + dp.height));
+ foundIt++;
+ }
+ }
+ is(foundIt, 1, "Found exactly one displayport for the subframe we were interested in.");
+}
+
+SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body style="overflow:hidden">
+ The iframe below is at (0, 400). Scroll it into view, and then scroll the contents. The content should be fully rendered in high-resolution.
+ <iframe id="subframe" style="position:absolute; left: 0px; top: 400px; width: 300px; height: 175px" src="helper_tall.html"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1285070.html b/gfx/layers/apz/test/mochitest/helper_bug1285070.html
new file mode 100644
index 0000000000..0df3a77f4a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1285070.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test pointer events are dispatched once for touch tap</title>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript">
+ async function test() {
+ let eventsList = ["pointerover", "pointerenter", "pointerdown",
+ "pointerup", "pointerleave", "pointerout",
+ "mousedown", "mouseup",
+ "touchstart", "touchend", "click"];
+ let eventsCount = {};
+
+ eventsList.forEach((eventName) => {
+ eventsCount[eventName] = 0;
+ document.getElementById("div1").addEventListener(eventName, (event) => {
+ ++eventsCount[event.type];
+ ok(true, "Received event " + event.type);
+ });
+ });
+
+ document.addEventListener("click", (event) => {
+ is(event.target, document.getElementById("div1"), "Clicked on div (at " + event.clientX + "," + event.clientY + ")");
+ for (var key in eventsCount) {
+ is(eventsCount[key], 1, "Event " + key + " should be generated once");
+ }
+ subtestDone();
+ });
+
+ await synthesizeNativeTap(document.getElementById("div1"), 100, 100);
+ }
+
+ waitUntilApzStable().then(test);
+
+ </script>
+</head>
+<body>
+ <div id="div1" style="width: 200px; height: 200px; background: black"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1299195.html b/gfx/layers/apz/test/mochitest/helper_bug1299195.html
new file mode 100644
index 0000000000..b1aad7ef11
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1299195.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0; user-scalable=no">
+ <title>Test pointer events are dispatched once for touch tap</title>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript">
+ /** Test for Bug 1299195 */
+ async function runTests() {
+ let target0 = document.getElementById("target0");
+ let mouseup_count = 0;
+ let mousedown_count = 0;
+ let pointerup_count = 0;
+ let pointerdown_count = 0;
+
+ target0.addEventListener("mouseup", () => {
+ ++mouseup_count;
+ if (mouseup_count == 2) {
+ is(mousedown_count, 2, "Double tap with touch should fire 2 mousedown events");
+ is(mouseup_count, 2, "Double tap with touch should fire 2 mouseup events");
+ is(pointerdown_count, 2, "Double tap with touch should fire 2 pointerdown events");
+ is(pointerup_count, 2, "Double tap with touch should fire 2 pointerup events");
+ subtestDone();
+ }
+ });
+ target0.addEventListener("mousedown", () => {
+ ++mousedown_count;
+ });
+ target0.addEventListener("pointerup", () => {
+ ++pointerup_count;
+ });
+ target0.addEventListener("pointerdown", () => {
+ ++pointerdown_count;
+ });
+ await synthesizeNativeTap(document.getElementById("target0"), 100, 100);
+ await synthesizeNativeTap(document.getElementById("target0"), 100, 100);
+ }
+ waitUntilApzStable().then(runTests);
+ </script>
+</head>
+<body>
+ <div id="target0" style="width: 200px; height: 200px; background: green"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1326290.html b/gfx/layers/apz/test/mochitest/helper_bug1326290.html
new file mode 100644
index 0000000000..17b5a36eaa
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1326290.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Dragging the mouse on a inactive scrollframe's scrollbar</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ #scrollable {
+ overflow: scroll;
+ height: 200px;
+ width: 200px;
+ }
+ .content {
+ width: 1000px;
+ height: 2000px;
+ }
+ </style>
+ <script type="text/javascript">
+
+async function test() {
+ var scrollableDiv = document.getElementById("scrollable");
+ let scrollPromise = new Promise(resolve => {
+ scrollableDiv.addEventListener("scroll", resolve, {once: true});
+ });
+
+ var dragFinisher = await promiseVerticalScrollbarDrag(scrollableDiv);
+ if (!dragFinisher) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ // the events above might be stuck in APZ input queue for a bit until the
+ // layer is activated, so we wait here until the scroll event listener is
+ // triggered.
+ await scrollPromise;
+
+ await dragFinisher();
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ // After dragging the scrollbar 20px on a 200px-high scrollable div, we should
+ // have scrolled approx 10% of the 2000px high content. There might have been
+ // scroll arrows and such so let's just have a minimum bound of 50px to be safe.
+ ok(scrollableDiv.scrollTop > 50, "Scrollbar drag resulted in a scroll position of " + scrollableDiv.scrollTop);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div id="scrollable">
+ <div class="content">Some content inside the inactive scrollframe</div>
+ </div>
+ <div class="content">Some content to ensure the root scrollframe is scrollable and the overflow:scroll div remains inactive</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1331693.html b/gfx/layers/apz/test/mochitest/helper_bug1331693.html
new file mode 100644
index 0000000000..6dd0de13cb
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1331693.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Dragging the mouse on a scrollframe inside an SVGEffects</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="text/javascript">
+
+async function test() {
+ var scrollableDiv = document.getElementById("scrollable");
+ let scrollPromise = new Promise(resolve => {
+ scrollableDiv.addEventListener("scroll", resolve, {once: true});
+ });
+
+ var dragFinisher = await promiseVerticalScrollbarDrag(scrollableDiv);
+ if (!dragFinisher) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ // the events above might be stuck in APZ input queue for a bit until the
+ // layer is activated, so we wait here until the scroll event listener is
+ // triggered.
+ await scrollPromise;
+
+ await dragFinisher();
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ // After dragging the scrollbar 20px on a 200px-high scrollable div, we should
+ // have scrolled approx 10% of the 2000px high content. There might have been
+ // scroll arrows and such so let's just have a minimum bound of 50px to be safe.
+ ok(scrollableDiv.scrollTop > 50, "Scrollbar drag resulted in a scroll position of " + scrollableDiv.scrollTop);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ #svgeffects {
+ background-color: lightgreen;
+ width: 300px;
+ height: 300px;
+ clip-path: circle(200px at 100% 0); /* ensure scrollthumb is in the clip */
+ }
+ #scrollable {
+ overflow: scroll;
+ height: 200px;
+ width: 200px;
+ }
+ #content {
+ width: 1000px;
+ height: 2000px;
+ background-image: linear-gradient(red,blue);
+ }
+ </style>
+</head>
+<body>
+ <div id="svgeffects">A div that generate an svg effects display item
+ <div id="scrollable">
+ <div id="content">Some content inside the scrollframe</div>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1346632.html b/gfx/layers/apz/test/mochitest/helper_bug1346632.html
new file mode 100644
index 0000000000..f91f8159b5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1346632.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Dragging the scrollbar on a page with a fixed-positioned element just past the right edge of the content</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ height: 2000px;
+ }
+ #fixed {
+ width: 240px;
+ height: 100%;
+ position: fixed;
+ top: 0px;
+ right: -240px;
+ z-index: 1000;
+ overflow-y: scroll;
+ }
+ #fixed-content {
+ height: 2000px;
+ }
+ </style>
+ <script type="text/javascript">
+async function test() {
+ var root = document.scrollingElement;
+ var scrollPos = root.scrollTop;
+ var scrollPromise = new Promise((resolve, reject) => {
+ document.addEventListener("scroll", () => {
+ ok(root.scrollTop > scrollPos, "document scrolled after dragging scrollbar");
+ resolve();
+ }, {once: true});
+ });
+
+ if (window.innerWidth == root.clientWidth) {
+ // No scrollbar, abort the test. This can happen e.g. on local macOS runs
+ // with OS settings to only show scrollbars on trackpad/mouse activity.
+ ok(false, "No scrollbars found, cannot run this test!");
+ return;
+ }
+
+ var scrollbarX = (window.innerWidth + root.clientWidth) / 2;
+ // Move the mouse to the scrollbar
+ await promiseNativeMouseEventWithAPZ({
+ target: root,
+ offsetX: scrollbarX,
+ offsetY: 100,
+ type: "mousemove",
+ });
+ // mouse down
+ await promiseNativeMouseEventWithAPZ({
+ target: root,
+ offsetX: scrollbarX,
+ offsetY: 100,
+ type: "mousedown",
+ });
+ // drag vertically
+ await promiseNativeMouseEventWithAPZ({
+ target: root,
+ offsetX: scrollbarX,
+ offsetY: 150,
+ type: "mousemove",
+ });
+ // wait for the scroll listener to fire
+ await scrollPromise;
+ // and release
+ await promiseNativeMouseEventWithAPZ({
+ target: root,
+ offsetX: scrollbarX,
+ offsetY: 150,
+ type: "mouseup",
+ });
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div id="fixed">
+ <p id="fixed-content"></p>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1414336.html b/gfx/layers/apz/test/mochitest/helper_bug1414336.html
new file mode 100644
index 0000000000..636328b7e4
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1414336.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1414336
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1414336</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="text/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ #target0 {
+ width: 200px;
+ height: 400px;
+ touch-action: auto;
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1414336">Mozilla Bug 1414336</a>
+<p id="display"></p>
+<div id="target0">
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+</div>
+<script type="text/javascript">
+/** Test for Bug 1414336 */
+waitUntilApzStable().then(async () => {
+ let target0 = window.document.getElementById("target0");
+ let target0_events = ["pointerdown", "pointermove"];
+
+ target0_events.forEach((elem, index, arr) => {
+ target0.addEventListener(elem, (event) => {
+ is(event.type, target0_events[0], "receive " + event.type + " on target0");
+ target0_events.shift();
+ }, { once: true });
+ });
+
+ target0.addEventListener("pointercancel", (event) => {
+ ok(false, "Shouldn't receive pointercancel when content prevents default on touchstart");
+ // Wait until the event is done processing before we end the subtest,
+ // otherwise on Android the pointer events pref is flipped back to false
+ // and debug builds will assert.
+ setTimeout(subtestDone, 0);
+ }, { once: true });
+
+ target0.addEventListener("touchstart", (event) => {
+ event.preventDefault();
+ }, { once: true });
+
+ target0.addEventListener("pointerup", (event) => {
+ ok(!target0_events.length, " should receive " + target0_events + " on target0");
+ // Wait until the event is done processing before we end the subtest,
+ // otherwise on Android the pointer events pref is flipped back to false
+ // and debug builds will assert.
+ setTimeout(subtestDone, 0);
+ }, { once: true });
+
+ await synthesizeNativeTouchDrag(target0, 2, 2, 0, 80);
+});
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1462961.html b/gfx/layers/apz/test/mochitest/helper_bug1462961.html
new file mode 100644
index 0000000000..d37d041800
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1462961.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Dragging the mouse on a transformed scrollframe inside a fixed-pos element</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="text/javascript">
+
+async function test() {
+ var scrollableDiv = document.getElementById("scrollable");
+ let scrollPromise = new Promise(resolve => {
+ scrollableDiv.addEventListener("scroll", resolve, {once: true});
+ });
+
+ // Scroll down a small amount (10px). The bug in this case is that the
+ // scrollthumb remains a little "above" where it's supposed to be, so if the
+ // bug manifests here, then the thumb will remain at the top of the track
+ // and the scroll position will remain at 0.
+ var dragFinisher = await promiseVerticalScrollbarDrag(scrollableDiv, 10, 10);
+ if (!dragFinisher) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ // the events above might be stuck in APZ input queue for a bit until the
+ // layer is activated, so we wait here until the scroll event listener is
+ // triggered.
+ await scrollPromise;
+
+ await dragFinisher();
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ // In this case we just want to make sure the scroll position moved from 0
+ // which indicates the thumb dragging worked properly.
+ ok(scrollableDiv.scrollTop > 0, "Scrollbar drag resulted in a scroll position of " + scrollableDiv.scrollTop);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ #fixed {
+ position: fixed;
+ left: 0;
+ top: 0;
+ width: 300px;
+ height: 100%;
+ }
+ #scrollable {
+ transform: translateY(100px);
+ overflow: scroll;
+ height: 100%;
+ }
+ #content {
+ height: 5000px;
+ background-image: linear-gradient(red,blue);
+ }
+ </style>
+</head>
+<body>
+<div id="fixed">
+ <div id="scrollable">
+ <div id="content"></div>
+ </div>
+</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1473108.html b/gfx/layers/apz/test/mochitest/helper_bug1473108.html
new file mode 100644
index 0000000000..118ac3fc54
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1473108.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1473108
+-->
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for Bug 1473108</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ .a {
+ background: green;
+ height: 64px;
+ width: 32px;
+ display: block;
+ }
+ span::before {
+ content: "";
+ background: red;
+ height: 32px;
+ width: 32px;
+ display: block;
+ }
+ span:active::after {
+ content: "";
+ }
+</style>
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1473108">Mozilla Bug 1473108</a>
+ <a class="a" id="event"><span id="target"></span></a>
+
+ <script type="application/javascript">
+
+ waitUntilApzStable().then(async () => {
+ let target = document.getElementById("target");
+ target.addEventListener("click", function(e) {
+ is(e.target, target, `Clicked on at (${e.clientX}, ${e.clientY})`);
+ subtestDone();
+ });
+ await synthesizeNativeTap(target, 5, 5);
+ });
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1490393-2.html b/gfx/layers/apz/test/mochitest/helper_bug1490393-2.html
new file mode 100644
index 0000000000..749110449e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1490393-2.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Dragging the mouse on a scrollbar for a scrollframe inside nested transforms</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="text/javascript">
+
+async function test() {
+ var scrollableDiv = document.getElementById("scrollable");
+ let scrollPromise = new Promise(resolve => {
+ scrollableDiv.addEventListener("scroll", resolve, {once: true});
+ });
+
+ // Scroll down a small amount (10px). The bug in this case is that the
+ // scrollthumb "jumps" by an additional 40 pixels (height of the "gap" div)
+ // and the scrollframe scrolls by a corresponding amount. So after doing this
+ // drag we check the scroll position to make sure it hasn't scrolled by
+ // too much.
+ // Given the scrollable height of 2000px and scrollframe height of 400px,
+ // the scrollthumb should be approximately 80px tall, and dragging it 10px
+ // should scroll approximately 50 pixels. If the bug manifests, it will get
+ // dragged 50px and scroll approximately 250px.
+ var dragFinisher = await promiseVerticalScrollbarDrag(scrollableDiv, 10, 10);
+ if (!dragFinisher) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ // the events above might be stuck in APZ input queue for a bit until the
+ // layer is activated, so we wait here until the scroll event listener is
+ // triggered.
+ await scrollPromise;
+
+ await dragFinisher();
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ // In this case we just want to make sure the scroll position moved from 0
+ // which indicates the thumb dragging worked properly.
+ ok(scrollableDiv.scrollTop < 100, "Scrollbar drag resulted in a scroll position of " + scrollableDiv.scrollTop);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div id="gap" style="min-height: 40px"></div>
+ <div style="height: 400px; transform: translateZ(0)">
+ <div style="height: 100%; overflow-x: auto; overflow-y: hidden; transform: translateZ(0)">
+ <div id="scrollable" style="display: inline-block; height: 100%; overflow-y: auto; transform: translateZ(0)">
+ <div style="min-height: 2000px">Yay text</div>
+ </div>
+ <div style="display: inline-block; width: 2000px; height: 100%;"></div>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1490393.html b/gfx/layers/apz/test/mochitest/helper_bug1490393.html
new file mode 100644
index 0000000000..6c18a2d24e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1490393.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Dragging the mouse on a scrollbar for a scrollframe inside nested transforms</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="text/javascript">
+
+async function test() {
+ var scrollableDiv = document.getElementById("scrollable");
+ let scrollPromise = new Promise(resolve => {
+ scrollableDiv.addEventListener("scroll", resolve, {once: true});
+ });
+
+ // Scroll down a small amount (10px). The bug in this case is that the
+ // scrollthumb "jumps" by an additional 40 pixels (height of the "gap" div)
+ // and the scrollframe scrolls by a corresponding amount. So after doing this
+ // drag we check the scroll position to make sure it hasn't scrolled by
+ // too much.
+ // Given the scrollable height of 2000px and scrollframe height of 400px,
+ // the scrollthumb should be approximately 80px tall, and dragging it 10px
+ // should scroll approximately 50 pixels. If the bug manifests, it will get
+ // dragged 50px and scroll approximately 250px.
+ var dragFinisher = await promiseVerticalScrollbarDrag(scrollableDiv, 10, 10);
+ if (!dragFinisher) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ // the events above might be stuck in APZ input queue for a bit until the
+ // layer is activated, so we wait here until the scroll event listener is
+ // triggered.
+ await scrollPromise;
+
+ await dragFinisher();
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ // In this case we just want to make sure the scroll position moved from 0
+ // which indicates the thumb dragging worked properly.
+ ok(scrollableDiv.scrollTop < 100, "Scrollbar drag resulted in a scroll position of " + scrollableDiv.scrollTop);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div id="gap" style="min-height: 40px"></div>
+ <div style="height: 400px; transform: translateZ(0)">
+ <div style="height: 100%; opacity: 0.9; will-change: opacity">
+ <div id="scrollable" style="height: 100%; overflow-y: auto; transform: translateZ(0)">
+ <div style="min-height: 2000px">Yay text</div>
+ </div>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1502010_unconsumed_pan.html b/gfx/layers/apz/test/mochitest/helper_bug1502010_unconsumed_pan.html
new file mode 100644
index 0000000000..73badf4bc7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1502010_unconsumed_pan.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test pointercancel doesn't get sent for horizontal panning on a pan-y element</title>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript">
+ var pointerMoveCount = 0;
+ var lastPointerCoord = -1;
+ var apzFlushed = false;
+ var endEventReceived = false;
+ var testEndResolveFunc = null;
+ var testEndPromise = new Promise(resolve => {
+ testEndResolveFunc = resolve;
+ });
+
+ function checkForTestEnd() {
+ if (apzFlushed && endEventReceived) {
+ var target = document.getElementById("carousel");
+ target.removeEventListener("pointermove", moveListener);
+
+ ok(pointerMoveCount > 0, "Got " + pointerMoveCount + " pointermove events");
+ is(document.scrollingElement.scrollTop, 0, "Document didn't y-scroll");
+ is(document.scrollingElement.scrollLeft, 0, "Document didn't x-scroll");
+
+ testEndResolveFunc();
+ }
+ }
+
+ function moveListener(event) {
+ ok(event.clientX >= lastPointerCoord, "Got nondecreasing pointermove to " + event.clientX + "," + event.clientY);
+ lastPointerCoord = event.clientX;
+ pointerMoveCount++;
+ }
+
+ async function test() {
+ var target = document.getElementById("carousel");
+ target.addEventListener("pointercancel", (event) => {
+ ok(false, "Received pointercancel, uh-oh!");
+ endEventReceived = true;
+ setTimeout(checkForTestEnd, 0);
+ }, {once: true});
+ target.addEventListener("pointerup", () => {
+ ok(true, "Received pointerup");
+ endEventReceived = true;
+ setTimeout(checkForTestEnd, 0);
+ }, {once: true});
+
+ target.addEventListener("pointermove", moveListener);
+
+ // Drag mostly horizontally but also slightly vertically. If the
+ // touch-action were not respected due to a bug this might result
+ // in vertical scrolling instead of pointermove events.
+ await new Promise(resolve => {
+ synthesizeNativeTouchDrag(target, 10, 10, 200, -10, resolve);
+ });
+ await promiseOnlyApzControllerFlushed();
+ apzFlushed = true;
+
+ setTimeout(checkForTestEnd, 0);
+
+ await testEndPromise;
+ }
+
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div id="carousel" style="height: 50px; touch-action: pan-y; background-color: blue"></div>
+ <div id="spacer" style="height: 2000px"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1506497_touch_action_fixed_on_fixed.html b/gfx/layers/apz/test/mochitest/helper_bug1506497_touch_action_fixed_on_fixed.html
new file mode 100644
index 0000000000..cc73fe99ea
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1506497_touch_action_fixed_on_fixed.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for Bug 1506497</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ document.getElementById("overlay").addEventListener("touchstart", function(e) {
+ // no need to do anything here. Just having a non-passive touchstart
+ // listener will force APZ to wait for the main thread to handle the
+ // touch event. The bug is that the touch-action:none property on the
+ // overlay gets ignored in this case and the body gets scrolled.
+ }, {passive: false});
+
+ // Ensure that APZ gets updated hit-test info
+ await promiseAllPaintsDone();
+
+ // Register a listener that fails the test if the APZ:TransformEnd event fires,
+ // because this test shouldn't actually be triggering any transforms
+ SpecialPowers.Services.obs.addObserver(function() {
+ ok(false, "The test fired an unexpected APZ:TransformEnd");
+ }, "APZ:TransformEnd");
+
+ // Listen for changes to the visual viewport offset.
+ let visScrEvtInternal = new EventCounter(window, "mozvisualscroll",
+ { mozSystemGroup: true });
+
+ // This promise will resolve after the main thread has processed
+ // all the synthesized touch events.
+ let promiseTouchEnd = new Promise(resolve => {
+ var waitForTouchEnd = function(e) {
+ dump("touchend listener hit\n");
+ resolve();
+ };
+ document.documentElement.addEventListener(
+ "touchend",
+ waitForTouchEnd,
+ {passive: true, once: true}
+ );
+ });
+
+ await synthesizeNativeTouchDrag(document.getElementById("boxOnTop"), 5, 5, 0, -50);
+ dump("finished drag, waiting for touchend listener...");
+ await promiseTouchEnd;
+
+ // Flush state.
+ await promiseApzFlushedRepaints();
+
+ // Check that the touch was prevented, per the touch-action
+ is(window.scrollY, 0, "window didn't scroll");
+ is(document.scrollingElement.scrollTop, 0, "scrollingElement didn't scroll");
+ visScrEvtInternal.unregister();
+ is(visScrEvtInternal.count, 0, "visual viewport didn't scroll");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ #filler {
+ height: 3000px;
+ background-image: linear-gradient(red, blue, green);
+ }
+ #overlay {
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ left: 0;
+ top: 0;
+ touch-action: none;
+ }
+ #boxOnTop {
+ position: fixed;
+ background-color: coral;
+ width: 20vw;
+ height: 20vh;
+ left: 40%;
+ top: 40%;
+ }
+ </style>
+</head>
+<body>
+ <div id="filler"></div>
+ <div id="overlay">
+ <div id="boxOnTop">Touch here and drag up</div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1509575.html b/gfx/layers/apz/test/mochitest/helper_bug1509575.html
new file mode 100644
index 0000000000..4c85d1db42
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1509575.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1509575
+-->
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Test for Bug 1509575</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+ <div id="expand" style="background-color: paleturquoise ;">
+ Now you're scrolled, now you're not?
+ </div>
+ <script type="application/javascript">
+
+async function test() {
+ let transformEndPromise = promiseTransformEnd();
+ await synthesizeNativeTouchDrag(document.body, 10, 100, -100, 0);
+ dump("Finished native drag, waiting for transform-end observer...\n");
+
+ // Wait for the APZ:TransformEnd to be fired after touch events are processed.
+ await transformEndPromise;
+
+ // Flush state.
+ await promiseApzFlushedRepaints();
+
+ is(window.scrollX, 0, "layout viewport didn't scroll");
+ let visualX = window.visualViewport.pageLeft;
+ ok(visualX > 0, "visual viewport did scroll");
+
+ let topWinUtils;
+ const isE10s = SpecialPowers.Services.appinfo.browserTabsRemoteAutostart;
+ // We need to reset the first paint flag on the root document in the process
+ // this test is loaded in.
+ if (!isE10s) {
+ // For non-e10s, such as in Fennec, this means we need the *chrome* window
+ // as the topmost entitiy in this process.
+ topWinUtils = SpecialPowers.getDOMWindowUtils(
+ SpecialPowers._getTopChromeWindow(window));
+ } else {
+ topWinUtils = SpecialPowers.getDOMWindowUtils(window);
+ }
+ let afterPaintPromise = promiseAfterPaint();
+ ok(topWinUtils.isFirstPaint === false, "first paint not set");
+ topWinUtils.isFirstPaint = true;
+ // do something that forces a paint *and* an APZ update.
+ document.getElementById("expand").style.width = "6000px";
+
+ // Wait for the event listener to fire.
+ await afterPaintPromise;
+ ok(true, "MozAfterPaint fired");
+
+ // Flush state just to be sure.
+ await promiseApzFlushedRepaints();
+
+ is(window.visualViewport.pageLeft, visualX, "visual viewport remains unchanged");
+}
+
+SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1519339_hidden_smoothscroll.html b/gfx/layers/apz/test/mochitest/helper_bug1519339_hidden_smoothscroll.html
new file mode 100644
index 0000000000..225182f749
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1519339_hidden_smoothscroll.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for bug 1519339</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ /* To exercise this bug, the page needs to be overflow:hidden in
+ * only one direction, and have actual room to scroll in the other.
+ * Otherwise we wouldn't try to hand the scroll off to APZ even
+ * before the fix.
+ */
+ html {
+ overflow-y: hidden;
+ }
+ div {
+ width: 200vw;
+ height: 10000px;
+ background-color: lightblue;
+ }
+ </style>
+</head>
+<body>
+ <div></div>
+ <script>
+ async function test() {
+ info("Testing scrollTo() in overflow:hidden direction");
+ let scrollEndPromise = promiseScrollend();
+ window.scrollTo({ top: 2000, behavior: 'smooth' });
+ await scrollEndPromise;
+ await promiseApzFlushedRepaints();
+ is(window.scrollY, 2000,
+ "scrollTo() in overflow:hidden direction scrolled to destination");
+
+ info("Testing scrollBy() in overflow:hidden direction");
+ scrollEndPromise = promiseScrollend();
+ window.scrollBy({ top: 2000, behavior: 'smooth'});
+ await scrollEndPromise;
+ await promiseApzFlushedRepaints();
+ is(window.scrollY, 4000,
+ "scrollBy() in overflow:hidden direction scrolled to destination");
+
+ info("Testing scrollByLines() in overflow:hidden direction");
+ scrollEndPromise = promiseScrollend();
+ window.scrollByLines(5, { behavior: 'smooth' });
+ await scrollEndPromise;
+ await promiseApzFlushedRepaints();
+ // Don't try to predict the exact scroll distance, just check we've
+ // scrolled at all.
+ ok(window.scrollY > 4000,
+ "scrollByLines() in overflow:hidden direction performed scrolling");
+
+ }
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1544966_zoom_on_touch_action_none.html b/gfx/layers/apz/test/mochitest/helper_bug1544966_zoom_on_touch_action_none.html
new file mode 100644
index 0000000000..7adfd5ba0f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1544966_zoom_on_touch_action_none.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for Bug 1544966</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var target = document.getElementById("target");
+
+ var pointersDown = 0;
+ var pointersUp = 0;
+ var pointerMoveCount = 0;
+
+ target.addEventListener("pointerdown", function(e) {
+ dump(`Got pointerdown, pointer id ${e.pointerId}\n`);
+ pointersDown++;
+ });
+ target.addEventListener("pointermove", function(e) {
+ dump(`Got pointermove, pointer id ${e.pointerId}, at ${e.clientX}, ${e.clientY}\n`);
+ pointerMoveCount++;
+ });
+ let pointersUpPromise = new Promise(resolve => {
+ target.addEventListener("pointercancel", function(e) {
+ dump(`Got pointercancel, pointer id ${e.pointerId}\n`);
+ ok(false, "Should not have gotten pointercancel");
+ pointersUp++;
+ if (pointersDown == pointersUp) {
+ // All pointers lifted, let's continue the test
+ resolve();
+ }
+ });
+ target.addEventListener("pointerup", function(e) {
+ dump(`Got pointerup, pointer id ${e.pointerId}\n`);
+ pointersUp++;
+ if (pointersDown == pointersUp) {
+ // All pointers lifted, let's continue the test
+ resolve();
+ }
+ });
+ });
+
+ var zoom_in = [
+ [ { x: 125, y: 175 }, { x: 175, y: 225 } ],
+ [ { x: 120, y: 150 }, { x: 180, y: 250 } ],
+ [ { x: 115, y: 125 }, { x: 185, y: 275 } ],
+ [ { x: 110, y: 100 }, { x: 190, y: 300 } ],
+ [ { x: 105, y: 75 }, { x: 195, y: 325 } ],
+ [ { x: 100, y: 50 }, { x: 200, y: 350 } ],
+ ];
+
+ var touchIds = [0, 1];
+ await synthesizeNativeTouchSequences(document.getElementById("target"), zoom_in, null, touchIds);
+
+ dump("All touch events synthesized, waiting for final pointerup...\n");
+ await pointersUpPromise;
+
+ // Should get at least one pointermove per pointer, even if the events
+ // get coalesced somewhere.
+ is(pointersDown, 2, "Got expected numbers of pointers recorded");
+ ok(pointerMoveCount >= 2, "Got " + pointerMoveCount + " pointermove events");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ body {
+ height: 5000px;
+ }
+ #target {
+ touch-action: none;
+ height: 400px
+ }
+ </style>
+</head>
+<body>
+ <div id="target">
+ Put down two fingers at the same time and do a pinch action.
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1550510.html b/gfx/layers/apz/test/mochitest/helper_bug1550510.html
new file mode 100644
index 0000000000..2f3be5dd2c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1550510.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Dragging the mouse on a scrollbar for a transformed, filtered scrollframe</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="text/javascript">
+
+async function test() {
+ var scrollableDiv = document.getElementById("scrollable");
+ let scrollPromise = new Promise(resolve => {
+ scrollableDiv.addEventListener("scroll", resolve, {once: true});
+ });
+
+ // Scroll down a small amount (10px). The bug in this case is that the
+ // scrollthumb "jumps" most of the way down the scroll track because with
+ // WR enabled the filter and transform display items combine to generate an
+ // incorrect APZC tree, and the mouse position gets untransformed incorrectly.
+ // Given the scrollable height of 2000px and scrollframe height of 400px,
+ // the scrollthumb should be approximately 80px tall, and dragging it 10px
+ // should scroll approximately 50 pixels. If the bug manifests, it will get
+ // dragged an extra ~150px and scroll to approximately 1250px.
+ var dragFinisher = await promiseVerticalScrollbarDrag(scrollableDiv, 10, 10);
+ if (!dragFinisher) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ // the events above might be stuck in APZ input queue for a bit until the
+ // layer is activated, so we wait here until the scroll event listener is
+ // triggered.
+ await scrollPromise;
+
+ await dragFinisher();
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ // In this case we just want to make sure the scroll position moved from 0
+ // which indicates the thumb dragging worked properly.
+ ok(scrollableDiv.scrollTop < 100, "Scrollbar drag resulted in a scroll position of " + scrollableDiv.scrollTop);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div style="position: fixed; left: 100px; top: 100px; width: 400px; height: 600px">
+ <div style="transform: translateY(150px); will-change: transform">
+ <div style="filter: grayscale(80%)">
+ <div id="scrollable" style="height: 400px; overflow-y: auto">
+ <div style="min-height: 2000px">
+ yay text
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1637113_main_thread_hit_test.html b/gfx/layers/apz/test/mochitest/helper_bug1637113_main_thread_hit_test.html
new file mode 100644
index 0000000000..c58c7e40b6
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1637113_main_thread_hit_test.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1637113
+-->
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Test for Bug 1637113</title>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <style>
+ iframe {
+ margin-top: 1000px;
+ }
+ </style>
+</head>
+<body>
+ <iframe id="subframe" srcdoc="<div id='target' style='width:100px;height:100px;'>" width="100px" height="100px"></iframe>
+ <script type="application/javascript">
+
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ // Reproducing the bug requires three ingredients:
+ // 1. A large layout viewport offset.
+ // 2. A large visual viewport offset relative to the layout viewport.
+ // 3. An event that's dispatched in the iframe's document.
+ // We make the first two happen by doing a large visual scroll that will
+ // also drag the layout viewport with it part of the way.
+ let visualScrollPromise = new Promise(resolve => {
+ window.visualViewport.addEventListener("scroll", resolve, { once: true });
+ });
+ utils.scrollToVisual(0, 900, utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_INSTANT);
+ await visualScrollPromise;
+ await promiseApzFlushedRepaints();
+
+ let target = subframe.contentWindow.document.getElementById("target");
+ // To get an event that's dispatched in the iframe's document,
+ // synthesize a native tap. This will synthesize three events:
+ // a mouse-move, a mouse-down, and a mouse-up. The mouse-move
+ // and mouse-down are dispatched in the root content document.
+ // The mouse-down causes the iframe to "capture" the mouse, which
+ // leads the mouse-up to be dispatched in the iframe's document
+ // instead. We listen for the mouse-up.
+ let mouseUpEvent = null;
+ let mouseUpPromise = new Promise(resolve => {
+ target.addEventListener("mouseup", function(e) {
+ mouseUpEvent = e;
+ resolve();
+ });
+ });
+
+ await synthesizeNativeTap(target, 10, 10);
+ await mouseUpPromise;
+
+ is(mouseUpEvent.target, target, "mouseup event targeted the correct element");
+}
+
+SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1637135_narrow_viewport.html b/gfx/layers/apz/test/mochitest/helper_bug1637135_narrow_viewport.html
new file mode 100644
index 0000000000..7b57416c04
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1637135_narrow_viewport.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1637135
+-->
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=400px">
+ <title>Test for Bug 1637135</title>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <style>
+ #target {
+ margin-left: 450px;
+ width: 100px;
+ height: 100px;
+ }
+ </style>
+</head>
+<body>
+ <div id="target">
+ <script type="application/javascript">
+
+async function test() {
+ // Tap the target element, which is located beyond x=400.
+ // The bug occurs when we cannot hit it because the viewport
+ // width of x=400 causes us to be unable to hit elements
+ // beyond that point.
+ let target = document.getElementById("target");
+ let mouseDownEvent = null;
+ let mouseDownPromise = new Promise(resolve => {
+ target.addEventListener("mousedown", function(e) {
+ mouseDownEvent = e;
+ resolve();
+ });
+ });
+
+ await synthesizeNativeTap(target, 10, 10);
+ await mouseDownPromise;
+
+ is(mouseDownEvent.target, target, "mousedown event targeted the correct element");
+}
+
+if (getPlatform() == "android") {
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+} else {
+ // The fix for bug 1637135 is limited to Android, because
+ // it breaks the ability to target scrollbars, so we can
+ // only run this test on Android.
+ ok(true, "This subtest is only run on Android");
+ subtestDone();
+}
+
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1638441_fixed_pos_hit_test.html b/gfx/layers/apz/test/mochitest/helper_bug1638441_fixed_pos_hit_test.html
new file mode 100644
index 0000000000..3665ef5a31
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1638441_fixed_pos_hit_test.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1638441
+-->
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Test for Bug 1638441</title>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <style>
+ #target {
+ position: fixed;
+ bottom: 50px;
+ width: 100px;
+ height: 100px;
+ }
+ </style>
+</head>
+<body>
+ <div id="target">
+ <script type="application/javascript">
+
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ // Do a large visual scroll to scroll the visual viewport to the bottom
+ // of the layout viewport.
+ let visualScrollPromise = new Promise(resolve => {
+ window.visualViewport.addEventListener("scroll", resolve, { once: true });
+ });
+ utils.scrollToVisual(0, 900, utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_INSTANT);
+ await visualScrollPromise;
+ await promiseApzFlushedRepaints();
+
+ // Tap the position-fixed element which is near the bottom of the
+ // layout viewport (and therefore visible now that the visual
+ // viewport is scrolled to the bottom of the layout viewport).
+ // The intention is to test that the visual-to-layout transform
+ // is applied correctly during the hit test.
+ let target = document.getElementById("target");
+ let mouseDownEvent = null;
+ let mouseDownPromise = new Promise(resolve => {
+ target.addEventListener("mousedown", function(e) {
+ mouseDownEvent = e;
+ resolve();
+ });
+ });
+
+ await synthesizeNativeTap(target, 10, 10);
+ await mouseDownPromise;
+
+ is(mouseDownEvent.target, target, "mousedown event targeted the correct element");
+}
+
+SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1638458_contextmenu.html b/gfx/layers/apz/test/mochitest/helper_bug1638458_contextmenu.html
new file mode 100644
index 0000000000..a5a43f7ca1
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1638458_contextmenu.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1638458
+-->
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Test for Bug 1638458</title>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <style>
+ #target {
+ margin-top: 1000px;
+ width: 100px;
+ height: 100px;
+ }
+ </style>
+</head>
+<body>
+ <div id="target">
+ <script type="application/javascript">
+
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ // Do a large visual scroll to scroll the visual viewport to the bottom
+ // of the layout viewport.
+ let visualScrollPromise = new Promise(resolve => {
+ window.visualViewport.addEventListener("scroll", resolve, { once: true });
+ });
+ utils.scrollToVisual(0, 900, utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_INSTANT);
+ await visualScrollPromise;
+ await promiseApzFlushedRepaints();
+
+ // Simulate a long-tap on the target. We do this by simply synthesizing
+ // a touch-start event; eventually, the long-tap timeout will be triggered
+ // and the "contextmenu" will be fired (on non-Windows platforms).
+ let target = document.getElementById("target");
+ let contextmenuEvent = null;
+ let contextmenuPromise = new Promise(resolve => {
+ window.addEventListener("contextmenu", function(e) {
+ contextmenuEvent = e;
+ // Don't actually open a context menu; it messes up subsequent
+ // tests unless we take additional action to close it.
+ e.preventDefault();
+ resolve();
+ });
+ });
+ await synthesizeNativeTouch(target, 10, 10, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT);
+ await contextmenuPromise;
+
+ // Check that the "contextmenu" event targets the correct element.
+ is(contextmenuEvent.target, target, "contextmenu event targeted the correct element");
+
+ // Clean up by firing a touch-end to clear the APZ gesture state.
+ await new Promise(resolve => {
+ synthesizeNativeTouch(target, 10, 10, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE,
+ resolve);
+ });
+}
+
+if (getPlatform() == "windows") {
+ // On Windows, contextmenu events work differently (e.g. they are fired
+ // after the touch-end) which makes them more involved to synthesize.
+ // We don't gain much value in terms of extra test coverage from running
+ // this subtest on windows, so just skip it.
+ ok(true, "Skipping this subtest on windows");
+ subtestDone();
+} else {
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+}
+
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1648491_no_pointercancel_with_dtc.html b/gfx/layers/apz/test/mochitest/helper_bug1648491_no_pointercancel_with_dtc.html
new file mode 100644
index 0000000000..3d8fdcf76e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1648491_no_pointercancel_with_dtc.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for Bug 1648491</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var body = document.body;
+
+ var pointersDown = 0;
+ var pointersUp = 0;
+ var pointerMoveCount = 0;
+
+ body.addEventListener("pointerdown", function(e) {
+ dump(`Got pointerdown, pointer id ${e.pointerId}\n`);
+ pointersDown++;
+ });
+ body.addEventListener("pointermove", function(e) {
+ dump(`Got pointermove, pointer id ${e.pointerId}, at ${e.clientX}, ${e.clientY}\n`);
+ pointerMoveCount++;
+ });
+ let pointersUpPromise = new Promise(resolve => {
+ body.addEventListener("pointercancel", function(e) {
+ dump(`Got pointercancel, pointer id ${e.pointerId}\n`);
+ ok(false, "Should not have gotten pointercancel");
+ pointersUp++;
+ if (pointersDown == pointersUp) {
+ // All pointers lifted, let's continue the test
+ resolve();
+ }
+ });
+ body.addEventListener("pointerup", function(e) {
+ dump(`Got pointerup, pointer id ${e.pointerId}\n`);
+ pointersUp++;
+ if (pointersDown == pointersUp) {
+ // All pointers lifted, let's continue the test
+ resolve();
+ }
+ });
+ });
+
+ var zoom_in = [
+ [ { x: 125, y: 175 }, { x: 175, y: 225 } ],
+ [ { x: 120, y: 150 }, { x: 180, y: 250 } ],
+ [ { x: 115, y: 125 }, { x: 185, y: 275 } ],
+ [ { x: 110, y: 100 }, { x: 190, y: 300 } ],
+ [ { x: 105, y: 75 }, { x: 195, y: 325 } ],
+ [ { x: 100, y: 50 }, { x: 200, y: 350 } ],
+ ];
+
+ var touchIds = [0, 1];
+ await synthesizeNativeTouchSequences(document.getElementById("target"), zoom_in, null, touchIds);
+
+ dump("All touch events synthesized, waiting for final pointerup...\n");
+ await pointersUpPromise;
+
+ // Should get at least one pointermove per pointer, even if the events
+ // get coalesced somewhere.
+ is(pointersDown, 2, "Got expected numbers of pointers recorded");
+ ok(pointerMoveCount >= 2, "Got " + pointerMoveCount + " pointermove events");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ body {
+ height: 5000px;
+ }
+ #target {
+ touch-action: pan-x pan-y;
+ height: 400px;
+ }
+ </style>
+</head>
+<body>
+ <div id="target" onwheel="return false;">
+ A two-finger pinch action here should send pointer events to content.
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1662800.html b/gfx/layers/apz/test/mochitest/helper_bug1662800.html
new file mode 100644
index 0000000000..eb804a40f6
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1662800.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Dragging the mouse on a scrollbar for a scrollframe inside nested transforms with a scale component</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="text/javascript">
+
+async function test() {
+ var scrollableDiv = document.getElementById("scrollable");
+ let scrollPromise = new Promise(resolve => {
+ scrollableDiv.addEventListener("scroll", resolve, {once: true});
+ });
+
+ // Scroll down a small amount (7px). The bug in this case is that the
+ // scrollthumb "jumps" most of the way down the scroll track because with
+ // the bug, the code was incorrectly combining the transforms.
+ // Given the scrollable height of 0.7*2000px and scrollframe height of 0.7*400px,
+ // the scrollthumb should be approximately 0.7*80px = 56px tall. Dragging it 7px
+ // should scroll approximately 50 (unscaled) pixels. If the bug manifests, it will get
+ // dragged by a lot more and scroll to approximately 1300px.
+ var dragFinisher = await promiseVerticalScrollbarDrag(scrollableDiv, 7, 7, 0.7);
+ if (!dragFinisher) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ // the events above might be stuck in APZ input queue for a bit until the
+ // layer is activated, so we wait here until the scroll event listener is
+ // triggered.
+ await scrollPromise;
+
+ await dragFinisher();
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ // Ensure the scroll position ended up roughly where we wanted it (around
+ // 50px, but definitely less than 1300px).
+ ok(scrollableDiv.scrollTop < 100, "Scrollbar drag resulted in a scroll position of " + scrollableDiv.scrollTop);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div style="width: 500px; height: 300px; transform: translate(500px, 500px) scale(0.7)">
+ <div id="scrollable" style="transform: translate(-600px, -600px); overflow: scroll">
+ <div style="width: 600px; height: 400px">
+ <div style="width: 600px; height: 2000px; background-image: linear-gradient(red,blue)"></div>
+ </div>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1663731_no_pointercancel_on_second_touchstart.html b/gfx/layers/apz/test/mochitest/helper_bug1663731_no_pointercancel_on_second_touchstart.html
new file mode 100644
index 0000000000..e0690c12c6
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1663731_no_pointercancel_on_second_touchstart.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for Bug 1663731</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var body = document.body;
+
+ var cancelledTouchMove = false;
+
+ // Event listeners just for logging/debugging purposes
+ body.addEventListener("pointerdown", function(e) {
+ dump(`Got pointerdown, pointer id ${e.pointerId}\n`);
+ });
+ body.addEventListener("touchstart", function(e) {
+ dump(`Got touchstart with ${e.touches.length} touches\n`);
+ }, {passive: true});
+
+
+ // Event listeners relevant to the test. We want to make sure that even
+ // though APZ can zoom the page, it does NOT dispatch pointercancel events in
+ // the scenario where the page calls preventDefault() on the first touchmove
+ // with two touch points. In other words, if the page chooses to disable
+ // browser pinch-zooming by preventDefault()'ing the first touchmove for
+ // the second touch point, then the browser should not dispatch pointercancel
+ // at all, but keep sending the pointerevents to the content. This is
+ // similar to what the browser does when zooming is disallowed by
+ // touch-action:none, for example.
+ body.addEventListener("pointercancel", function(e) {
+ dump(`Got pointercancel, pointer id ${e.pointerId}\n`);
+ ok(false, "Should not get any pointercancel events");
+ });
+ body.addEventListener("touchmove", function(e) {
+ dump(`Got touchmove with ${e.touches.length} touches\n`);
+ if (e.touches.length > 1) {
+ dump(`Preventing...\n`);
+ e.preventDefault();
+ cancelledTouchMove = true;
+ }
+ }, {passive: false});
+
+ let touchEndPromise = new Promise(resolve => {
+ // This listener is just to catch the end of the touch sequence so we can
+ // end the test at the right time.
+ body.addEventListener("touchend", function(e) {
+ dump(`Got touchend with ${e.touches.length} touches\n`);
+ if (!e.touches.length) {
+ resolve();
+ }
+ });
+ });
+
+ // We can't await this call, because this pinch action doesn't generate a
+ // APZ:TransformEnd. Instead we await the touchend.
+ pinchZoomOutWithTouchAtCenter();
+ await touchEndPromise;
+
+ ok(cancelledTouchMove, "Checking that we definitely cancelled the touchmove");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ body {
+ height: 5000px;
+ }
+ </style>
+</head>
+<body>
+ A two-finger pinch action here should send pointer events to content and not do browser zooming.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1669625.html b/gfx/layers/apz/test/mochitest/helper_bug1669625.html
new file mode 100644
index 0000000000..95d2a4bc2c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1669625.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Scrolling doesn't cause extra SchedulePaint calls</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ if (SpecialPowers.getBoolPref("apz.force_disable_desktop_zooming_scrollbars") ||
+ getPlatform() == "android") {
+ return;
+ }
+
+ while (window.scrollY == 0) {
+ // the scrollframe is not yet marked as APZ-scrollable. Mark it so before
+ // continuing.
+ window.scrollTo(0, 1000);
+ await promiseApzFlushedRepaints();
+ }
+
+ window.synthesizeKey("KEY_ArrowDown");
+ // This is really tricky. We want to check that during the main part of
+ // scrolling after this we don't get any SchedulePaint calls. The way that we
+ // test that is to use checkAndClearDisplayListState on the document element
+ // to make sure it didn't have display list building ran for it. The
+ // synthesizeKey calls above will end up in ScrollFrameHelper::ScrollBy,
+ // which calls SchedulePaint in order to pass the scroll to the compositor to
+ // perform. That SchedulePaint will result in display list building for the
+ // document element, and that's okay, but we want to call
+ // checkAndClearDisplayListState (to clear the display list building state)
+ // right after that display list building, so that we can observe if any
+ // display list building happens after it. That way that we do that is a rAF,
+ // which runs immediately before painting, and then a setTimeout from the
+ // rAF, which should run almost immediately after painting. Then we wait for
+ // a scroll event, this scroll event is triggered by the compositor updating
+ // the main thread scroll position. And here is where we finally get to what
+ // we want to actually test. The original bug came about when the main
+ // thread, while processing the repaint request from the compositor, called
+ // SchedulePaint, and hence caused display list building. So we want to check
+ // that the refresh driver tick after the scroll event does not do any
+ // display list building. We again use a setTimeout from a rAF to run right
+ // after the paint and check that there was no display list building.
+ await new Promise(resolve => {
+ window.requestAnimationFrame(() => {
+ setTimeout(checkNoDisplayListRebuild, 0, resolve);
+ });
+ });
+}
+
+function checkNoDisplayListRebuild(resolve) {
+ var utils = window.opener.SpecialPowers.getDOMWindowUtils(window);
+ var elem = document.documentElement;
+ utils.checkAndClearDisplayListState(elem);
+ window.addEventListener("scroll", function () {
+ window.requestAnimationFrame(() => {
+ setTimeout(function() {
+ is(utils.checkAndClearDisplayListState(elem), false, "Document element didn't get display list");
+ resolve();
+ },0);
+ });
+ }, {once: true});
+}
+
+waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body style="height: 5000px">
+ <div style="height: 50px">spacer</div>
+ <button id="b" style="width: 10px; height: 10px"></button>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1674935.html b/gfx/layers/apz/test/mochitest/helper_bug1674935.html
new file mode 100644
index 0000000000..f5efa16d5f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1674935.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests that keyboard arrow keys scroll a very specific page</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+function start() {
+ document.documentElement.addEventListener("keyup", function() { console.log("keyup"); });
+ document.documentElement.addEventListener("keydown", function() { console.log("keydown"); });
+ document.documentElement.addEventListener("keypress", function() { console.log("keypress"); });
+}
+ </script>
+ <style>
+ .z1asCe {
+ display: inline-block;
+ width: 24px
+ }
+ .kno-ecr-pt {
+ position: relative;
+ }
+ .rsir2d {
+ opacity: 0.54
+ }
+ .bErdLd {
+ position: fixed;
+ right: 0;
+ bottom: 0;
+ top: 0;
+ left: 0;
+ }
+ </style>
+</head>
+<body onload="start();">
+ <div style="height: 4000px;">
+ <div class="rsir2d">
+ <span class=" z1asCe ">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+ <path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z"></path>
+ </svg>
+ </span>
+ <div class="bErdLd">
+ </div>
+ </div>
+ <h2 class="kno-ecr-pt"><span>Firefox</span></h2>
+ </div>
+
+ <script type="application/javascript">
+
+ function waitForScrollEvent(target) {
+ return new Promise(resolve => {
+ target.addEventListener("scroll", resolve, { once: true });
+ });
+ }
+
+ async function test() {
+ is(window.scrollX, 0, "shouldn't have scrolled (1)");
+ is(window.scrollY, 0, "shouldn't have scrolled (2)");
+
+ let waitForScroll = waitForScrollEvent(window);
+
+ window.synthesizeKey("KEY_ArrowDown");
+
+ await waitForScroll;
+
+ is(window.scrollX, 0, "shouldn't have scrolled (3)");
+ isnot(window.scrollY, 0, "should have scrolled (4)");
+ }
+
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1682170_pointercancel_on_touchaction_pinchzoom.html b/gfx/layers/apz/test/mochitest/helper_bug1682170_pointercancel_on_touchaction_pinchzoom.html
new file mode 100644
index 0000000000..b9c31dfe89
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1682170_pointercancel_on_touchaction_pinchzoom.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var body = document.body;
+
+ // Event listeners just for logging/debugging purposes
+ body.addEventListener("pointerdown", function(e) {
+ dump(`Got pointerdown, pointer id ${e.pointerId}\n`);
+ });
+ body.addEventListener("touchstart", function(e) {
+ dump(`Got touchstart with ${e.touches.length} touches\n`);
+ }, {passive: true});
+
+
+ // Event listeners relevant to the test. We want to make sure that a
+ // pointercancel event is dispatched to web content, so we listen for that.
+ // Also we want to ensure the main thread TouchActionHelper code is run and
+ // used, so we add a non-passive touchmove listener that ensures the body has
+ // a d-t-c region.
+ var gotPointerCancel = false;
+ body.addEventListener("pointercancel", function(e) {
+ dump(`Got pointercancel, pointer id ${e.pointerId}\n`);
+ gotPointerCancel = true;
+ });
+ body.addEventListener("touchmove", function(e) {
+ dump(`Got touchmove with ${e.touches.length} touches\n`);
+ }, {passive: false});
+
+ let touchEndPromise = new Promise(resolve => {
+ // This listener is just to catch the end of the touch sequence so we can
+ // end the test at the right time.
+ body.addEventListener("touchend", function(e) {
+ dump(`Got touchend with ${e.touches.length} touches\n`);
+ if (!e.touches.length) {
+ resolve();
+ }
+ });
+ });
+
+ // We can't await this call, because this pinch action doesn't generate a
+ // APZ:TransformEnd. Instead we await the touchend.
+ pinchZoomOutWithTouchAtCenter();
+ await touchEndPromise;
+
+ ok(gotPointerCancel, "Checking that we definitely cancelled the pointerevents");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ body {
+ height: 5000px;
+ touch-action: pinch-zoom;
+ }
+ </style>
+</head>
+<body>
+ A two-finger pinch action here should trigger browser zoom and trigger a pointercancel to content.
+ Note that the code does a zoom-out and the page is already at min zoom, so
+ the zoom doesn't produce any visual effect. But the DOM events should be the
+ same either way.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1695598.html b/gfx/layers/apz/test/mochitest/helper_bug1695598.html
new file mode 100644
index 0000000000..fb6102e33d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1695598.html
@@ -0,0 +1,123 @@
+<html>
+<head>
+ <title>Test for bug 1695598</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="text/javascript">
+ let scrollEvents = 100;
+ let i = 0;
+
+ // Scroll points
+ let numscrolls = 0;
+ let last_numscrolls = 0;
+ let start_scrolly = 0;
+ let missed_events = 0;
+ let missed_scroll_updates = 0;
+
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ let timeStamp = document.timeline.currentTime;
+ async function sendScrollEvent(aRafTimestamp) {
+ if (i < scrollEvents) {
+ if (timeStamp == document.timeline.currentTime) {
+ // If we are in a rAF callback at the same time stamp we've already
+ // sent a key event, skip it, otherwise we will not get the
+ // corresponding scroll event for the key event since it will be
+ // coalesced into a single scroll event.
+ window.requestAnimationFrame(sendScrollEvent);
+ return;
+ }
+ timeStamp = document.timeline.currentTime;
+ // Sent a key event in a setTimeout callback so that it will be
+ // processed in between nsRefreshDriver::Tick calls, thus the async
+ // scroll triggered by the key event is going to be processed on the
+ // main-thread before RepaintRequests are processed inside
+ // nsRefreshDriver::Tick, which results there's an async scroll when
+ // RepaintRequests are processed.
+ setTimeout(async () => {
+ window.synthesizeKey("KEY_ArrowDown");
+ i++;
+ // "apz-repaints-flush" is notified in an early runner of
+ // nsRefreshDriver, which means it will be delivered inside
+ // a nsRefreshDriver::Tick call before rAF callbacks.
+ await promiseOnlyApzControllerFlushedWithoutSetTimeout();
+
+ // Wait an animationiteration event since animation events are fired
+ // before rAF callbacks and after scroll events so that it's a good
+ // place to tell whether the expected scroll event got fired or not.
+ await promiseOneEvent(document.getElementById("animation"),
+ "animationiteration", null);
+ if (numscrolls == last_numscrolls) {
+ missed_events++;
+ }
+ if (window.scrollY <= start_scrolly) {
+ missed_scroll_updates++;
+ }
+ last_numscrolls = numscrolls;
+ start_scrolly = window.scrollY;
+ window.requestAnimationFrame(sendScrollEvent);
+ }, 0);
+ } else {
+ // There's a race condition even if we got an "apz-repaints-flush"
+ // notification but any scroll event isn't fired and scroll position
+ // isn't updated since the notification was corresponding to a layers
+ // update triggered by the key event above, which means there was no
+ // repaint request corresponding to APZ animation sample in the time
+ // frame. We allow the case here in the half of key events.
+ ok(missed_events < scrollEvents / 2, `missed event firing ${missed_events} times`);
+ ok(missed_scroll_updates < scrollEvents / 2, `missed scroll update ${missed_scroll_updates} times`);
+ endTest();
+ }
+ }
+
+ async function endTest() {
+ document.removeEventListener("scroll", gotScroll);
+ subtestDone();
+ }
+
+ function gotScroll() {
+ numscrolls++;
+ }
+
+ function startTest() {
+ document.addEventListener("scroll", gotScroll);
+ window.requestAnimationFrame(sendScrollEvent);
+ }
+
+ if (!isApzEnabled()) {
+ ok(true, "APZ not enabled, skipping test");
+ subtestDone();
+ }
+
+ waitUntilApzStable()
+ .then(forceLayerTreeToCompositor)
+ .then(startTest);
+ </script>
+ <style>
+ #content {
+ height: 10000vh;
+ background: repeating-linear-gradient(#EEE, #EEE 100px, #DDD 100px, #DDD 200px);
+ }
+ @keyframes anim {
+ from { opacity: 0; }; /* To avoid churning this scroll test */
+ to { opacity: 0; };
+ }
+ #animation {
+ position: absolute;
+ width: 100px;
+ height: 100px;
+ visibility: hidden; /* for skipping restyles on the main-thread */
+ animation-name: anim;
+ animation-iteration-count: infinite;
+ animation-duration: 1ms; /* to get an animationiteration event in each tick */
+ }
+ </style>
+</head>
+<body>
+ <div id="animation"></div>
+ <div id="content">
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1714934_mouseevent_buttons.html b/gfx/layers/apz/test/mochitest/helper_bug1714934_mouseevent_buttons.html
new file mode 100644
index 0000000000..35cea7de4f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1714934_mouseevent_buttons.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1714934
+-->
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Test for Bug 1714934</title>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <style>
+ iframe {
+ margin-top: 1000px;
+ }
+ </style>
+</head>
+<body>
+ <script type="application/javascript">
+
+async function test() {
+ let mousedownPromise = new Promise(resolve => {
+ document.addEventListener("mousedown", e => {
+ is(e.buttons, 1, "Mousedown event reports 1 button pressed")
+ resolve();
+ }, { once: true })
+ });
+ await synthesizeNativeTap(window, 100, 100);
+ await mousedownPromise;
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1719330.html b/gfx/layers/apz/test/mochitest/helper_bug1719330.html
new file mode 100644
index 0000000000..c99b6e1012
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1719330.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests that the arrow down key does not scroll by more than 1 element</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ .time-column {
+ width: 68px;
+ height: 28px;
+
+ overflow-y: scroll;
+ scroll-snap-type: y mandatory;
+
+ border-radius: 4px;
+ border: 1px solid red;
+ }
+
+ .time-item {
+ scroll-snap-align: center;
+ height: 100%;
+ }
+ </style>
+</head>
+<body>
+ <div class="time-column"></div>
+
+ <script type="application/javascript">
+
+ function waitForScrollEvent(target) {
+ return new Promise(resolve => {
+ target.addEventListener("scroll", resolve, { once: true });
+ });
+ }
+
+ async function test() {
+ const timeCol = document.querySelector('.time-column');
+
+ for (let i = 0; i < 60; i++) {
+ let item = document.createElement('div');
+ item.classList.add('time-item');
+ item.textContent = i;
+ timeCol.appendChild(item);
+ }
+
+ is(timeCol.scrollTop, 0, "should begin with no scroll (1)");
+
+ let waitForScroll = waitForScrollEvent(timeCol);
+
+ timeCol.focus();
+ window.synthesizeKey("KEY_ArrowDown");
+
+ await waitForScroll;
+
+ ok(timeCol.scrollTop > 0, "should have scrolled (2)");
+ ok(timeCol.scrollTop < 30, "should have not scrolled too far (3)");
+ }
+
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1719855.html b/gfx/layers/apz/test/mochitest/helper_bug1719855.html
new file mode 100644
index 0000000000..d8cf566774
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1719855.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>preventDefault() in touchmove prevents scrolling even after a long tap event</title>
+ <meta name="viewport" content="width=device-width,initial-scale=1">
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+</head>
+<body>
+ <!-- An anchor to open a context menu -->
+ <a href="about:blank" style="position:absolute; top: 100px; left: 100px;">about:blank</a>
+ <!-- make the root scroll container scrollable -->
+ <div style="height: 200vh;"></div>
+</body>
+<script type="application/javascript">
+
+const searchParams = new URLSearchParams(location.search);
+
+const isAndroid = getPlatform() == "android";
+
+async function test() {
+ // Setup a touchmove event listener where we do preventDefault() to prevent
+ // scrolling.
+ let touchmoveCount = 0;
+ document.scrollingElement.addEventListener("touchmove", e => {
+ info("Got a touchmove");
+ touchmoveCount++;
+ e.preventDefault();
+ }, { passive: false });
+
+ // Setup touchstart/touchend event listeners just for debugging purpose.
+ document.scrollingElement.addEventListener("touchstart", () => {
+ info("Got a touchstart");
+ }, { passive: false });
+ document.scrollingElement.addEventListener("touchend", () => {
+ info("Got a touchend");
+ }, { passive: false });
+
+ window.addEventListener("scroll", () => {
+ ok(false, "The content should never be scrolled");
+ });
+
+ let contextmenuPromise = promiseOneEvent(window, "contextmenu", e => {
+ if (searchParams.get("prevent") == "contextmenu") {
+ e.preventDefault();
+ }
+ return true;
+ });
+
+ // Ensure that the setup-ed information has reached to APZ.
+ await promiseApzFlushedRepaints();
+
+ // Start a touch on the anchor.
+ await synthesizeNativeTouch(window, 100, 100, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT);
+
+ // And wait for a contextmenu event (i.e. a long-tap event)
+ await contextmenuPromise;
+
+ // Extend apz.content_response_timeout to avoid timeout on waiting the content
+ // response.
+ await SpecialPowers.pushPrefEnv({ set: [["apz.content_response_timeout", 40000]] });
+
+ // Make sure the touch start does nothing.
+ is(window.scrollY, 0, "The original scroll position is zero");
+
+ // Try to scroll down by touch moving.
+ for (let i = 1; i < 50; i++) {
+ synthesizeNativeTouch(window, 100, 100 - i, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT);
+ }
+ await synthesizeNativeTouch(window, 100, 50, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE);
+
+ await waitToClearOutAnyPotentialScrolls();
+
+ if (searchParams.get("prevent") == "contextmenu") {
+ ok(touchmoveCount > 0, "There should be at least one touch-move event");
+ } else {
+ is(touchmoveCount, 0, "There should be no touch-move event when the context menu opened");
+ }
+ is(window.scrollY, 0, "The scroll position should stay the original position");
+
+ if (searchParams.get("prevent") != "contextmenu") {
+ // If we've opened the context menu, close it.
+ await closeContextMenu();
+ }
+}
+
+if (getPlatform() == "windows") {
+ // On Windows every context menu on touch screens opens __after__ lifting the
+ // finger.
+ ok(true, "Test doesn't need to run on Windows");
+ subtestDone();
+} else {
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+}
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1719855_pointercancel_on_touchmove_after_contextmenu_prevented.html b/gfx/layers/apz/test/mochitest/helper_bug1719855_pointercancel_on_touchmove_after_contextmenu_prevented.html
new file mode 100644
index 0000000000..af4118eeb1
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1719855_pointercancel_on_touchmove_after_contextmenu_prevented.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+ <!-- make the root scroll container scrollable -->
+ <div style="height: 200vh;"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ let pointercancelCount = 0;
+ let pointercancelPromise = new Promise(resolve => {
+ window.addEventListener("pointercancel", () => {
+ pointercancelCount++;
+ resolve();
+ });
+ });
+
+ let scrollendCount = 0;
+ let scrollendPromise = new Promise(resolve => {
+ window.addEventListener("scrollend", () => {
+ scrollendCount++;
+ resolve();
+ });
+ });
+
+ let contextmenuPromise = promiseOneEvent(window, "contextmenu", e => {
+ // Do preventDefault() to prevent opening a context menu, it will suppress
+ // a pointercancel event triggered by the context menu.
+ e.preventDefault();
+ return true;
+ });
+
+ // Ensure that above setup-ed information has reached to APZ.
+ await promiseApzFlushedRepaints();
+
+ // Start a touch.
+ await synthesizeNativeTouch(window, 100, 100, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT);
+
+ // And wait for a contextmenu event (i.e. a long-tap event)
+ await contextmenuPromise;
+
+ // Try to scroll down by touch moving.
+ for (let i = 1; i < 50; i++) {
+ synthesizeNativeTouch(window, 100, 100 - i, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT);
+ }
+ await synthesizeNativeTouch(window, 100, 50, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE);
+
+ // Now the content should have been scrolled and there should be a
+ // pointercancel event.
+ await Promise.all([ pointercancelPromise, scrollendPromise ]);
+ is(pointercancelCount, 1, "There should be only one pointercancel event");
+ is(scrollendCount, 1, "There should be only one scrollend event");
+}
+
+if (getPlatform() == "windows") {
+ // On Windows every context menu on touch screens opens __after__ lifting the
+ // finger.
+ ok(true, "Test doesn't need to run on Windows");
+ subtestDone();
+} else {
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+}
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1724759.html b/gfx/layers/apz/test/mochitest/helper_bug1724759.html
new file mode 100644
index 0000000000..a012bb0fc0
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1724759.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html>
+<meta name="viewport" content="width=device-width; initial-scale=1.0">
+<title>Tests that :active state is cleared after a longpress event</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<style>
+ #button {
+ width: 100px;
+ height: 100px;
+ }
+</style>
+<button id="button">Button</button>
+<script>
+async function test() {
+ const contextmenuPromise = promiseOneEvent(button, "contextmenu", e => {
+ e.preventDefault();
+ return true;
+ });
+ await synthesizeNativeTouch(button, 10, 10, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT);
+
+ // In JS there's no way to ensure `APZStateChange::eStartTouch` notification
+ // has been processed. So we wait for `:active` state change here.
+ await SimpleTest.promiseWaitForCondition(
+ () => button.matches(":active"),
+ "Waiting for :active state change");
+
+ ok(button.matches(":active"), "should be active");
+
+ await contextmenuPromise;
+
+ await synthesizeNativeTouch(button, 10, 10, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE);
+
+ // Same as above. We need to wait for not `:active` state here.
+ await SimpleTest.promiseWaitForCondition(
+ () => !button.matches(":active"),
+ "Waiting for :active state change");
+
+ ok(!button.matches(":active"), "should not be active");
+}
+
+if (getPlatform() == "windows") {
+ // On Windows every context menu on touch screens opens __after__ lifting the
+ // finger.
+ ok(true, "Test doesn't need to run on Windows");
+ subtestDone();
+} else {
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+}
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1756529.html b/gfx/layers/apz/test/mochitest/helper_bug1756529.html
new file mode 100644
index 0000000000..e1767f4f57
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1756529.html
@@ -0,0 +1,226 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1756529
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Page scrolling bug test, helper page</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+ // --------------------------------------------------------------------
+ // Page scrolling not smooth test
+ //
+ // This test checks that a page scroll respects general_smoothScroll_pages preference.
+ //
+ // The page contains a <div> that is large enough to make the page
+ // scrollable.
+ //
+ // We trigger the page scroll and then we wait to reach destination
+ // Expecting an instant scroll, we check that the scroll event is called once
+ // --------------------------------------------------------------------
+ const testData = [
+ {scrollOrigin: "page", smooth: false,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.pages", false],
+ ["general.smoothScroll.msdPhysics.enabled", true]]},
+ {scrollOrigin: "page", smooth: false,
+ prefs: [["general.smoothScroll", false], ["general.smoothScroll.pages", true],
+ ["general.smoothScroll.msdPhysics.enabled", true]]},
+ {scrollOrigin: "page", smooth: true,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.pages", true],
+ ["general.smoothScroll.msdPhysics.enabled", true]]},
+ {scrollOrigin: "page", smooth: false,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.pages", false],
+ ["general.smoothScroll.msdPhysics.enabled", false]]},
+ {scrollOrigin: "page", smooth: false,
+ prefs: [["general.smoothScroll", false], ["general.smoothScroll.pages", true],
+ ["general.smoothScroll.msdPhysics.enabled", false]]},
+ {scrollOrigin: "page", smooth: true,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.pages", true],
+ ["general.smoothScroll.msdPhysics.enabled", false]]},
+ // Origin:Line Scrolling tests
+ {scrollOrigin: "line", smooth: false,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.lines", false],
+ ["general.smoothScroll.msdPhysics.enabled", true]]},
+ {scrollOrigin: "line", smooth: false,
+ prefs: [["general.smoothScroll", false], ["general.smoothScroll.lines", true],
+ ["general.smoothScroll.msdPhysics.enabled", true]]},
+ {scrollOrigin: "line", smooth: true,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.lines", true],
+ ["general.smoothScroll.msdPhysics.enabled", true]]},
+ {scrollOrigin: "line", smooth: false,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.lines", false],
+ ["general.smoothScroll.msdPhysics.enabled", false]]},
+ {scrollOrigin: "line", smooth: false,
+ prefs: [["general.smoothScroll", false], ["general.smoothScroll.lines", true],
+ ["general.smoothScroll.msdPhysics.enabled", false]]},
+ {scrollOrigin: "line", smooth: true,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.lines", true],
+ ["general.smoothScroll.msdPhysics.enabled", false]]},
+ // Origin:Other Scrolling test
+ {scrollOrigin: "other", smooth: false,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.other", false],
+ ["general.smoothScroll.msdPhysics.enabled", true]]},
+ {scrollOrigin: "other", smooth: false,
+ prefs: [["general.smoothScroll", false], ["general.smoothScroll.other", true],
+ ["general.smoothScroll.msdPhysics.enabled", true]]},
+ {scrollOrigin: "other", smooth: true,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.other", true],
+ ["general.smoothScroll.msdPhysics.enabled", true]]},
+ {scrollOrigin: "other", smooth: false,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.other", false],
+ ["general.smoothScroll.msdPhysics.enabled", false]]},
+ {scrollOrigin: "other", smooth: false,
+ prefs: [["general.smoothScroll", false], ["general.smoothScroll.other", true],
+ ["general.smoothScroll.msdPhysics.enabled", false]]},
+ {scrollOrigin: "other", smooth: true,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.other", true],
+ ["general.smoothScroll.msdPhysics.enabled", false]]}];
+
+ async function test(data) {
+ /*
+ Test Data:
+ {
+ scrollOrigin: "page"|"other"|"line",
+ smooth: bool,
+ prefs: prefences
+ }
+ */
+ const scrollOrigin = data.scrollOrigin;
+ const smooth = data.smooth;
+ const msdPhysics = data.prefs[2][1];
+ let destination = 0;
+ let key = "";
+ switch (scrollOrigin){
+ case "page":
+ destination = document.scrollingElement.clientHeight * 0.8;
+ key = "KEY_PageDown";
+ break;
+ case "other":
+ destination = 40000; // Div is 50k
+ key = "KEY_End";
+ break;
+ case "line":
+ default:
+ destination = 50; // pref set to scroll by 5 lines
+ // line scroll amounts vary by platform but are
+ // in the 16-19px range
+ key = "KEY_ArrowDown";
+ }
+ await SpecialPowers.pushPrefEnv({ set: data.prefs });
+ info(`Testing Scrolling preferences. [origin: ${scrollOrigin}; smooth: ${smooth}; msdPhysics: ${msdPhysics}; ${destination}]`);
+
+ // Send the synthesized key event, and wait until it arrives in the
+ // content process.
+ let keyPromise = promiseOneEvent(window, "keydown", null);
+ window.synthesizeKey(key);
+ await keyPromise;
+
+ // Take control of the refresh driver. It's important to do this
+ // as soon as the key event has arrived, to ensure that any compositor
+ // animation hasn't started yet. Otherwise, the compositor animation
+ // could start and get in multiple samples (potentially the entire
+ // animation) before the content process gets a chance to observe it,
+ // preventing us from distinguishing smooth scrolls from instant scrolls.
+ let utils = SpecialPowers.DOMWindowUtils;
+ utils.advanceTimeAndRefresh(0);
+
+ // Flush any pending paints. This gives a chance for any handoff of
+ // the scroll to APZ to occur.
+ await promiseAllPaintsDone();
+
+ // Tick the refresh driver manually until we detect that scrolling has
+ // started (scrollY > 0) and then stopped (scroll offset the same in
+ // two subsequent ticks).
+ let startedScroll = false;
+ let stoppedScroll = false;
+ let scrollCount = 0;
+ let prevScrollPos = window.scrollY;
+ while (!stoppedScroll) {
+ // Tick the refresh driver. This triggers a composite, so any
+ // compositor animation will be sampled. (Main thread animations
+ // will also be sampled.)
+ utils.advanceTimeAndRefresh(16);
+
+ // Flush APZ repaints to ensure that scroll offset changes from
+ // a compositor sample reach the content process.
+ await promiseApzFlushedRepaints();
+
+ // Track the number of ticks in which the scroll offset changed.
+ let scrollPos = window.scrollY;
+ if (startedScroll && scrollPos == prevScrollPos) {
+ stoppedScroll = true;
+ break;
+ }
+ if (!startedScroll && scrollPos > 0) {
+ startedScroll = true;
+ }
+ if (startedScroll) {
+ scrollCount++;
+ }
+ prevScrollPos = scrollPos;
+ }
+
+ info(`Scrolled to ${window.scrollY}`);
+
+ // Relinquish control of the refresh driver.
+ utils.restoreNormalRefresh();
+
+ ok(window.scrollY >= destination, `The page did not scroll [origin: ${scrollOrigin}, smooth: ${smooth}]`);
+ if (smooth)
+ ok(scrollCount > 1,
+ `Scrolled only once, but expecting a smooth transtion [origin: ${scrollOrigin}; msdPhysics: ${msdPhysics}]`);
+ else
+ is(scrollCount, 1,
+ `Scrolled more than once, but expecting an instant scroll [origin: ${scrollOrigin}; msdPhysics: ${msdPhysics}]`);
+
+ // Synthesize a touch tap to cancel the animation if it's still in-progress.
+ // (scrollTo() does not do this as of bug 1692708, it adjusts the destination
+ // of the animation by a relative delta).
+ let touchStartPromise = promiseOneEvent(window, "touchstart", null);
+ await synthesizeNativeTap(window, 50, 50);
+ // Wait until the tap is actually processed by APZ.
+ await touchStartPromise;
+ await promiseApzFlushedRepaints();
+
+ // Reset scroll position for next case.
+ window.scrollTo(0, 0);
+ await promiseApzFlushedRepaints();
+ is(0, window.scrollY, `Expected to be scrolled to origin, actually scrolled to ${window.scrollY}`)
+ }
+
+ async function runTests() {
+ for (i = 0; i < testData.length; i++){
+ await test(testData[i]);
+ }
+ }
+
+ if (getPlatform() == "linux" || getPlatform() == "mac") {
+ // FIXME(bug 1760731): On Linux, this test frequently hangs at
+ // "await touchStartPromise", so we skip it.
+ // For Mac, the test is disabled due to a high intermittent failure
+ // rate reported in bug 1771836.
+ ok(true, "Test is disabled on Linux and Mac, skipping");
+ subtestDone();
+ } else {
+ waitUntilApzStable()
+ .then(runTests)
+ .then(subtestDone, subtestFailed);
+ }
+ </script>
+</head>
+<body style="height: 10000px; overflow: scroll;">
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1756529">SmoothScrollPage not honored with MSD physics bug.</a>
+ <!-- Put enough content into the page to make it have a nonzero scroll range -->
+ <div style="height: 50000px;">
+ <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Tellus in metus vulputate eu. Vestibulum morbi blandit cursus risus at ultrices mi tempus imperdiet. Congue quisque egestas diam in. Pretium vulputate sapien nec sagittis aliquam malesuada bibendum arcu. Eleifend mi in nulla posuere. Proin libero nunc consequat interdum varius. Risus pretium quam vulputate dignissim suspendisse in est. Lacus vel facilisis volutpat est. Donec pretium vulputate sapien nec. Feugiat sed lectus vestibulum mattis. Platea dictumst quisque sagittis purus. Vulputate eu scelerisque felis imperdiet proin fermentum leo vel. Enim facilisis gravida neque convallis a cras semper auctor. Placerat orci nulla pellentesque dignissim enim sit.</p>
+ <p>Augue neque gravida in fermentum et sollicitudin ac. Mattis enim ut tellus elementum sagittis vitae et. Malesuada nunc vel risus commodo viverra maecenas accumsan. Viverra nibh cras pulvinar mattis nunc sed. Lectus nulla at volutpat diam ut venenatis tellus in. Non tellus orci ac auctor. Magna etiam tempor orci eu lobortis. Malesuada nunc vel risus commodo viverra maecenas accumsan lacus vel. Sagittis orci a scelerisque purus. Tellus pellentesque eu tincidunt tortor. Vulputate dignissim suspendisse in est ante in. Tristique et egestas quis ipsum suspendisse. Quisque egestas diam in arcu cursus. Massa massa ultricies mi quis hendrerit dolor magna eget. Mattis nunc sed blandit libero volutpat sed. Consectetur purus ut faucibus pulvinar elementum integer enim.</p>
+ <p>Vestibulum lorem sed risus ultricies tristique nulla. Imperdiet nulla malesuada pellentesque elit eget gravida. Feugiat nisl pretium fusce id velit ut tortor pretium. Commodo ullamcorper a lacus vestibulum sed arcu non odio. Id nibh tortor id aliquet lectus proin nibh nisl condimentum. Amet volutpat consequat mauris nunc congue nisi vitae suscipit tellus. Neque ornare aenean euismod elementum. Semper quis lectus nulla at. Massa sed elementum tempus egestas. Praesent elementum facilisis leo vel fringilla est ullamcorper eget nulla. Pellentesque elit eget gravida cum sociis natoque penatibus et. Massa enim nec dui nunc mattis enim. Laoreet suspendisse interdum consectetur libero id faucibus nisl. Fusce ut placerat orci nulla.</p>
+ <p>Vitae tempus quam pellentesque nec nam aliquam. Vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt ornare. Nam libero justo laoreet sit amet. Arcu non sodales neque sodales. Nec ultrices dui sapien eget mi proin sed. Parturient montes nascetur ridiculus mus mauris vitae ultricies. Lacus sed viverra tellus in hac habitasse. Orci phasellus egestas tellus rutrum. Leo a diam sollicitudin tempor id eu nisl. Diam phasellus vestibulum lorem sed risus ultricies tristique nulla. Lectus nulla at volutpat diam ut venenatis tellus in. Cursus metus aliquam eleifend mi in nulla. Et ultrices neque ornare aenean euismod. Sit amet aliquam id diam maecenas ultricies mi. Volutpat diam ut venenatis tellus in metus vulputate eu.</p>
+ <p>Pellentesque elit ullamcorper dignissim cras tincidunt. Morbi tincidunt augue interdum velit euismod. Diam vel quam elementum pulvinar etiam non quam. Eget duis at tellus at urna. Posuere ac ut consequat semper viverra nam libero justo laoreet. Ac turpis egestas maecenas pharetra convallis posuere. Ultrices tincidunt arcu non sodales neque sodales ut etiam sit. In eu mi bibendum neque egestas. Pellentesque sit amet porttitor eget dolor morbi. Ac tortor dignissim convallis aenean et tortor at. Elementum tempus egestas sed sed risus pretium quam. Nisi scelerisque eu ultrices vitae auctor eu augue. Urna duis convallis convallis tellus id interdum velit laoreet id. Auctor eu augue ut lectus arcu bibendum at varius vel.</p>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1756814.html b/gfx/layers/apz/test/mochitest/helper_bug1756814.html
new file mode 100644
index 0000000000..c67531e650
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1756814.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+</head>
+<body>
+ <div style="height: 200vh;"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ const moveDistance = 10;
+
+ // Start dragging the vertical scrollbar thumb.
+ const startPoint = await scrollbarDragStart(window, 1);
+ await promiseNativeMouseEventWithAPZ({
+ target: window,
+ offsetX: startPoint.x,
+ offsetY: startPoint.y,
+ type: "mousemove",
+ });
+ await promiseNativeMouseEventWithAPZ({
+ target: window,
+ offsetX: startPoint.x,
+ offsetY: startPoint.y,
+ type: "mousedown",
+ });
+
+ // Move the thumb and wait for a scroll event triggered by the movement.
+ let scrollEventPromise = waitForScrollEvent(window);
+ await promiseNativeMouseEventWithAPZ({
+ target: window,
+ offsetX: startPoint.x,
+ offsetY: startPoint.y + moveDistance,
+ type: "mousemove",
+ });
+ await scrollEventPromise;
+
+ let scrollPosition = window.scrollY;
+
+ // Append an element to the scroll container to expand the scroll range.
+ const content = document.createElement("div");
+ content.style.height = "200vh";
+ document.body.appendChild(content);
+
+ // flush the above change.
+ document.documentElement.getBoundingClientRect();
+
+ // Make sure the change has been reflected into APZ.
+ await promiseApzFlushedRepaints();
+
+ // Move the thumb again.
+ scrollEventPromise = waitForScrollEvent(window);
+ await promiseNativeMouseEventWithAPZ({
+ target: window,
+ offsetX: startPoint.x,
+ offsetY: startPoint.y + moveDistance * 2,
+ type: "mousemove",
+ });
+
+ await scrollEventPromise;
+ ok(window.scrollY <= scrollPosition * 2,
+ `The scroll position ${window.scrollY} should be less than ${scrollPosition*2}`);
+
+ await promiseNativeMouseEventWithAPZ({
+ target: window,
+ offsetX: startPoint.x,
+ offsetY: startPoint.y + moveDistance * 2,
+ type: "mouseup",
+ });
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1780701.html b/gfx/layers/apz/test/mochitest/helper_bug1780701.html
new file mode 100644
index 0000000000..f445f9abe6
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1780701.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Test that scroll snap wont't happen on zoomed content</title>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ margin: 0;
+ }
+ html {
+ overflow-y: scroll;
+ scroll-snap-type: y proximity;
+ }
+ .snap {
+ width: 100vw;
+ height: 100vh;
+ background-color: blue;
+ position: absolute;
+ top: 200px;
+ scroll-snap-align: start;
+ }
+ </style>
+</head>
+<body>
+ <div class="snap"></div>
+ <div style="width: 100%; height: 500vh;"></div>
+ <script type="application/javascript">
+ async function test() {
+ let transformEndPromise = promiseTransformEnd();
+
+ // Use scrollToVisual() to scroll visual viewport.
+ SpecialPowers.DOMWindowUtils.scrollToVisual(
+ 100, 400,
+ SpecialPowers.DOMWindowUtils.UPDATE_TYPE_MAIN_THREAD,
+ SpecialPowers.DOMWindowUtils.SCROLL_MODE_SMOOTH);
+
+ // Wait for the end of the scroll.
+ await transformEndPromise;
+ await waitToClearOutAnyPotentialScrolls();
+
+ const pageTop = visualViewport.pageTop;
+ const pageLeft = visualViewport.pageLeft;
+
+ let eventFired = false;
+ window.visualViewport.addEventListener("scroll", () => {
+ eventFired = true;
+ });
+
+ // Trigger a scroll snap, it should nothing.
+ SpecialPowers.wrap(document.documentElement).mozScrollSnap();
+
+ await waitToClearOutAnyPotentialScrolls();
+ ok(!eventFired, "No visual scroll should happen");
+
+ // Sanity checks to see whether the visual viewport hasn't been changed.
+ is(visualViewport.pageTop, pageTop);
+ is(visualViewport.pageLeft, pageLeft);
+ }
+
+ SpecialPowers.DOMWindowUtils.setResolutionAndScaleTo(10.0);
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1783936.html b/gfx/layers/apz/test/mochitest/helper_bug1783936.html
new file mode 100644
index 0000000000..8aec5eafef
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1783936.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Test that scroll snap happens on pan end without fling</title>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ margin: 0;
+ padding: 0;
+ }
+ html {
+ overflow-y: scroll;
+ scroll-snap-type: y mandatory;
+ scroll-behavior: auto;
+ }
+ .snap {
+ width: 100%;
+ height: 100vh;
+ background-color: blue;
+ position: absolute;
+ top: 200px;
+ scroll-snap-align: start;
+ }
+ </style>
+</head>
+<body>
+ <div class="snap"></div>
+ <div style="width: 100%; height: 500vh;"></div>
+ <script type="application/javascript">
+ async function test() {
+ is(window.scrollY, 200, "The initial layout should result snapping to 200px");
+
+ // Start scrolling back by a pan gesture and wait its' scroll end.
+ let transformEndPromise = promiseTransformEnd();
+ await promiseNativeTouchpadPanEventAndWaitForObserver(
+ window,
+ 100,
+ 100,
+ 0, -100,
+ SpecialPowers.DOMWindowUtils.PHASE_BEGIN);
+
+ // Finish the pan gesture.
+ await promiseNativeTouchpadPanEventAndWaitForObserver(
+ window,
+ 100,
+ 100,
+ 0, 0,
+ SpecialPowers.DOMWindowUtils.PHASE_END);
+ await transformEndPromise;
+
+ // Make sure the new scroll positions have reached to the main-thread.
+ await promiseOnlyApzControllerFlushed();
+
+ is(window.scrollY, 200, "The pan-end should result snapping to 200px");
+ }
+
+ // This test is supposed to run on environments where
+ // PanGestureInput.mSimulateMomentum for pan gestures is true, which means
+ // as of now it's only on Linux.
+ if (getPlatform() == "linux") {
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ } else {
+ ok(true, "Skipping test because this test isn't supposed to work on " + getPlatform());
+ subtestDone();
+ }
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug982141.html b/gfx/layers/apz/test/mochitest/helper_bug982141.html
new file mode 100644
index 0000000000..8ffda2dd2f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug982141.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=982141
+-->
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=980, user-scalable=no">
+ <title>Test for Bug 982141, helper page</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+ // In this test we have a simple page with a scrollable <div> which has
+ // enough content to make it scrollable. We test that this <div> got
+ // a displayport.
+
+ function test() {
+ // Get the content- and compositor-side test data from nsIDOMWindowUtils.
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ var contentTestData = utils.getContentAPZTestData();
+ var compositorTestData = utils.getCompositorAPZTestData();
+
+ // Get the sequence number of the last paint on the compositor side.
+ // We do this before converting the APZ test data because the conversion
+ // loses the order of the paints.
+ ok(!!compositorTestData.paints.length,
+ "expected at least one paint in compositor test data");
+ var lastCompositorPaint = compositorTestData.paints[compositorTestData.paints.length - 1];
+ var lastCompositorPaintSeqNo = lastCompositorPaint.sequenceNumber;
+
+ // Convert the test data into a representation that's easier to navigate.
+ contentTestData = convertTestData(contentTestData);
+ compositorTestData = convertTestData(compositorTestData);
+
+ // Reconstruct the APZC tree structure in the last paint.
+ var apzcTree = buildApzcTree(compositorTestData.paints[lastCompositorPaintSeqNo]);
+
+ // The apzc tree for this page should consist of a single child APZC on
+ // the RCD node (the child is for scrollable <div>). Note that in e10s/B2G
+ // cases the RCD will be the root of the tree but on Fennec it will not.
+ var rcd = findRcdNode(apzcTree);
+ ok(rcd != null, "found the RCD node");
+ is(rcd.children.length, 1, "expected a single child APZC");
+ var childScrollId = rcd.children[0].scrollId;
+
+ // We should have content-side data for the same paint.
+ ok(lastCompositorPaintSeqNo in contentTestData.paints,
+ "expected a content paint with sequence number" + lastCompositorPaintSeqNo);
+ var correspondingContentPaint = contentTestData.paints[lastCompositorPaintSeqNo];
+
+ var dp = getPropertyAsRect(correspondingContentPaint, childScrollId, "displayport");
+ var subframe = document.getElementById("subframe");
+ // The clientWidth and clientHeight may be less than 50 if there are scrollbars showing.
+ // In general they will be (50 - <scrollbarwidth>, 50 - <scrollbarheight>).
+ ok(subframe.clientWidth > 0, "Expected a non-zero clientWidth, got: " + subframe.clientWidth);
+ ok(subframe.clientHeight > 0, "Expected a non-zero clientHeight, got: " + subframe.clientHeight);
+ ok(dp.width >= subframe.clientWidth && dp.height >= subframe.clientHeight,
+ "expected a displayport at least as large as the scrollable element, got " + JSON.stringify(dp));
+ }
+
+ waitUntilApzStable()
+ .then(forceLayerTreeToCompositor)
+ .then(test)
+ .then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body style="overflow: hidden;"><!-- This combined with the user-scalable=no ensures the root frame is not scrollable -->
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=982141">Mozilla Bug 982141</a>
+ <!-- A scrollable subframe, with enough content to make it have a nonzero scroll range -->
+ <div id="subframe" style="height: 50px; width: 50px; overflow: scroll">
+ <div style="width: 100px">
+ Wide content so that the vertical scrollbar for the parent div
+ doesn't eat into the 50px width and reduce the width of the
+ displayport.
+ </div>
+ Line 1<br>
+ Line 2<br>
+ Line 3<br>
+ Line 4<br>
+ Line 5<br>
+ Line 6<br>
+ Line 7<br>
+ Line 8<br>
+ Line 9<br>
+ Line 10<br>
+ Line 11<br>
+ Line 12<br>
+ Line 13<br>
+ Line 14<br>
+ Line 15<br>
+ Line 16<br>
+ Line 17<br>
+ Line 18<br>
+ Line 19<br>
+ Line 20<br>
+ Line 21<br>
+ Line 22<br>
+ Line 23<br>
+ Line 24<br>
+ Line 25<br>
+ Line 26<br>
+ Line 27<br>
+ Line 28<br>
+ Line 29<br>
+ Line 30<br>
+ Line 31<br>
+ Line 32<br>
+ Line 33<br>
+ Line 34<br>
+ Line 35<br>
+ Line 36<br>
+ Line 37<br>
+ Line 38<br>
+ Line 39<br>
+ Line 40<br>
+ Line 41<br>
+ Line 42<br>
+ Line 43<br>
+ Line 44<br>
+ Line 45<br>
+ Line 46<br>
+ Line 40<br>
+ Line 48<br>
+ Line 49<br>
+ Line 50<br>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_check_dp_size.html b/gfx/layers/apz/test/mochitest/helper_check_dp_size.html
new file mode 100644
index 0000000000..0a81a69958
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_check_dp_size.html
@@ -0,0 +1,124 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1689492
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1689492, helper page</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+ // -------------------------------------------------------------------
+ // Infrastructure to get the test assertions to run at the right time.
+ // -------------------------------------------------------------------
+ var SimpleTest = window.opener.SimpleTest;
+
+ // --------------------------------------------------------------------
+ // In this test we have a scrollable root scroll frame (not needed, but
+ // more representative), a scrollable outer div, and a scrollable inner
+ // div. We scroll the inner div, and test that it gets a non-zero
+ // display port (not the main reason for the test, that should already
+ // work, but it's a good sanity check), and then check that the outer
+ // div gets a display port and (here's the important part of the test)
+ // that that display port has zero margins, ie it's relatively close to the
+ // dimensions of the outer div (it can't be exact because we align display
+ // ports). This tests a regression where the outer div would get non-zero
+ // margin display port even though it had never been scrolled (it still
+ // needs a display port because it has a scrollable child). We run the
+ // test several times with different sized outerdiv.
+ // --------------------------------------------------------------------
+
+ function createDivs(outerwidth, outerheight) {
+ let outerdiv = document.createElement("div");
+ outerdiv.id = "outerdiv";
+ outerdiv.style.width = outerwidth + "px";
+ outerdiv.style.height = outerheight + "px";
+ outerdiv.style.overflow = "scroll";
+
+ let innerdiv = document.createElement("div");
+ innerdiv.id = "innerdiv";
+ innerdiv.style.width = "25px";
+ innerdiv.style.height = "25px";
+ innerdiv.style.overflow = "scroll";
+ outerdiv.appendChild(innerdiv);
+
+ let innerspacer = document.createElement("div");
+ innerspacer.style.width = "25px";
+ innerspacer.style.height = "100px";
+ innerdiv.appendChild(innerspacer);
+
+ let outerspacer = document.createElement("div");
+ outerspacer.style.width = "50px";
+ outerspacer.style.height = "10000px";
+ outerdiv.appendChild(outerspacer);
+
+
+ let theplace = document.getElementById("theplace");
+ theplace.parentNode.insertBefore(outerdiv, theplace.nextSibling);
+ }
+
+ async function testOne(theheight, allowedscalefactor, outputprefix) {
+ createDivs(50, theheight);
+ // flush layout
+ document.documentElement.getBoundingClientRect();
+ await promiseApzFlushedRepaints();
+
+ document.getElementById("innerdiv").scrollTop = "10px";
+
+ // Activate the inner div.
+ await promiseMoveMouseAndScrollWheelOver(document.getElementById("innerdiv"), 0, 10);
+
+ await promiseApzFlushedRepaints();
+
+ let innerdp = getLastContentDisplayportFor("innerdiv");
+ ok(innerdp.height > 30, outputprefix + " innerdiv display port should be larger than innerdiv");
+
+ let outerdp = getLastContentDisplayportFor("outerdiv");
+ is(outerdp.x, 0, outputprefix + " outerdiv display port should be relatively bounded x");
+ is(outerdp.y, 0, outputprefix + " outerdiv display port should be relatively bounded y");
+ ok(outerdp.width <= 50, outputprefix + " outerdiv display port should relatively bounded w");
+ ok(outerdp.height < theheight * allowedscalefactor, outputprefix + " outerdiv display port should be relatively bounded h");
+
+ ok(true, "innerdp " + JSON.stringify(innerdp));
+ ok(true, "outerdp " + JSON.stringify(outerdp));
+
+ document.getElementById("outerdiv").remove();
+ }
+
+ async function test() {
+ // We test a variety of scroll frame heights.
+ // The first argument of testOne is the scroll frame height.
+ // The second argument is the allowed scale factor of scroll frame height
+ // to display port height.
+ // In the comment following each line we record the values of the display
+ // port height at the time of writing the test in both the good (ie with
+ // the bug this test is testing fixed), and bad (before the bug this
+ // test is testing fixed) cases. These values can obviously be different,
+ // but it gives a good idea that the good and bad values are far apart so
+ // this test should be robust, and provides good context in the future if
+ // this test starts failing.
+ await testOne( 50, 5.2, "(height 50)"); // good 256, bad 256
+ await testOne(128, 2.1, "(height128)"); // good 256, bad 512
+ await testOne(200, 2.0, "(height200)"); // good 384, bad 768
+ await testOne(256, 1.6, "(height256)"); // good 384, bad 768
+ await testOne(329, 1.6, "(height329)"); // good 512, bad 896
+ await testOne(500, 1.3, "(height500)"); // good 640, bad 280
+ await testOne(640, 1.7, "(height640)"); // good 1024, bad 1536
+
+ }
+
+ waitUntilApzStable()
+ .then(forceLayerTreeToCompositor)
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</head>
+<body>
+ <a id="theplace" target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1689492">Mozilla Bug 1689492</a>
+ <!-- Put enough content into the page to make it have a nonzero scroll range, not needed -->
+ <div style="height: 5000px"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_checkerboard_apzforcedisabled.html b/gfx/layers/apz/test/mochitest/helper_checkerboard_apzforcedisabled.html
new file mode 100644
index 0000000000..404368a803
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_checkerboard_apzforcedisabled.html
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<html id="root-element">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Checkerboarding while root scrollframe async-scrolls and a
+ subframe has APZ force disabled</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ var subframe = document.getElementById('subframe');
+
+ // layerize subframe
+ await promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "click",
+ target: subframe,
+ offsetX: 10,
+ offsetY: 10,
+ });
+
+ // verify layerization
+ await promiseAllPaintsDone();
+ ok(isLayerized("subframe"), "subframe should be layerized at this point");
+ var subframeScrollId = utils.getViewId(subframe);
+ ok(subframeScrollId > 0, "subframe should have a scroll id");
+
+ // then disable APZ for it
+ utils.disableApzForElement(subframe);
+
+ // wait for the dust to settle
+ await promiseAllPaintsDone();
+
+ // Check that the root element's displayport has at least 500px of vertical
+ // displayport margin on either side. This will ensure that we can scroll
+ // by 500px without causing the displayport to move, which in turn means that
+ // the scroll will not trigger repaints (due to paint-skipping).
+ var rootElem = document.documentElement;
+ var rootDisplayport = getLastContentDisplayportFor(rootElem.id);
+ ok(rootDisplayport != null, "root element should have a displayport");
+ dump("root dp: " + JSON.stringify(rootDisplayport) +
+ ", height: " + rootElem.clientHeight);
+ var rootDpVerticalMargin = (rootDisplayport.height - rootElem.clientHeight) / 2;
+ ok(rootDpVerticalMargin > 500,
+ "root element should have at least 500px of vertical displayport margin");
+
+ // Scroll enough that we reveal new parts of the subframe, but not so much
+ // that the root displayport starts moving. If the root displayport moves,
+ // the main-thread will trigger a repaint of the subframe, but if the root
+ // displayport doesn't move, we get a paint-skipped scroll which is where the
+ // bug manifests. (The bug being that the subframe ends in a visual perma-
+ // checkerboarding state). Note that we do an 'auto' behavior scroll so
+ // that it's "instant" rather than an animation. Animations would demonstrate
+ // the bug too but are more complicated to wait for.
+ window.scrollBy({top: 500, left: 0, behavior: 'auto'});
+ is(window.scrollY, 500, "document got scrolled instantly");
+
+ // Note that at this point we must NOT call promiseOnlyApzControllerFlushed, because
+ // otherwise APZCCallbackHelper::NotifyFlushComplete will trigger a repaint
+ // (for unrelated reasons), and the repaint will clear the checkerboard
+ // state. We do, however, want to wait for a "steady state" here that
+ // includes all pending paints from the main thread and a composite that
+ // samples the APZ state. In order to accomplish this we wait for all the main
+ // thread paints, and then force a composite via advanceTimeAndRefresh. The
+ // advanceTimeAndRefresh has the additional advantage of freezing the refresh
+ // driver which avoids any additional externally-triggered repaints from
+ // erasing the symptoms of the bug.
+ await promiseAllPaintsDone();
+ assertNotCheckerboarded(utils, subframeScrollId, "subframe");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ #subframe {
+ overflow-x: auto;
+ margin-left: 100px; /* makes APZ minimap easier to see */
+ }
+ </style>
+</head>
+<body>
+ <div id="subframe">
+ <div style="width: 10000px; height: 10000px; background-image: linear-gradient(green, red)">
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_checkerboard_no_multiplier.html b/gfx/layers/apz/test/mochitest/helper_checkerboard_no_multiplier.html
new file mode 100644
index 0000000000..149ef9fbba
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_checkerboard_no_multiplier.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html lang="en"><head>
+<meta http-equiv="content-type" content="text/html; charset=UTF-8"><meta charset="utf-8">
+<title>Testcase for checkerboarding with displayport multipliers dropped to zero</title>
+<script type="application/javascript" src="apz_test_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<meta name="viewport" content="width=device-width"/>
+<style>
+body, html {
+ margin: 0;
+}
+</style>
+<body>
+ <div style="height: 5000px; background-color: green"></div>
+</body>
+<script type="application/javascript">
+async function test() {
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ var scrollerId = utils.getViewId(document.scrollingElement);
+
+ // Zoom in a bunch
+ const scale = 3.0;
+ utils.setResolutionAndScaleTo(scale);
+
+ // And now we scroll the visual viewport to cover the range it has inside
+ // the layout viewport, plus a bit more so that we also cover the boundary
+ // case where the layout viewport has to move.
+ // At each scroll position, we make sure there's no checkerboarding.
+ // We advance the scroll position on each axis by 43 CSS pixels at a time,
+ // because 43 is a non-power-of-two/prime number and should give us reasonable
+ // coverage of different displayport tile alignment values. Making the
+ // increment too small increases runtime and too large might miss some
+ // alignment values so this seems like a good number.
+
+ async function scrollAndCheck(x, y) {
+ dump(`Scrolling visual viewport to ${x}, ${y}\n`);
+ utils.scrollToVisual(x, y, utils.UPDATE_TYPE_MAIN_THREAD, utils.SCROLL_MODE_INSTANT);
+ await promiseApzFlushedRepaints();
+ assertNotCheckerboarded(utils, scrollerId, `At ${x}, ${y}`);
+ }
+
+ let vv_scrollable_x = window.innerWidth - (window.innerWidth / scale);
+ for (var x = 0; x < vv_scrollable_x + 100; x += 43) {
+ await scrollAndCheck(x, 0);
+ }
+ ok(window.scrollX == 0, "Layout viewport couldn't move on the x-axis, page not scrollable that way");
+ let vv_scrollable_y = window.innerHeight - (window.innerHeight / scale);
+ for (var y = 0; y < vv_scrollable_y + 100; y += 43) {
+ await scrollAndCheck(0, y);
+ }
+ ok(window.scrollY > 0, `Layout viewport moved down to ${window.scrollY} on the y-axis`);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed)
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_checkerboard_scrollinfo.html b/gfx/layers/apz/test/mochitest/helper_checkerboard_scrollinfo.html
new file mode 100644
index 0000000000..2e718ad44b
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_checkerboard_scrollinfo.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Scrolling a scrollinfo layer and making sure it doesn't checkerboard</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+<style>
+ #withfilter {
+ filter: url(#menushadow);
+ }
+
+ #scroller {
+ width: 300px;
+ height: 1038px;
+ overflow: scroll;
+ }
+
+ .spacer {
+ height: 1878px;
+ background-image: linear-gradient(red, blue);
+ }
+</style>
+</head>
+<body>
+ <div id="withfilter">
+ <div id="scroller">
+ <div class="spacer"></div>
+ </div>
+ </div>
+<!-- the SVG below copied directly from the Gecko Profiler code that
+ demonstrated the original bug. It basically generates a bit of a "drop
+ shadow" effect on the div it's applied to. Original SVG can be found at
+ https://github.com/firefox-devtools/profiler/blame/624f71bce5469cf4f8b2be720e929ba69fa6bfdc/res/img/svg/shadowfilter.svg -->
+ <svg xmlns="http://www.w3.org/2000/svg">
+ <defs>
+ <filter id="menushadow" color-interpolation-filters="sRGB" x="-10" y="-10" width="30" height="30">
+ <feComponentTransfer in="SourceAlpha">
+ <feFuncA type="linear" slope="0.3"/>
+ </feComponentTransfer>
+ <feGaussianBlur stdDeviation="5"/>
+ <feOffset dy="10" result="shadow"/>
+ <feComponentTransfer in="SourceAlpha">
+ <feFuncA type="linear" slope="0.1"/>
+ </feComponentTransfer>
+ <feMorphology operator="dilate" radius="0.5" result="rim"/>
+ <feMerge><feMergeNode in="shadow"/><feMergeNode in="rim"/></feMerge>
+ <feComposite operator="arithmetic" in2="SourceAlpha" k2="1" k3="-0.1"/>
+ <feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge>
+ </filter>
+ </defs>
+ </svg>
+</body>
+<script type="application/javascript">
+async function test() {
+ var scroller = document.querySelector("#scroller");
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ var scrollerId = utils.getViewId(scroller);
+
+ // Scroll to the bottom of the page, so that the bottom of #scroller is
+ // visible; that's where the checkerboarding happens.
+ document.scrollingElement.scrollTop = document.scrollingElement.scrollTopMax;
+
+ // After the first call to promiseApzFlushedRepaints, the scroller will have
+ // zero displayport margins (because it's inside an SVG filter, and so takes
+ // the "scroll info layer" codepath in APZ's CalculatePendingDisplayPort
+ // function. The main-thread then computes a displayport using those zero
+ // margins and alignment heuristics. If those heuristics are buggy, then the
+ // scroller may end up checkerboarding. That's what we check for on each
+ // scroll increment.
+
+ // The scroll values here just need to be "thorough" enough to exercise the
+ // code at different alignments, so using a non-power-of-two or prime number
+ // for the increment seems like a good idea. The smaller the increment, the
+ // longer the test takes to run (because more iterations) so we don't want it
+ // too small either.
+ for (var y = 3; y <= scroller.scrollTopMax; y += 17) {
+ dump(`Scrolling scroller to ${y}\n`);
+ scroller.scrollTo(0, y);
+ await promiseApzFlushedRepaints();
+ assertNotCheckerboarded(utils, scrollerId, `At y=${y}`);
+ }
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_checkerboard_zoom_during_load.html b/gfx/layers/apz/test/mochitest/helper_checkerboard_zoom_during_load.html
new file mode 100644
index 0000000000..650b1c265d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_checkerboard_zoom_during_load.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html lang="en"><head>
+<meta http-equiv="content-type" content="text/html; charset=UTF-8"><meta charset="utf-8">
+<title>Testcase for checkerboarding after zooming during page load</title>
+<script type="application/javascript" src="apz_test_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<meta name="viewport" content="width=device-width"/>
+<style>
+body, html {
+ margin: 0;
+}
+</style>
+<body>
+ <div style="height: 5000px; background-color: green"></div>
+</body>
+<script type="application/javascript">
+
+// This function runs after page load, but simulates what might happen if
+// the user does a zoom during page load. It's hard to actually do this
+// during page load, because the specific behaviour depends on interleaving
+// between paints and the load event which is hard to control from a test.
+// So instead, we do the zoom after page load, and then trigger a MVM reset
+// which simulates what happens during the pageload process.
+async function test() {
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+
+ // Make it so that the layout and visual viewports diverge. We do this by
+ // zooming and then moving the visual viewport.
+ utils.setResolutionAndScaleTo(2);
+ var x = window.innerWidth / 2;
+ var y = window.innerHeight / 2;
+ utils.scrollToVisual(x, y, utils.UPDATE_TYPE_MAIN_THREAD, utils.SCROLL_MODE_INSTANT);
+ dump("Done scrollToVisual\n");
+
+ // Next, kick off a paint transaction to APZ, so that it sets appropriate
+ // displayport margins with visual/layout adjustment factors.
+ await promiseApzFlushedRepaints();
+
+ // Once that's done, we want to trigger the MobileViewportManager to update
+ // the displayport margins.
+ dump("Resetting MVM...\n");
+ utils.resetMobileViewportManager();
+
+ // The bug is that at this point, paints end up checkerboarding because the
+ // MVM code to update the displayport margins doesn't preserve the layout
+ // adjustment factor needed.
+ utils.advanceTimeAndRefresh(0);
+ assertNotCheckerboarded(utils, utils.getViewId(document.scrollingElement), `Should not see checkerboarding`);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_checkerboard_zoomoverflowhidden.html b/gfx/layers/apz/test/mochitest/helper_checkerboard_zoomoverflowhidden.html
new file mode 100644
index 0000000000..63a07ebe9b
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_checkerboard_zoomoverflowhidden.html
@@ -0,0 +1,150 @@
+<!DOCTYPE HTML>
+<html id="root-element">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Checkerboarding in while scrolling a subframe when root scrollframe has
+ overflow hidden and pinch zoomed in</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+
+ var initial_resolution = await getResolution();
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+
+ var subframe = document.getElementById('bugzilla-body');
+
+ // layerize subframe
+ await promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "click",
+ target: subframe,
+ offsetX: 10,
+ offsetY: 10,
+ });
+
+ // verify layerization
+ await promiseAllPaintsDone();
+ ok(isLayerized("bugzilla-body"), "subframe should be layerized at this point");
+ var subframeScrollId = utils.getViewId(subframe);
+ ok(subframeScrollId > 0, "subframe should have a scroll id");
+
+ // wait for the dust to settle
+ await promiseAllPaintsDone();
+
+ let touchEndPromise = promiseTouchEnd(document.documentElement, 2);
+
+ // Ensure that APZ gets updated hit-test info
+ await promiseAllPaintsDone();
+
+ var zoom_in = [
+ [ { x: 130, y: 280 }, { x: 150, y: 300 } ],
+ [ { x: 120, y: 250 }, { x: 160, y: 380 } ],
+ [ { x: 115, y: 200 }, { x: 180, y: 410 } ],
+ [ { x: 110, y: 150 }, { x: 200, y: 440 } ],
+ [ { x: 105, y: 120 }, { x: 210, y: 470 } ],
+ [ { x: 100, y: 100 }, { x: 230, y: 500 } ],
+ ];
+
+ var touchIds = [0, 1];
+ await synthesizeNativeTouchSequences(document.body, zoom_in, null, touchIds);
+
+ await touchEndPromise;
+ // Flush state and get the resolution we're at now
+ await promiseApzFlushedRepaints();
+ let final_resolution = await getResolution();
+ ok(final_resolution > initial_resolution, "The final resolution (" + final_resolution + ") is greater than the initial resolution");
+
+ touchEndPromise = promiseTouchEnd(document.documentElement);
+
+ // pan back up to the top left
+ await promiseNativeTouchDrag(window,
+ 5,
+ 5,
+ 500,
+ 500,
+ 2);
+
+ await touchEndPromise; // wait for the touchend listener to fire
+ await promiseApzFlushedRepaints();
+ await promiseAllPaintsDone();
+
+ touchEndPromise = promiseTouchEnd(document.documentElement);
+
+ // pan right to expose the bug
+ await promiseNativeTouchDrag(window,
+ 100,
+ 20,
+ -180,
+ 0,
+ 3);
+
+ await touchEndPromise; // wait for the touchend listener to fire
+ await promiseApzFlushedRepaints();
+
+ assertNotCheckerboarded(utils, subframeScrollId, "Subframe");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+html,
+body {
+ overflow-y: hidden;
+ height: 100%;
+}
+
+body {
+ position: absolute;
+ margin: 0;
+ width: 100%;
+ height: 100%;
+}
+
+#wrapper {
+ position: initial !important;
+ display: flex;
+ flex-direction: column;
+ position: absolute;
+ overflow: hidden;
+ width: 100%;
+ height: 100%;
+}
+
+#bugzilla-body {
+ flex: auto;
+ position: relative;
+ outline: none;
+ padding: 0 15px;
+ overflow-x: auto;
+ overflow-y: scroll;
+ will-change: transform;
+}
+ </style>
+</head>
+<body>
+ <div id="wrapper">
+ <main id="bugzilla-body">
+ <p>STR:</p>
+ <ol>
+ <li>set <code>apz.allow_zoom</code> to <code>true</code></li>
+ <li>visit any bugzilla site (like this one)</li>
+ <li>zoom into the page and observe the left edge of the viewport</li>
+ </ol>
+ <p>ER: content should be shown<br>
+ AR: foreground content seems to disappear, looks like it's being cut off
+ </p>
+ <p>I attached a video of the STR to show the problem a little bit better. So far, I could only reproduce this on bugzilla. Words words words words words words words words words words words words words words words words words words words words words words.</p>
+
+ <div style="height: 10000px;"></div>
+ </main>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_click.html b/gfx/layers/apz/test/mochitest/helper_click.html
new file mode 100644
index 0000000000..7c4501cb46
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_click.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity mouse-clicking test</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function clickButton() {
+ let clickPromise = new Promise(resolve => {
+ document.addEventListener("click", resolve);
+ });
+
+ if (getQueryArgs().dtc) {
+ // force a dispatch-to-content region on the document
+ document.addEventListener("wheel", function() { /* no-op */ }, { passive: false });
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ }
+
+ await synthesizeNativeMouseEventWithAPZ(
+ { type: "click", target: document.getElementById("b"), offsetX: 5, offsetY: 5 },
+ () => dump("Finished synthesizing click, waiting for button to be clicked...\n")
+ );
+
+ let e = await clickPromise;
+ is(e.target, document.getElementById("b"), "Clicked on button, yay! (at " + e.clientX + "," + e.clientY + ")");
+}
+
+waitUntilApzStable()
+.then(clickButton)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <button id="b" style="width: 10px; height: 10px"></button>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_click_interrupt_animation.html b/gfx/layers/apz/test/mochitest/helper_click_interrupt_animation.html
new file mode 100644
index 0000000000..adba0d90ea
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_click_interrupt_animation.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Clicking on the content (not scrollbar) should interrupt animations</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript">
+
+async function test() {
+ var scroller = document.documentElement;
+ var verticalScrollbarWidth = window.innerWidth - scroller.clientWidth;
+
+ if (verticalScrollbarWidth == 0) {
+ ok(true, "Scrollbar width is zero on this platform, test is useless here");
+ return;
+ }
+
+ // The anchor is the fixed-pos div that we use to calculate coordinates to
+ // click on the scrollbar. That way we don't have to recompute coordinates
+ // as the page scrolls. The anchor is at the bottom-right corner of the
+ // content area.
+ var anchor = document.getElementById('anchor');
+
+ var xoffset = (verticalScrollbarWidth / 2);
+ // Get a y-coord near the bottom of the vertical scrollbar track. Assume the
+ // vertical thumb is near the top of the scrollback track (since scroll
+ // position starts off at zero) and won't get in the way. Also assume the
+ // down arrow button, if there is one, is square.
+ var yoffset = 0 - verticalScrollbarWidth - 5;
+
+ // Take control of the refresh driver
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ utils.advanceTimeAndRefresh(0);
+
+ // Click at the bottom of the scrollbar track to trigger a page-down kind of
+ // scroll. This should use "desktop zooming" scrollbar code which should
+ // trigger an APZ scroll animation.
+ await promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "click",
+ target: anchor,
+ offsetX: xoffset,
+ offsetY: yoffset,
+ eventTypeToWait: "mouseup"
+ });
+
+ // Run a few frames, that should be enough to let the scroll animation
+ // start. We check to make sure the scroll position has changed.
+ for (let i = 0; i < 5; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ await promiseOnlyApzControllerFlushed();
+
+ let curPos = scroller.scrollTop;
+ ok(curPos > 0,
+ `Scroll offset has moved down some, to ${curPos}`);
+
+ // Now we click on the content, which should cancel the animation. Run
+ // everything to reach a stable state.
+ await promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "click",
+ target: anchor,
+ offsetX: -5,
+ offsetY: -5,
+ });
+ for (let i = 0; i < 1000; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ await promiseOnlyApzControllerFlushed();
+
+ // Ensure the scroll position hasn't changed since the last time we checked,
+ // which indicates the animation got interrupted.
+ is(scroller.scrollTop, curPos, `Scroll position hasn't changed again`);
+
+ utils.restoreNormalRefresh();
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div style="position:fixed; bottom: 0; right: 0; width: 1px; height: 1px" id="anchor"></div>
+ <div style="height: 300vh; margin-bottom: 10000px; background-image: linear-gradient(red,blue)"></div>
+ The above div is sized to 3x screen height so the linear gradient is more steep in terms of
+ color/pixel. We only scroll a few pages worth so we don't need the gradient all the way down.
+ And then we use a bottom-margin to make the page really big so the scrollthumb is
+ relatively small, giving us lots of space to click on the scrolltrack.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_content_response_timeout.html b/gfx/layers/apz/test/mochitest/helper_content_response_timeout.html
new file mode 100644
index 0000000000..41a0319699
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_content_response_timeout.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<style>
+html { overflow-y: scroll; }
+body { margin: 0; }
+div { height: 1000vh; }
+</style>
+<div id='target'></div>
+<script>
+window.addEventListener('wheel', (e) => {
+ const timeAtStart = window.performance.now();
+ while (window.performance.now() - timeAtStart < 200) {
+ // Make a 200ms busy state.
+ }
+}, { passive: false});
+// Silence SimpleTest warning about missing assertions by having it wait
+// indefinitely. We don't need to give it an explicit finish because the
+// entire window this test runs in will be closed after subtestDone is called.
+SimpleTest.waitForExplicitFinish();
+</script>
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_disallow_doubletap_zoom_inside_oopif.html b/gfx/layers/apz/test/mochitest/helper_disallow_doubletap_zoom_inside_oopif.html
new file mode 100644
index 0000000000..2c83cb1a1f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_disallow_doubletap_zoom_inside_oopif.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="user-scalable=no"/>
+ <title>Check that double tapping inside an oop iframe doesn't work if the top
+ level content document doesn't allow zooming</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Set up a Promise waiting for a TransformEnd which should never happen.
+ promiseTransformEnd().then(() => {
+ ok(false, "No TransformEnd should happen!");
+ });
+
+ // A double tap inside the OOP iframe.
+ await synthesizeDoubleTap(document.getElementById("target"), 20, 20, useTouchpad);
+
+ for (let i = 0; i < 10; i++) {
+ await promiseFrame();
+ }
+
+ // Flush state just in case.
+ await promiseApzFlushedRepaints();
+
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ is(resolution, prev_resolution, "No zoom should happen");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+<style>
+iframe {
+ margin: 0;
+ padding: 0;
+ border: 1px solid black;
+}
+</style>
+</head>
+<body>
+
+<iframe id="target" width="100" height="100" src="http://example.org/"></iframe>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_displayport_expiry.html b/gfx/layers/apz/test/mochitest/helper_displayport_expiry.html
new file mode 100644
index 0000000000..023786a270
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_displayport_expiry.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for DisplayPort Expiry</title>
+ <meta charset="utf-8">
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+#first {
+ height: 40vh;
+ width: 100%;
+ background: green;
+ overflow: scroll;
+}
+
+#second {
+ height: 40vh;
+ width: 100%;
+ background: yellow;
+ overflow: scroll;
+}
+
+#inner {
+ height: 20vh;
+ width: 50%;
+ background: red;
+}
+
+.big {
+ height: 100vh;
+ width: 100vw;
+}
+</style>
+</head>
+<body>
+ <div id="first">
+ <div class="big">
+ </div>
+ </div>
+ <br>
+ <div id="second">
+ <div id="inner">
+ <div class="big">
+ </div>
+ </div>
+ <div class="big">
+ </div>
+ </div>
+</body>
+ <script>
+async function test() {
+ await promiseFrame();
+
+ let paintCount = 0;
+ function countPaints(e) {
+ paintCount += 1;
+ }
+
+ window.addEventListener("MozAfterPaint", countPaints);
+
+ await SpecialPowers.promiseTimeout(200);
+
+ window.removeEventListener("MozAfterPaint", countPaints);
+
+ info("paint count: " + paintCount);
+
+ ok(paintCount < 5, "Paint count is within the expect range");
+}
+
+SimpleTest.waitForExplicitFinish();
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+ </script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_div_pan.html b/gfx/layers/apz/test/mochitest/helper_div_pan.html
new file mode 100644
index 0000000000..b740b2f16a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_div_pan.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity panning test for scrollable div</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let transformEndPromise = promiseTransformEnd();
+
+ await synthesizeNativeTouchDrag(document.getElementById("outer"), 10, 100, 0, -50);
+ dump("Finished native drag, waiting for transform-end observer...\n");
+
+ await transformEndPromise;
+
+ dump("Transform complete; flushing repaints...\n");
+ await promiseOnlyApzControllerFlushed();
+
+ var outerScroll = document.getElementById("outer").scrollTop;
+ is(outerScroll, 50, "check that the div scrolled");
+}
+
+waitUntilApzStable()
+ .then(test)
+ .then(subtestDone);
+
+ </script>
+</head>
+<body>
+ <div id="outer" style="height: 250px; border: solid 1px black; overflow:scroll">
+ <div style="height: 5000px; background-color: lightblue">
+ This div makes the |outer| div scrollable.
+ </div>
+ </div>
+ <div style="height: 5000px; background-color: lightgreen;">
+ This div makes the top-level page scrollable.
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_dommousescroll.html b/gfx/layers/apz/test/mochitest/helper_dommousescroll.html
new file mode 100644
index 0000000000..390db367f5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_dommousescroll.html
@@ -0,0 +1,33 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test that Direct Manipulation generated pan gesture events generate DOMMouseScroll events with reasonable line scroll amounts</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+</head>
+<body>
+ <script type="application/javascript">
+async function test() {
+ let numLines = 0;
+ window.addEventListener("DOMMouseScroll", function (event) { numLines += event.detail; }, { passive: false });
+ let promise = new Promise(resolve => {
+ window.addEventListener("DOMMouseScroll", resolve, { passive: false });
+ });
+ await promiseApzFlushedRepaints();
+
+ await synthesizeTouchpadPan(20, 100, [0,0,0], [10,100,0], {});
+
+ await waitToClearOutAnyPotentialScrolls(window);
+ await promise;
+
+ info(numLines + " numLines");
+ ok(numLines < 10, "not too many lines");
+ ok(numLines > 0, "some lines");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom.html
new file mode 100644
index 0000000000..4cd613edbb
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Sanity check for double-tap zooming</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ var resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(document.getElementById("target"), 10, 10, useTouchpad);
+ var prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // Check that double-tapping again on the same spot zooms out
+ await doubleTapOn(document.getElementById("target"), 10, 10, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style type="text/css">
+ .box {
+ width: 800px;
+ height: 500px;
+ margin: 0 auto;
+ }
+</style>
+</head>
+<body>
+<div class="box">Text before the div.</div>
+<div id="target" style="margin-left: 100px; width:900px; height: 400px; background-image: linear-gradient(blue,red)"></div>
+<div class="box">Text after the div.</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_bug1702464.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_bug1702464.html
new file mode 100644
index 0000000000..34d06fc039
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_bug1702464.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping internal calculations correctly convert the tap point</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ const deviceScale = window.devicePixelRatio;
+ let target2 = document.getElementById("target2")
+
+ info("deviceScale " + deviceScale);
+ info("window.innerwh " + window.innerWidth + " " + window.innerHeight);
+ info("vv " + visualViewport.offsetLeft + " " + visualViewport.offsetTop + " " + visualViewport.width + " " + visualViewport.height);
+
+ let resolution = await getResolution();
+ let vvHeightAtUnitZoom = visualViewport.height * resolution;
+
+ // The max amount of zoom is 10 (as defined in dom/base/nsViewportInfo.h),
+ // but that includes the deviceScale (bug 1700865 is filed about this), so
+ // this is the max amount of zoom that double tap can increase by.
+ let maxPinchZoom = 10/deviceScale;
+
+ // Compute the visual viewport size at that max zoom.
+ let minVVHeight = vvHeightAtUnitZoom / maxPinchZoom;
+
+ // Make the element height to just fit inside the minimum visual viewport
+ // height, minus the margins that get added for the zoom target rect (15 on
+ // each side) and a little wiggle room just in case (rounding, etc).
+ let elementHeight = Math.floor(minVVHeight) - 2*15 - 4;
+
+ ok(elementHeight > 10, "tall enough element");
+
+ // And then make the element skinnier than the window size so it triggers
+ // the bug. (half the aspect ratio minus 5 just to be sure)
+ let elementWidth = Math.max(12, Math.floor(elementHeight * window.innerWidth / (2 * window.innerHeight)) - 5);
+
+ info("element size " + elementWidth + " " + elementHeight);
+
+ target2.style.width = elementWidth + "px";
+ target2.style.height = elementHeight + "px";
+
+ await promiseApzFlushedRepaints();
+
+ resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(document.getElementById("target1"), 10, 10, useTouchpad);
+ var prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // Check that double-tapping the smaller element zooms in more
+ await doubleTapOn(target2, 8, elementHeight-8, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The second double-tap has increased the resolution to " + resolution);
+
+ let rect = target2.getBoundingClientRect();
+ info("rect " + rect.x + " " + rect.y + " " + rect.width + " " + rect.height);
+ info("vv " + visualViewport.offsetLeft + " " + visualViewport.offsetTop + " " + visualViewport.width + " " + visualViewport.height);
+
+ ok(visualViewport.offsetLeft < rect.x, "visual viewport contains zoom element left");
+ ok(visualViewport.offsetTop < rect.y, "visual viewport contains zoom element top");
+ ok(visualViewport.offsetLeft + visualViewport.width > rect.x + rect.width, "visual viewport contains zoom element right");
+ ok(visualViewport.offsetTop + visualViewport.height > rect.y + rect.height, "visual viewport contains zoom element bottom");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+
+<div id="target1" style="background: blue; width: 50vw; height: 200px; position: absolute; top: 50vh;">
+ <div id="target2" style="background: green; width: 50px; height: 135px; position: absolute; right: 0;"></div>
+</div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos.html
new file mode 100644
index 0000000000..1da5607d7e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping active scrollable elements in fixed pos work</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function makeActive(x, y, targetId) {
+ let theTarget = document.getElementById(targetId);
+ await promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "click",
+ target: theTarget,
+ offsetX: x,
+ offsetY: y,
+ });
+
+ await promiseApzFlushedRepaints();
+
+ ok(isLayerized(targetId), "target should be layerized at this point");
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ let targetScrollId = utils.getViewId(theTarget);
+ ok(targetScrollId > 0, "target should have a scroll id");
+}
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ await makeActive(100, 50, "target");
+
+ let target = document.getElementById("target");
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(target, 100, 50, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // Check that double-tapping again on the same spot zooms out
+ await doubleTapOn(target, 100, 50, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+<style>
+.fixed {
+ top: 100px;
+ width: 500px;
+ height: 300px;
+ background: blue;
+ position: fixed;
+}
+.abox {
+ width: 200px;
+ height: 100px;
+ background: yellow;
+ overflow: auto;
+}
+.spacer {
+ height: 400vh;
+ background: lightgrey;
+}
+</style>
+</head>
+<body>
+
+<div class="fixed">
+ <div class="abox" id="target">
+ <div class="spacer" style="width: 50px;"></div>
+ </div>
+</div>
+<div class="spacer" style="width: 100px;"></div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos_overflow.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos_overflow.html
new file mode 100644
index 0000000000..0658c562c1
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos_overflow.html
@@ -0,0 +1,113 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping elements with large overflow inside active scrollable elements in fixed pos work</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function makeActive(x, y, targetId) {
+ let theTarget = document.getElementById(targetId);
+ await promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "click",
+ target: theTarget,
+ offsetX: x,
+ offsetY: y,
+ });
+
+ await promiseApzFlushedRepaints();
+
+ ok(isLayerized(targetId), "target should be layerized at this point");
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ let targetScrollId = utils.getViewId(theTarget);
+ ok(targetScrollId > 0, "target should have a scroll id");
+}
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ await makeActive(25, 25, "scrollertarget");
+
+ let target = document.getElementById("target");
+
+ // Check that double-tapping once zooms in
+ // Coords outside of the main rect but inside the overflow to trigger the
+ // bug we are testing.
+ await doubleTapOn(target, 25, 120, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // Check that double-tapping again on the same spot zooms out
+ await doubleTapOn(target, 25, 120, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+<style>
+.fixed {
+ top: 100px;
+ width: 500px;
+ height: 300px;
+ background: blue;
+ position: fixed;
+}
+.abox {
+ width: 200px;
+ height: 200px;
+ background: yellow;
+ overflow: auto;
+}
+.spacer {
+ height: 400vh;
+ background: lightgrey;
+}
+</style>
+</head>
+<body>
+
+<div class="fixed">
+ <div id="scrollertarget" class="abox">
+ <div class="spacer" style="width: 150px;">
+ <div id="target" style="background-color: #eee; width: 145px; height: 50px; border: 1px dotted black; overflow: visible;">
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ </div>
+ </div>
+ </div>
+</div>
+<div class="spacer" style="width: 100px;"></div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_gencon.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_gencon.html
new file mode 100644
index 0000000000..01b1f060d8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_gencon.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that on generated content works</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ let initial_resolution = resolution;
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ let target = document.getElementById("target");
+
+ info("tar " + target.getBoundingClientRect().width);
+
+ // Check that first double tap zooms in
+ info("sending first double tap");
+ await doubleTapOn(target, 10, 10, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "After double-tap the resolution has increased to " + resolution);
+
+ // Check that second double tap zooms out
+ info("sending second double tap");
+ await doubleTapOn(target, 10, 10, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "After double-tap the resolution has decreased to " + resolution);
+ ok(resolution == initial_resolution, "After double-tap the resolution has decreased to initial_resolution");
+
+ info(" window.innerWidth " + window.innerWidth);
+
+ // Check that third double tap zooms in
+ info("sending third double tap");
+ await doubleTapOn(document.getElementById("placeholder"), 10, 10, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "After double-tap the resolution has increased to " + resolution);
+
+ info(" window.innerWidth " + window.innerWidth);
+
+ // Check that fourth double tap zooms out
+ info("sending forth double tap");
+ await doubleTapOn(document.getElementById("placeholder"), 10, 10, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "After double-tap the resolution has decreased to " + resolution);
+ ok(resolution == initial_resolution, "After double-tap the resolution has decreased to initial_resolution");
+
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+body, html {
+ margin: 0;
+}
+.withafter {
+ width: 200px;
+ height: 200px;
+ left: 0;
+ background: green;
+ position: relative;
+}
+.withafter::after {
+ width: 20vw;
+ height: 100px;
+ background: blue;
+ position: absolute;
+ left: 80vw;
+ content: 'after';
+}
+.placeholder {
+ width: 20vw;
+ height: 100px;
+ background: blue;
+ position: absolute;
+ left: 80vw;
+ top:0;
+ z-index: -10;
+}
+</style>
+</head>
+<body>
+
+<div id="target" class="withafter">some text</div>
+<div id="placeholder" class="placeholder"></div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_horizontal_center.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_horizontal_center.html
new file mode 100644
index 0000000000..ab945fab1f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_horizontal_center.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping an element that doesn't fill the width of the viewport as maximum zoom centers it</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ var resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(document.getElementById("target"), 10, 10, useTouchpad);
+ var prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > 2*prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ info("window.innerWidth " + window.innerWidth); // value when writing this test: 1280
+ info("visualViewport.offsetLeft " + visualViewport.offsetLeft); //value when writing this test: 625 with bug, 537 with fix
+ info("visualViewport.width " + visualViewport.width); // value when writing this test: 253
+ // the left hand edge of the div is at window.innerWidth/2
+ // we want approximately half of the visual viewport width to be to the left of that point.
+ // we have to remove the 50 pixels for the width of the div from that first though.
+ // we multiply that by 80% to factor in the 15 pixel margin we give the zoomed-to element
+ // (we don't hard code 15 though since we might want to tweak that)
+ ok(visualViewport.offsetLeft < window.innerWidth/2 - 0.8*(visualViewport.width-50)/2, "moved over far enough");
+ // and then have a sanity check that it's not too small.
+ // using the same 20% factor as before but in the other direction.
+ ok(visualViewport.offsetLeft > window.innerWidth/2 - 1.2*(visualViewport.width-50)/2, "but not too far");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+
+<div id="target" style="background: blue; width: 50px; height: 50px; position: absolute; left: 50vw;"></div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable.html
new file mode 100644
index 0000000000..21c3fb7e70
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that tall element wider than the viewport doesn't scroll to the top</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+// Although this test has hscrollable in the name, it does not test any
+// horizontal scrolling. Rather it is the mere presence of horizontally
+// scrollable content that triggers this bug (it confused the code that
+// picked a rect to zoom to).
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // instant scroll down
+ window.scrollTo({
+ top: window.innerHeight * 2,
+ left: 0,
+ behavior: 'auto'
+ });
+
+ await promiseApzFlushedRepaints();
+
+ let scrollPos = window.scrollY;
+ ok(scrollPos > window.innerHeight * 2 - 50, "window scrolled down");
+
+ info("window.scrollY " + window.scrollY);
+
+ info("window.innerHeight " + window.innerHeight);
+
+ info("document.documentElement.scrollHeight " + document.documentElement.scrollHeight);
+
+ let target = document.getElementById("target");
+
+ let x = 20;
+ let y = scrollPos + window.innerHeight / 2;
+
+ // Check that second double tap does not scroll up
+ info("sending second double tap");
+ await doubleTapOn(target, x, y, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "After double-tap the resolution is the same: " + resolution);
+
+ ok(window.scrollY > window.innerHeight * 2 - 50, "window is still scrolled down");
+ ok(Math.abs(window.scrollY - scrollPos) < 1, "window didnt scroll");
+ info("window.scrollY " + window.scrollY);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ .spacer {
+ background-color: #eee;
+ height: 800vh;
+ width: 200vw;
+ }
+ .rect {
+ width: 90vw;
+ height: 30px;
+ background-color: #aaa;
+ }
+</style>
+</head>
+<body>
+<div id="firsttarget" class="rect">
+</div>
+
+<div id="target" class="spacer">
+</div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable2.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable2.html
new file mode 100644
index 0000000000..fc74ff1c89
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable2.html
@@ -0,0 +1,109 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that tall element wider than the viewport after zooming in doesn't scroll up</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // instant scroll down so the rect is roughly halfway down
+ window.scrollTo({
+ top: window.innerHeight * 3.5,
+ left: 0,
+ behavior: 'auto'
+ });
+
+ await promiseApzFlushedRepaints();
+
+ let scrollPos = window.scrollY;
+ ok(scrollPos > window.innerHeight * 3.5 - 50, "window scrolled down");
+
+ info("window.scrollY " + window.scrollY);
+
+ info("window.innerHeight " + window.innerHeight);
+
+ info("document.documentElement.scrollHeight " + document.documentElement.scrollHeight);
+
+ let target = document.getElementById("target");
+
+ // Check that first double tap does not scroll up
+ info("sending first double tap");
+ await doubleTapOn(target, 15, 15, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "After double-tap the resolution increased to " + resolution);
+
+ // These values were determined experimentally, have not investigated the
+ // reason for the difference between platforms or the large tolerance needed.
+ let tolerance = 2;
+ if (getPlatform() == "mac") {
+ tolerance = 24;
+ }
+
+ ok(window.scrollY > window.innerHeight * 3.5 - tolerance, "window is still scrolled down");
+ ok(Math.abs(window.scrollY - scrollPos) < tolerance, "window didnt scroll: " + Math.abs(window.scrollY - scrollPos));
+ info("window.scrollY " + window.scrollY);
+
+ // Check that second double tap does not scroll up
+ // Intentionally miss the target and hit the large spacer div, which
+ // should cause us to zoom out, but not scroll up (too much).
+ info("sending second double tap");
+ await doubleTapOn(target, -10, 15, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "After double-tap the resolution decreased to " + resolution);
+
+ // These values were determined experimentally, have not investigated the
+ // reason for the difference between platforms or the large tolerance needed.
+ let amountToExpectScrollUp = 0;
+ if (getPlatform() == "android") {
+ amountToExpectScrollUp = 767;
+ tolerance = 4.6;
+ }
+ scrollPos -= amountToExpectScrollUp;
+
+ ok(window.scrollY > window.innerHeight * 3.5 - tolerance - amountToExpectScrollUp, "window is still scrolled down");
+ ok(Math.abs(window.scrollY - scrollPos) < tolerance, "window didnt scroll: " + Math.abs(window.scrollY - scrollPos));
+ info("window.scrollY " + window.scrollY);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ .spacer {
+ background-color: #eee;
+ height: 800vh;
+ width: 1600vw;
+ }
+ .rect {
+ position: absolute;
+ width: 30px;
+ height: 30px;
+ background-color: #aaa;
+ top: 400vh;
+ right: 0;
+ }
+</style>
+</head>
+<body>
+<div id="target" class="rect">
+</div>
+
+<div class="spacer">
+</div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_htmlelement.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_htmlelement.html
new file mode 100644
index 0000000000..8fadc4eb3e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_htmlelement.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping on a scrollbar does not scroll to top</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ var resolution = await getResolution();
+ var start_resolution = resolution;
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(document.getElementById("target"), 10, 10, useTouchpad);
+ var prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // instant scroll to the middle of the page
+ window.scrollTo({
+ top: 4 * window.innerHeight,
+ left: 0,
+ behavior: 'auto'
+ });
+
+ await promiseApzFlushedRepaints();
+
+ let prevScrollX = window.scrollX;
+ let prevScrollY = window.scrollY;
+
+ ok(3.9 * window.innerHeight < window.scrollY && window.scrollY < 4.1 * window.innerHeight,
+ "scrollY looks good");
+
+ // Check that double-tapping on the bottom scrollbar does not scroll us to the top
+ // Need to divide by resolution because the coords are assumed to be inside the resolution
+ await doubleTapOn(window, (window.innerWidth/2)/resolution, (window.innerHeight - 5)/resolution, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+ ok(resolution == start_resolution, "The second double-tap has decreased the resolution to the start to " + resolution);
+
+ info("prevscroll " + prevScrollX + " " + prevScrollY + "\n");
+ info("window.scroll " + window.scrollX + " " + window.scrollY + "\n");
+
+ ok(0.88*prevScrollY < window.scrollY && window.scrollY < prevScrollY*1.12, "scroll y didn't change by much");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style type="text/css">
+ .spacer {
+ background-color: #eee;
+ width: 10px;
+ height: 800vh;
+ }
+</style>
+</head>
+<body>
+
+<div id="target" style="width: 100px; height: 100px; background: red;">
+</div>
+<div class="spacer">
+</div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_img.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_img.html
new file mode 100644
index 0000000000..2559a3dd23
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_img.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping img works</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ var resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(document.getElementById("target"), 10, 10, useTouchpad);
+ var prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // Check that double-tapping again on the same spot zooms out
+ await doubleTapOn(document.getElementById("target"), 10, 10, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+
+<img id="target" width="100" height="100" src="green100x100.png">
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_large_overflow.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_large_overflow.html
new file mode 100644
index 0000000000..02c4ca52f8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_large_overflow.html
@@ -0,0 +1,300 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping on overflow centers the zoom where we double tap</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ var resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // instant scroll to the bottom of the page
+ window.scrollTo({
+ top: 10000,
+ left: 0,
+ behavior: 'auto'
+ });
+
+ await promiseApzFlushedRepaints();
+
+ let scrollPos = window.scrollY;
+ ok(scrollPos > 1500, "window scrolled down");
+
+ info("window.scrollY " + window.scrollY);
+
+ info("window.innerHeight " + window.innerHeight);
+
+ info("document.documentElement.scrollHeight " + document.documentElement.scrollHeight);
+
+ let target = document.getElementById("target");
+
+ let x = 10;
+ let y = document.documentElement.scrollHeight - 60;
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(target, x, y, useTouchpad);
+ var prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ ok(window.scrollY > 1500, "window is still scrolled down");
+ ok(window.scrollY >= scrollPos-2, "window is still scrolled down");
+ info("window.scrollY " + window.scrollY);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ .container {
+ background-color: #eee;
+ width: 200px;
+ height: 50px;
+ border: 1px dotted black;
+ overflow: visible;
+ }
+</style>
+</head>
+<body>
+
+<div id="target" class="container">
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+
+</div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_noscroll.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_noscroll.html
new file mode 100644
index 0000000000..00e3638ba7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_noscroll.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping something tall that we are already zoomed to doesn't scroll (it zooms out)</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+function within(a, b, tolerance) {
+ return (Math.abs(a-b) < tolerance);
+}
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ let initial_resolution = resolution;
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ ok(window.scrollY == 0, "window not scrolled");
+ info("window.scrollY " + window.scrollY);
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(document.getElementById("target"), 10, 10, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+ ok(window.scrollY == 0, "window not scrolled");
+ info("window.scrollY " + window.scrollY);
+
+ let x = document.getElementById("target").getBoundingClientRect().width/2;
+ let y = window.visualViewport.height - 20;
+ // Check that near the bottom doesn't scroll but zooms out
+ await doubleTapOn(document.getElementById("target"), x, y, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+ // slight float inaccuracy, not sure why
+ ok(within(resolution, initial_resolution, 0.0002), "The second double-tap has restored the resolution to " + resolution);
+ ok(window.scrollY == 0, "window not scrolled");
+ info("window.scrollY " + window.scrollY);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+
+<div id="target" style="background: grey; width: 50vw; height: 300vh;">
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing.html
new file mode 100644
index 0000000000..12005b3552
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping when zoomed out and there is nothing to zoom to zooms in</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ let initial_resolution = resolution;
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(document.getElementById("thebody"), 20, 20, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // Check that double-tapping again on the same spot zooms out
+ await doubleTapOn(document.getElementById("thebody"), 20, 20, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+ ok(resolution == initial_resolution, "The second double-tap has decreased the resolution to the start resolution " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+<style>
+ body, html {margin: 0; width: 100%; height: 100%;}
+</style>
+</head>
+<body id="thebody">
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing_listener.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing_listener.html
new file mode 100644
index 0000000000..82805fe321
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing_listener.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping when zoomed out and there is nothing to zoom to does not zoom in if this is a non-passive wheel listener</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+function aListener() {
+ info("aListener called");
+ preventDefault();
+}
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ document.getElementById("thebody").addEventListener('wheel', aListener, {passive: false});
+ await promiseApzFlushedRepaints();
+
+ // Check that double-tapping does not zoom in
+ info("sending first double tap");
+ await doubleTapOn(document.getElementById("thebody"), 20, 20, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "The first double-tap did not change the resolution: " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+<style>
+ body, html {margin: 0; width: 100%; height: 100%;}
+</style>
+</head>
+<body id="thebody">
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif-2.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif-2.html
new file mode 100644
index 0000000000..1eda605376
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif-2.html
@@ -0,0 +1,128 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100,initial-scale=0.4"/>
+ <title>Tests that double tap to zoom in iframe works regardless whether cross-origin or not</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html {
+ /* To avoid bug 1865573 */
+ scrollbar-width: none;
+ }
+ iframe {
+ position: absolute;
+ top: 100px;
+ left: 100px;
+ border: none;
+ }
+ </style>
+
+ <script>
+
+async function setupIframe(aURL) {
+ const iframe = document.querySelector("iframe");
+ const iframeLoadPromise = promiseOneEvent(iframe, "load", null);
+ iframe.src = aURL;
+ await iframeLoadPromise;
+ info(`${aURL} loaded`);
+
+ await SpecialPowers.spawn(iframe, [], async () => {
+ await content.wrappedJSObject.waitUntilApzStable();
+ await SpecialPowers.contentTransformsReceived(content);
+ });
+}
+
+async function targetElementPosition() {
+ const iframe = document.querySelector("iframe");
+ return SpecialPowers.spawn(iframe, [], async () => {
+ return content.document.querySelector("#target").getBoundingClientRect();
+ });
+}
+
+const useTouchpad = (location.search == "?touchpad");
+
+async function test(aTestFile) {
+ let iframeURL = SimpleTest.getTestFileURL(aTestFile);
+
+ // Load the test document in the same origin.
+ await setupIframe(iframeURL);
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+ let initial_resolution = resolution;
+
+ let pos = await targetElementPosition();
+ const iframe = document.querySelector("iframe");
+ await doubleTapOn(iframe, pos.x + 10, pos.y + 10, useTouchpad);
+
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ let zoomedInState = cloneVisualViewport();
+
+ await doubleTapOn(iframe, pos.x + 10, pos.y + 10, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+
+ let zoomedOutState = cloneVisualViewport();
+
+ // Reset the scale to the initial one since in the `visible: overflow` case
+ // the second double-tap doesn't restore to the initial scale.
+ SpecialPowers.DOMWindowUtils.setResolutionAndScaleTo(initial_resolution);
+ await promiseApzFlushedRepaints();
+
+ // Now load the document in an OOP iframe.
+ iframeURL = iframeURL.replace(window.location.origin, "https://example.com");
+ await setupIframe(iframeURL);
+
+ pos = await targetElementPosition();
+ await doubleTapOn(iframe, pos.x + 10, pos.y + 10, useTouchpad);
+
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ compareVisualViewport(zoomedInState, cloneVisualViewport(), "zoomed-in state");
+
+ await doubleTapOn(iframe, pos.x + 10, pos.y + 10, useTouchpad);
+ compareVisualViewport(zoomedOutState, cloneVisualViewport(), "zoomed-out state");
+}
+
+async function moveIframe() {
+ const iframe = document.querySelector("iframe");
+ iframe.style.top = "500vh";
+
+ // Scroll to the bottom to make the layout scroll offset non-zero.
+ window.scrollTo(0, document.documentElement.scrollHeight);
+ ok(window.scrollY > 0, "The root scroll position should be non-zero");
+
+ await SpecialPowers.spawn(iframe, [], async () => {
+ await SpecialPowers.contentTransformsReceived(content);
+ });
+}
+
+waitUntilApzStable()
+.then(async () => test("helper_doubletap_zoom_oopif_subframe-1.html"))
+// A test case where the layout scroll offset isn't zero.
+.then(async () => moveIframe())
+.then(async () => test("helper_doubletap_zoom_oopif_subframe-1.html"))
+// A test case where the layout scroll offset in the iframe isn't zero.
+.then(async () => test("helper_doubletap_zoom_oopif_subframe-2.html#target"))
+// A test case where the double-tap-to-zoom target element is `overflow: visible`.
+.then(async () => test("helper_doubletap_zoom_oopif_subframe-3.html#target"))
+// Similar to above but the target element has positive `margin-top`.
+.then(async () => test("helper_doubletap_zoom_oopif_subframe-4.html#target"))
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+<iframe width="500" height="500"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif.html
new file mode 100644
index 0000000000..b6a51e8229
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping inside an oop iframe works</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(document.getElementById("target"), 20, 20, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // Check that double-tapping again on the same spot zooms out
+ await doubleTapOn(document.getElementById("target"), 20, 20, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+<style>
+iframe {
+ margin: 0;
+ padding: 0;
+ border: 1px solid black;
+}
+</style>
+</head>
+<body>
+
+<iframe id="target" width="100" height="100" src="http://example.org/"></iframe>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif_subframe-1.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif_subframe-1.html
new file mode 100644
index 0000000000..19da59c594
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif_subframe-1.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+html, body {
+ margin: 0;
+ padding: 0;
+}
+</style>
+<div id="target" style="width: 100px; height: 100px; position: absolute; top: 300px; left: 100px; background: blue;"></div>
+<script>
+ // Silence SimpleTest warning about missing assertions by having it wait
+ // indefinitely. We don't need to give it an explicit finish because the
+ // entire window this test runs in will be closed after the main browser test
+ // finished.
+ SimpleTest.waitForExplicitFinish();
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif_subframe-2.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif_subframe-2.html
new file mode 100644
index 0000000000..f2550e9524
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif_subframe-2.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+html, body {
+ margin: 0;
+ padding: 0;
+}
+</style>
+<div id="target" style="width: 100px; height: 100px; position: absolute; top: 300vh; left: 100px; background: blue;"></div>
+<script>
+ // Silence SimpleTest warning about missing assertions by having it wait
+ // indefinitely. We don't need to give it an explicit finish because the
+ // entire window this test runs in will be closed after the main browser test
+ // finished.
+ SimpleTest.waitForExplicitFinish();
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif_subframe-3.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif_subframe-3.html
new file mode 100644
index 0000000000..30b3ca930c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif_subframe-3.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+.container {
+ background-color: #eee;
+ width: 200px;
+ height: 50px;
+ border: 1px dotted black;
+ overflow: visible;
+}
+</style>
+<div class="container"></div>
+<script>
+ let text = "";
+ for (let i = 0; i < 200; i++) {
+ text += "Text text text text text text text text text text text text text text text text\n";
+ }
+ text += '<div id="target" style="width: 100%; height: 10px; background-color: red; position: relative; top: -20px; z-index: -1"></div>';
+ document.querySelector(".container").innerHTML = text;
+
+ // Silence SimpleTest warning about missing assertions by having it wait
+ // indefinitely. We don't need to give it an explicit finish because the
+ // entire window this test runs in will be closed after the main browser test
+ // finished.
+ SimpleTest.waitForExplicitFinish();
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif_subframe-4.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif_subframe-4.html
new file mode 100644
index 0000000000..b98f48450e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif_subframe-4.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+.container {
+ background-color: #eee;
+ width: 200px;
+ height: 50px;
+ border: 1px dotted black;
+ overflow: visible;
+ margin-top: 200px;
+}
+</style>
+<div class="container"></div>
+<script>
+ let text = '<div id="target" style="width: 100%; height: 10px; background-color: red; position: relative; top: 60px; z-index: -1"></div>';
+ for (let i = 0; i < 200; i++) {
+ text += "Text text text text text text text text text text text text text text text text\n";
+ }
+ document.querySelector(".container").innerHTML = text;
+
+ // Silence SimpleTest warning about missing assertions by having it wait
+ // indefinitely. We don't need to give it an explicit finish because the
+ // entire window this test runs in will be closed after the main browser test
+ // finished.
+ SimpleTest.waitForExplicitFinish();
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_scrolled_overflowhidden.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_scrolled_overflowhidden.html
new file mode 100644
index 0000000000..bba8eeec08
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_scrolled_overflowhidden.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping when the page is overflow hidden and has been scrolled down by js works</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ var resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // instant scroll down
+ window.scrollTo({
+ top: window.innerHeight * 2 - 50,
+ left: 0,
+ behavior: 'auto'
+ });
+
+ await promiseApzFlushedRepaints();
+
+ let scrollPos = window.scrollY;
+ ok(scrollPos > window.innerHeight + 100, "window scrolled down");
+
+ info("window.scrollY " + window.scrollY);
+
+ info("window.innerHeight " + window.innerHeight);
+
+ info("document.documentElement.scrollHeight " + document.documentElement.scrollHeight);
+
+ let target = document.getElementById("target");
+
+ // Check that double-tapping once zooms in
+ info("sending double tap");
+ await doubleTapOn(target, 10, 10, useTouchpad);
+ var prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ ok(window.scrollY > window.innerHeight + 100, "window is still scrolled down");
+ ok(Math.abs(window.scrollY - scrollPos) < 2, "window didnt scroll");
+ info("window.scrollY " + window.scrollY);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ html, body {
+ overflow: hidden;
+ }
+ .spacer {
+ background-color: #eee;
+
+ height: 200vh;
+ }
+ .rect {
+ background-color: #aaa;
+ width: 100px;
+ height: 100px;
+ }
+</style>
+</head>
+<body>
+
+<div class="spacer">
+</div>
+<div id="target" class="rect">
+</div>
+<div class="spacer">
+</div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_shadowdom.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_shadowdom.html
new file mode 100644
index 0000000000..eed45028e2
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_shadowdom.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping shadow dom works</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function attach() {
+ let attachpoint = document.getElementById('attachpoint');
+ attachpoint.attachShadow({mode: 'open'}).innerHTML = "<span>some text</span>";
+
+ // flush layout
+ attachpoint.getBoundingClientRect();
+ await promiseApzFlushedRepaints();
+}
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ await attach();
+
+ var resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once zooms in
+ // This will hit the span inside the shadow dom, inline elements are not
+ // suitable zoom targets, so we will walk up, if you fail to walk out of
+ // the shadow tree we won't get an element and fail to zoom. If we succeed
+ // we'll hit the div with id target and zoom.
+ info("sending first double tap");
+ await doubleTapOn(document.getElementById("target"), 10, 10, useTouchpad);
+ var prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // Check that double-tapping again on the same spot zooms out
+ info("sending second double tap");
+ await doubleTapOn(document.getElementById("target"), 10, 10, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ .outer {
+ width: 200px;
+ height: 200px;
+ background: yellow;
+ }
+ </style>
+</head>
+<body>
+
+<div id="target" class="outer">
+ <div id="attachpoint"></div>
+</div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_small.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_small.html
new file mode 100644
index 0000000000..1a2a52aff8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_small.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping a small element works</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ var resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(document.getElementById("target"), 1, 1, useTouchpad);
+ var prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // Check that double-tapping again on the same spot zooms out
+ await doubleTapOn(document.getElementById("target"), 1, 1, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+
+<div id="target" style="background: blue; width: 3px; height: 3px;"></div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_smooth.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_smooth.html
new file mode 100644
index 0000000000..65b4c6698f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_smooth.html
@@ -0,0 +1,161 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping zoom out animation is smooth</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+let hasPrev = false;
+let zoomingIn = true;
+let lastVVLeft = 0, lastVVTop = 0, lastVVWidth = 0, lastVVHeight = 0;
+let lastScrollX = 0, lastScrollY = 0;
+let lastResolution = 0;
+
+function within(a, b, tolerance) {
+ return (Math.abs(a-b) < tolerance);
+}
+
+async function afterpaint() {
+ info("vv pos " + visualViewport.pageLeft + "," + visualViewport.pageTop);
+ info("vv size " + visualViewport.width + "," + visualViewport.height);
+ info("win scroll " + window.scrollX + "," + window.scrollY);
+ info("res " + await getResolution());
+ if (hasPrev) {
+ ok(zoomingIn ?
+ lastVVLeft <= visualViewport.pageLeft :
+ lastVVLeft >= visualViewport.pageLeft,
+ "vvleft monotonic");
+ // When zooming in pageTop stays 0, when zooming out (at least on mac)
+ // the final value of pageTop is 2.5. Hence why the direction of these
+ // inequalities.
+ ok(zoomingIn ?
+ lastVVTop >= visualViewport.pageTop :
+ lastVVTop <= visualViewport.pageTop,
+ "vvtop monotonic");
+ ok(zoomingIn ?
+ lastVVWidth >= visualViewport.width :
+ lastVVWidth <= visualViewport.width,
+ "vvwidth monotonic");
+ ok(zoomingIn ?
+ lastVVHeight >= visualViewport.height :
+ lastVVHeight <= visualViewport.height,
+ "vvheight monotonic");
+ ok(zoomingIn ?
+ lastScrollX <= window.scrollX :
+ lastScrollX >= window.scrollX,
+ "scrollx monotonic");
+ if (!within(lastScrollY, window.scrollY, 2)) {
+ ok(zoomingIn ?
+ lastScrollY >= window.scrollY :
+ lastScrollY <= window.scrollY,
+ "scrolly monotonic");
+ }
+
+ // At the upper and lower limits of zoom constraints we can briefly go over
+ // the limit and then back because of floating point inaccuracies.
+ // In apzc callback helper we set the new content resolution to
+ // (last presshell resolution that apz knows about) * (apz async zoom)
+ // and (apz async zoom) is calculated as (apz zoom) / (last content resolution that apz knows about)
+ // and (last content resolution that apz knows about) = (dev pixels per css pixel) * (ps resolution) * (resolution induced by transforms)
+ // For simplicity we can assume that (dev pixels per css pixel) == (resolution induced by transforms) == 1
+ // and (ps resolution) == (last presshell resolution that apz knows about).
+ // The calculation then boils down to
+ // (ps resolution) * ((apz zoom) / (ps resolution))
+ // The fact that we divide by (ps resolution) first, and then multiply by
+ // it means the result is not quite equal to (apz zoom).
+ const deviceScale = window.devicePixelRatio;
+ const maxZoom = 10.0;
+ if (!within(lastResolution, maxZoom/deviceScale, 0.0001) &&
+ !within(await getResolution(), maxZoom/deviceScale, 0.0001) &&
+ !within(lastResolution, 1, 0.0001) &&
+ !within(await getResolution(), 1, 0.0001)) {
+ ok(zoomingIn ?
+ lastResolution <= await getResolution() :
+ lastResolution >= await getResolution(),
+ "resolution monotonic");
+ }
+
+ } else {
+ hasPrev = true;
+ }
+ lastVVLeft = visualViewport.pageLeft;
+ lastVVTop = visualViewport.pageTop;
+ lastVVWidth = visualViewport.width;
+ lastVVHeight = visualViewport.height;
+ lastScrollX = window.scrollX;
+ if (!within(lastScrollY, window.scrollY, 2)) {
+ lastScrollY = window.scrollY;
+ }
+ lastResolution = await getResolution();
+}
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ info("dpi: " + window.devicePixelRatio);
+
+ window.addEventListener("MozAfterPaint", afterpaint);
+ let intervalID = setInterval(afterpaint, 16);
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once on a small element zooms in
+ info("sending first double tap");
+ await doubleTapOn(document.getElementById("target2"), 10, 10, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ await promiseApzFlushedRepaints();
+
+ hasPrev = false;
+ zoomingIn = false;
+
+ // Double tap, this won't hit anything but the body/html, and so will zoom us out.
+ info("sending second double tap");
+ await doubleTapOn(document.getElementById("target2"), 5, 65, useTouchpad);
+ await promiseApzFlushedRepaints();
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+
+ clearInterval(intervalID);
+ window.removeEventListener("MozAfterPaint", afterpaint);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ .container {
+ background-color: #eee;
+ width: 95vw;
+ height: 400vh;
+ }
+ .smallcontainer {
+ background-color: #aaa;
+ width: 40px;
+ height: 40px;
+ position: absolute;
+ right: 0;
+ top: 20px;
+ }
+</style>
+</head>
+<body>
+
+<div id="target" class="container">
+</div>
+<div id="target2" class="smallcontainer">
+</div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_square.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_square.html
new file mode 100644
index 0000000000..c8278093af
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_square.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping on a square img doesn't cut off parts of the image</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ let initial_resolution = resolution;
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ let target = document.getElementById("target");
+
+ // Check that double-tapping once on the element zooms in
+ info("sending first double tap");
+ await doubleTapOn(target, 20, 20, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ let rect = target.getBoundingClientRect();
+ ok(visualViewport.pageLeft < rect.x, "left");
+ ok(visualViewport.pageTop < rect.y, "top");
+ ok(visualViewport.pageLeft + visualViewport.width > rect.x + rect.width, "right");
+ ok(visualViewport.pageTop + visualViewport.height > rect.y + rect.height, "bottom");
+
+ // Check that double-tapping the second time on the element zooms out
+ info("sending second double tap");
+ await doubleTapOn(target, 20, 20, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+ ok(resolution == initial_resolution, "The second double-tap has restored the resolution to " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ .bigsquare {
+ width: 40vh;
+ height: 40vh;
+ }
+</style>
+</head>
+<body>
+
+<img id="target" class="bigsquare" src="green100x100.png">
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tablecell.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tablecell.html
new file mode 100644
index 0000000000..f578ccc592
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tablecell.html
@@ -0,0 +1,110 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping small table cells does not zoom</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping does not zoom in
+ info("sending first double tap");
+ await doubleTapOn(document.getElementById("target1"), 10, 10, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "The first double-tap did not change the resolution: " + resolution);
+
+ // Check that double-tapping does not zoom in
+ info("sending second double tap");
+ await doubleTapOn(document.getElementById("target2"), 10, 10, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "The second double-tap did not change the resolution: " + resolution);
+
+ // Check that double-tapping does zoom in
+ info("sending third double tap");
+ await doubleTapOn(document.getElementById("target3"), 10, 10, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The third double-tap has increased the resolution to " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ table {
+ width: 100%;
+ }
+ table,
+ td {
+ border: 1px solid #333;
+ }
+ td {
+ height: 25px;
+ }
+ .small {
+ width: 15vw;
+ }
+ .big {
+ width: 50vw;
+ }
+ </style>
+</head>
+<body>
+<table>
+ <thead>
+ <tr>
+ <th colspan="2">The table header</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td class="small"><div id="target1" class="small">The table body</div></td>
+ <td>with two columns</td>
+ </tr>
+ </tbody>
+</table>
+
+<table>
+ <thead>
+ <tr>
+ <th colspan="2">The table header</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td class="small"><div class="small"><div id="target2" class="small">The table body</div></div></td>
+ <td>with two columns</td>
+ </tr>
+ </tbody>
+</table>
+
+<table>
+ <thead>
+ <tr>
+ <th colspan="2">The table header</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td id="target3" class="big">The table body</td>
+ <td>with two columns</td>
+ </tr>
+ </tbody>
+</table>
+
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tallwide.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tallwide.html
new file mode 100644
index 0000000000..438c63b0b0
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tallwide.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping on a tall element that is >90% width of viewport doesn't scroll to the top of it when scrolled down</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ var resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once on a small element zooms in
+ await doubleTapOn(document.getElementById("target2"), 10, 10, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // instant scroll to the bottom of the page
+ window.scrollTo({
+ top: 40000,
+ left: 0,
+ behavior: 'auto'
+ });
+
+ await promiseApzFlushedRepaints();
+
+ let scrollPos = window.scrollY;
+ ok(scrollPos > 1500, "window scrolled down (1)");
+ ok(scrollPos > window.innerHeight * 2, "window scrolled down (2)")
+
+ info("window.scrollY " + window.scrollY);
+ info("window.innerHeight " + window.innerHeight);
+ info("visualViewport.pageTop " + visualViewport.pageTop);
+
+ let target = document.getElementById("target");
+
+ let x = 20;
+ let y = visualViewport.pageTop + 20;
+
+ // Check that double-tapping on the big element zooms out
+ await doubleTapOn(target, x, y, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+
+ info("scrollPos " + scrollPos);
+ info("window.scrollY " + window.scrollY);
+ ok(window.scrollY > 1500, "window is still scrolled down (1)");
+ ok(window.scrollY >= scrollPos - window.innerHeight, "window is still scrolled down (2)");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ .container {
+ background-color: #eee;
+ width: 95vw;
+ height: 400vh;
+ }
+ .smallcontainer {
+ background-color: #aaa;
+ width: 40px;
+ height: 40px;
+ }
+</style>
+</head>
+<body>
+
+<div id="target" class="container">
+ <div id="target2" class="smallcontainer">
+ </div>
+</div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_textarea.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_textarea.html
new file mode 100644
index 0000000000..99616d9834
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_textarea.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping textarea works</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ var resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(document.getElementById("target"), 10, 10, useTouchpad);
+ var prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // Check that double-tapping again on the same spot zooms out
+ await doubleTapOn(document.getElementById("target"), 10, 10, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+
+<textarea id="target" rows="10" cols="30">The cat was playing in the garden.</textarea>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_touch_action_manipulation.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_touch_action_manipulation.html
new file mode 100644
index 0000000000..20a8bfc113
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_touch_action_manipulation.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Tests that double tap to zoom doesn't work on touch-action: manipulation element</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ var resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // A double tap on the touch-action: manipulation element.
+ await synthesizeDoubleTap(document.getElementById("target"), 20, 20, useTouchpad);
+
+ for (let i = 0; i < 10; i++) {
+ await promiseFrame();
+ }
+
+ // Flush state just in case.
+ await promiseApzFlushedRepaints();
+
+ var prev_resolution = resolution;
+ resolution = await getResolution();
+ if (!useTouchpad) {
+ is(resolution, prev_resolution, "No zoom should happen on touchscreen");
+ } else {
+ isnot(resolution, prev_resolution, "Zoom should happen on touchpad");
+ }
+
+ // Send another tap event outside of the area where
+ // `touch-action: manipulation` is specified so that it will create a new
+ // touch block to avoid bug 1848062 on Mac.
+ await synthesizeNativeTap(target, 200, 200);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+
+<div id="target" style="width: 100px; height: 100px; touch-action: manipulation;"></div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_touch_action_manipulation_in_iframe.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_touch_action_manipulation_in_iframe.html
new file mode 100644
index 0000000000..0c1eb5dd33
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_touch_action_manipulation_in_iframe.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100,initial-scale=0.4"/>
+ <title>Tests that double tap to zoom doesn't work on touch-action: manipulation element in an iframe</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ iframe {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ border: none;
+ }
+ </style>
+
+ <script>
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+ let iframeURL =
+ SimpleTest.getTestFileURL("helper_doubletap_zoom_touch_action_manipulation_subframe.html")
+ .replace(window.location.origin, "https://example.com/");
+
+ let iframe = document.querySelector("iframe");
+ const iframeLoadPromise = promiseOneEvent(iframe, "load", null);
+ iframe.src = iframeURL;
+ await iframeLoadPromise;
+
+ await SpecialPowers.spawn(iframe, [], async () => {
+ await content.wrappedJSObject.waitUntilApzStable();
+ });
+
+ var resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Activate the root scroller in the iframe, otherwise the next touch event
+ // will be handled by the root content APZC incorrectly on non-Fission
+ // environments.
+ await SpecialPowers.spawn(iframe, [], async () => {
+ await SpecialPowers.DOMWindowUtils.setDisplayPortForElement(
+ 0, 0, 500, 500, content.document.documentElement, 1);
+ await content.wrappedJSObject.promiseApzFlushedRepaints();
+ });
+
+ await SpecialPowers.spawn(iframe, [useTouchpad], async (aUseTouchpad) => {
+ await content.wrappedJSObject.synthesizeDoubleTap(
+ content.document.querySelector("div"), 20, 20, aUseTouchpad);
+ });
+
+ for (let i = 0; i < 10; i++) {
+ await promiseFrame();
+ }
+
+ // Flush state just in case.
+ await promiseApzFlushedRepaints();
+
+ var prev_resolution = resolution;
+ resolution = await getResolution();
+ if (!useTouchpad) {
+ is(resolution, prev_resolution, "No zoom should happen on touchscreen");
+ } else {
+ isnot(resolution, prev_resolution, "Zoom should happen on touchpad");
+ }
+
+ // Send another tap event outside of the area where
+ // `touch-action: manipulation` is specified so that it will create a new
+ // touch block to avoid bug 1848062 on Mac.
+ await synthesizeNativeTap(iframe, 600, 600);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+<iframe width="500" height="500"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_touch_action_manipulation_subframe.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_touch_action_manipulation_subframe.html
new file mode 100644
index 0000000000..959994d156
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_touch_action_manipulation_subframe.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+html, body {
+ margin: 0;
+ padding: 0;
+}
+</style>
+<div style="width: 1000px; height: 1000px; position: absolute; top: 0px; left: 0px; touch-action: manipulation;"></div>
+<script>
+ // Silence SimpleTest warning about missing assertions by having it wait
+ // indefinitely. We don't need to give it an explicit finish because the
+ // entire window this test runs in will be closed after the main browser test
+ // finished.
+ SimpleTest.waitForExplicitFinish();
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_drag_bug1719913.html b/gfx/layers/apz/test/mochitest/helper_drag_bug1719913.html
new file mode 100644
index 0000000000..36192aed6d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_drag_bug1719913.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for bug 1719913</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="text/javascript">
+
+async function test() {
+ var subframe = document.getElementById("scroller");
+ let scrollPromise = new Promise(resolve => {
+ subframe.addEventListener("scroll", resolve, {once: true});
+ });
+
+ // Scroll down a small amount (5px). The bug in this case is that the
+ // scrollthumb "jumps" further down the scroll track because with the bug,
+ // incorrect location of a transform in the WebRender scroll nodes results
+ // in a miscalculation of the scroll position corresponding to a mouse event
+ // position during dragging.
+ var dragFinisher = await promiseVerticalScrollbarDrag(subframe, 5, 5);
+ if (!dragFinisher) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ // the events above might be stuck in APZ input queue for a bit until the
+ // layer is activated, so we wait here until the scroll event listener is
+ // triggered.
+ await scrollPromise;
+
+ await dragFinisher();
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ // The expected scroll position from the 5px of dragging, based on local
+ // testing, is 49px. With the bug, it's 1038px. We check that it's < 100
+ // which should rule out the bug while allowing for minor variations in
+ // scrollbar sizing etc.
+ ok(subframe.scrollTop < 100, "Scrollbar drag resulted in a scroll position of " + subframe.scrollTop);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ .columns {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ overflow: auto;
+ }
+ .column {
+ position: relative;
+ width: 50%;
+ height: 500px;
+ }
+ .header {
+ height: 100px;
+ }
+ .scroller {
+ overflow: auto;
+ will-change: transform;
+ height: 100%;
+ }
+ .content {
+ height: 5000px;
+ width: 100%;
+ background: linear-gradient(green, blue);
+ }
+ </style>
+</head>
+<body>
+ <div class="columns">
+ <div class="column">
+ <div class="header"></div>
+ <div class="scroller" id="scroller">
+ <div class="content"></div>
+ </div>
+ </div>
+ <div class="column"></div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_drag_bug1794590.html b/gfx/layers/apz/test/mochitest/helper_drag_bug1794590.html
new file mode 100644
index 0000000000..71fd365a84
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_drag_bug1794590.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for bug 1794590</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="text/javascript">
+
+ const utils = SpecialPowers.getDOMWindowUtils(window);
+
+ async function test() {
+ // Zoom in. This is part of the bug 1794590 STR.
+ let resolution = 3.0;
+ utils.setResolutionAndScaleTo(resolution);
+ await promiseApzFlushedRepaints();
+
+ let scrollPromise = new Promise(resolve => {
+ subframe.addEventListener("scroll", resolve, { once: true });
+ });
+
+ var dragFinisher = await promiseVerticalScrollbarDrag(subframe, 20);
+ if (!dragFinisher) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ // the events above might be stuck in APZ input queue for a bit until the
+ // layer is activated, so we wait here until the scroll event listener is
+ // triggered.
+ await scrollPromise;
+
+ await dragFinisher();
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ ok(subframe.scrollTop > 0, "Scrollbar drag resulted in a scroll position of " + subframe.scrollTop);
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ #subframe {
+ width: 200px;
+ height: 100px;
+ margin: 100px;
+ background-color: cyan;
+ overflow: scroll;
+ white-space: pre;
+ }
+ #content {
+ width: 200px;
+ height: 200px;
+ background: linear-gradient(green, blue);
+ }
+ </style>
+</head>
+
+<body>
+ <div id="subframe">
+ <div id="content"></div>>
+ </div>
+</body>
+
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_drag_bug1827330.html b/gfx/layers/apz/test/mochitest/helper_drag_bug1827330.html
new file mode 100644
index 0000000000..dc0265f971
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_drag_bug1827330.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for bug 1827330</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ #scrollbox {
+ overflow: scroll;
+ width: 300px;
+ height: 300px;
+ }
+ .spacer {
+ height: 200%;
+ } </style>
+</head>
+<body>
+ <div id="scrollbox">
+ <iframe src="https://example.com" width="200" height="200"></iframe>
+ <div class="spacer"></div>
+ </div>
+ <script type="text/javascript">
+
+ async function test() {
+ const utils = SpecialPowers.getDOMWindowUtils(window);
+
+ // Zoom in.
+ let resolution = 3.0;
+ utils.setResolutionAndScaleTo(resolution);
+ await promiseApzFlushedRepaints();
+
+ var subframe = document.getElementById("scrollbox");
+ let scrollPromise = new Promise(resolve => {
+ subframe.addEventListener("scroll", resolve, {once: true});
+ });
+
+ // Scroll down a small amount (5px). The bug in this case is that the
+ // scroll thumb does not start moving until the mouse has already moved
+ // by some distance, so if the bug occurs, the first 5px of mouse movement
+ // will cause no scrolling.
+ var dragFinisher = await promiseVerticalScrollbarDrag(subframe, 5, 5);
+ if (!dragFinisher) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ // the events above might be stuck in APZ input queue for a bit until the
+ // layer is activated, so we wait here until the scroll event listener is
+ // triggered.
+ await scrollPromise;
+
+ await dragFinisher();
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ // Check that we've scrolled at all.
+ ok(subframe.scrollTop > 0, "Scrollbar drag resulted in a scroll position of " + subframe.scrollTop);
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_drag_click.html b/gfx/layers/apz/test/mochitest/helper_drag_click.html
new file mode 100644
index 0000000000..01722c798d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_drag_click.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity mouse-drag click test</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let clickPromise = new Promise(resolve => {
+ document.addEventListener("click", resolve);
+ });
+
+ // Ensure the pointer is inside the window
+ await promiseNativeMouseEventWithAPZ({
+ target: document.getElementById("b"),
+ offsetX: 5,
+ offsetY: 5,
+ type: "mousemove",
+ });
+ // mouse down, move it around, and release it near where it went down. this
+ // should generate a click at the release point
+ await promiseNativeMouseEventWithAPZ({
+ target: document.getElementById("b"),
+ offsetX: 5,
+ offsetY: 5,
+ type: "mousedown",
+ });
+ await promiseNativeMouseEventWithAPZ({
+ target: document.getElementById("b"),
+ offsetX: 100,
+ offsetY: 100,
+ type: "mousemove",
+ });
+ await promiseNativeMouseEventWithAPZ({
+ target: document.getElementById("b"),
+ offsetX: 10,
+ offsetY: 10,
+ type: "mousemove",
+ });
+ await promiseNativeMouseEventWithAPZ({
+ target: document.getElementById("b"),
+ offsetX: 8,
+ offsetY: 8,
+ type: "mouseup",
+ });
+ dump("Finished synthesizing click with a drag in the middle\n");
+
+ let e = await clickPromise;
+ // The mouse down at (5, 5) should not have generated a click, but the up
+ // at (8, 8) should have.
+ is(e.target, document.getElementById("b"), "Clicked on button, yay! (at " + e.clientX + "," + e.clientY + ")");
+ is(e.clientX, 8 + Math.floor(document.getElementById("b").getBoundingClientRect().left), "x-coord of click event looks sane");
+ is(e.clientY, 8 + Math.floor(document.getElementById("b").getBoundingClientRect().top), "y-coord of click event looks sane");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <button id="b" style="width: 10px; height: 10px; padding: 0;"></button>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_drag_root_scrollbar.html b/gfx/layers/apz/test/mochitest/helper_drag_root_scrollbar.html
new file mode 100644
index 0000000000..1665e168cb
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_drag_root_scrollbar.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Dragging the mouse on the viewport's scrollbar</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ .content {
+ width: 1000px;
+ height: 5000px;
+ }
+ </style>
+ <script type="text/javascript">
+
+async function test() {
+ let scrollPromise = new Promise(resolve => {
+ window.addEventListener("scroll", resolve, {once: true});
+ });
+
+ // Do the scroll in one increment so that when the scroll event fires
+ // we're done all the scrolling we're going to do.
+ var dragFinisher = await promiseVerticalScrollbarDrag(window, 20, 20);
+ if (!dragFinisher) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ // the events above might be stuck in APZ input queue for a bit until the
+ // layer is activated, so we wait here until the scroll event listener is
+ // triggered.
+ await scrollPromise;
+
+ await dragFinisher();
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ // After dragging the scrollbar 20px on a 1000px-high viewport, we should
+ // have scrolled approx 2% of the 5000px high content. There might have been
+ // scroll arrows and such so let's just have a minimum bound of 50px to be safe.
+ ok(window.scrollY > 50, "Scrollbar drag resulted in a vertical scroll position of " + window.scrollY);
+
+ // Check that we did not get spurious horizontal scrolling, as we might if the
+ // drag gesture is mishandled by content as a select-drag rather than a scrollbar
+ // drag.
+ is(window.scrollX, 0, "Scrollbar drag resulted in a horizontal scroll position of " + window.scrollX);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div class="content">Some content to ensure the root scrollframe is scrollable</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_drag_scroll.html b/gfx/layers/apz/test/mochitest/helper_drag_scroll.html
new file mode 100644
index 0000000000..15ca680ad6
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_drag_scroll.html
@@ -0,0 +1,653 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Dragging the mouse on a content-implemented scrollbar</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ background: linear-gradient(135deg, red, blue);
+ }
+ #scrollbar {
+ position:fixed;
+ top: 0;
+ right: 10px;
+ height: 100%;
+ width: 150px;
+ background-color: gray;
+ }
+ </style>
+ <script type="text/javascript">
+var bar = null;
+var mouseDown = false;
+var mouseDownY = -1;
+
+async function moveTo(mouseY) {
+ var fraction = (mouseY - bar.getBoundingClientRect().top) / bar.getBoundingClientRect().height;
+ fraction = Math.max(0, fraction);
+ fraction = Math.min(1, fraction);
+ var oldScrollPos = document.scrollingElement.scrollTop;
+ var newScrollPos = fraction * window.scrollMaxY;
+ ok(newScrollPos > oldScrollPos, "Scroll position strictly increased");
+ // split the scroll in two with a paint in between, just to increase the
+ // complexity of the simulated web content, and to ensure this works as well.
+ document.scrollingElement.scrollTop = (oldScrollPos + newScrollPos) / 2;
+ await promiseAllPaintsDone();
+ document.scrollingElement.scrollTop = newScrollPos;
+}
+
+async function downMouseAndHandleEvent(x, y) {
+ let mouseDownHandledPromise = new Promise(resolve => {
+ bar.addEventListener("mousedown", async function(e) {
+ dump("Got mousedown clientY " + e.clientY + "\n");
+ mouseDown = true;
+ mouseDownY = e.clientY;
+ await moveTo(e.clientY);
+ resolve();
+ }, {capture: true, once: true});
+ });
+ await synthesizeNativeMouseEventWithAPZ({
+ target: bar,
+ offsetX: x,
+ offsetY: y,
+ type: "mousedown",
+ });
+ await mouseDownHandledPromise;
+}
+
+async function moveMouseAndHandleEvent(x, y) {
+ let mouseMoveHandledPromise = new Promise(resolve => {
+ async function mouseOnTarget(e) {
+ if (!mouseDown) {
+ return;
+ }
+ dump("Got mousemove clientY " + e.clientY + "\n");
+ e.stopPropagation();
+ if (e.clientY == mouseDownY) {
+ dump("Discarding spurious mousemove\n");
+ return;
+ }
+ await moveTo(e.clientY);
+ handled();
+ }
+
+ function mouseOffTarget(e) {
+ if (!mouseDown) {
+ return;
+ }
+ ok(false, "The mousemove at " + e.clientY + " was not stopped by the bar listener, and is a glitchy event!");
+ handled();
+ }
+
+ function handled() {
+ bar.removeEventListener("mousemove", mouseOnTarget, true);
+ window.removeEventListener("mousemove", mouseOffTarget);
+ resolve();
+ }
+
+ bar.addEventListener("mousemove", mouseOnTarget, true);
+ window.addEventListener("mousemove", mouseOffTarget);
+ });
+ await synthesizeNativeMouseEventWithAPZ({
+ target: bar,
+ offsetX: x,
+ offsetY: y,
+ type: "mousemove",
+ });
+ await mouseMoveHandledPromise;
+}
+
+async function test() {
+ bar = document.getElementById("scrollbar");
+ mouseDown = false;
+ mouseDownY = -1;
+
+ bar.addEventListener("mouseup", function(e) {
+ mouseDown = false;
+ dump("Got mouseup clientY " + e.clientY + "\n");
+ }, true);
+
+ // Move the mouse to the "scrollbar" (the div upon which dragging changes scroll position)
+ await promiseNativeMouseEventWithAPZ({
+ target: bar,
+ offsetX: 10,
+ offsetY: 10,
+ type: "mousemove",
+ });
+
+ // mouse down
+ await downMouseAndHandleEvent(10, 10);
+
+ // drag vertically by 400px, in 50px increments
+ await moveMouseAndHandleEvent(10, 60);
+ await moveMouseAndHandleEvent(10, 110);
+ await moveMouseAndHandleEvent(10, 160);
+ await moveMouseAndHandleEvent(10, 210);
+ await moveMouseAndHandleEvent(10, 260);
+ await moveMouseAndHandleEvent(10, 310);
+ await moveMouseAndHandleEvent(10, 360);
+ await moveMouseAndHandleEvent(10, 410);
+ // and release
+ await promiseNativeMouseEventWithAPZ({
+ target: bar,
+ offsetX: 10,
+ offsetY: 410,
+ type: "mouseup",
+ });
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+
+<div id="scrollbar">Drag up and down on this bar. The background/scrollbar shouldn't glitch</div>
+This is a tall page<br/>
+1<br/>
+2<br/>
+3<br/>
+4<br/>
+5<br/>
+6<br/>
+7<br/>
+8<br/>
+9<br/>
+10<br/>
+11<br/>
+12<br/>
+13<br/>
+14<br/>
+15<br/>
+16<br/>
+17<br/>
+18<br/>
+19<br/>
+20<br/>
+21<br/>
+22<br/>
+23<br/>
+24<br/>
+25<br/>
+26<br/>
+27<br/>
+28<br/>
+29<br/>
+30<br/>
+31<br/>
+32<br/>
+33<br/>
+34<br/>
+35<br/>
+36<br/>
+37<br/>
+38<br/>
+39<br/>
+40<br/>
+41<br/>
+42<br/>
+43<br/>
+44<br/>
+45<br/>
+46<br/>
+47<br/>
+48<br/>
+49<br/>
+50<br/>
+51<br/>
+52<br/>
+53<br/>
+54<br/>
+55<br/>
+56<br/>
+57<br/>
+58<br/>
+59<br/>
+60<br/>
+61<br/>
+62<br/>
+63<br/>
+64<br/>
+65<br/>
+66<br/>
+67<br/>
+68<br/>
+69<br/>
+70<br/>
+71<br/>
+72<br/>
+73<br/>
+74<br/>
+75<br/>
+76<br/>
+77<br/>
+78<br/>
+79<br/>
+80<br/>
+81<br/>
+82<br/>
+83<br/>
+84<br/>
+85<br/>
+86<br/>
+87<br/>
+88<br/>
+89<br/>
+90<br/>
+91<br/>
+92<br/>
+93<br/>
+94<br/>
+95<br/>
+96<br/>
+97<br/>
+98<br/>
+99<br/>
+100<br/>
+101<br/>
+102<br/>
+103<br/>
+104<br/>
+105<br/>
+106<br/>
+107<br/>
+108<br/>
+109<br/>
+110<br/>
+111<br/>
+112<br/>
+113<br/>
+114<br/>
+115<br/>
+116<br/>
+117<br/>
+118<br/>
+119<br/>
+120<br/>
+121<br/>
+122<br/>
+123<br/>
+124<br/>
+125<br/>
+126<br/>
+127<br/>
+128<br/>
+129<br/>
+130<br/>
+131<br/>
+132<br/>
+133<br/>
+134<br/>
+135<br/>
+136<br/>
+137<br/>
+138<br/>
+139<br/>
+140<br/>
+141<br/>
+142<br/>
+143<br/>
+144<br/>
+145<br/>
+146<br/>
+147<br/>
+148<br/>
+149<br/>
+150<br/>
+151<br/>
+152<br/>
+153<br/>
+154<br/>
+155<br/>
+156<br/>
+157<br/>
+158<br/>
+159<br/>
+160<br/>
+161<br/>
+162<br/>
+163<br/>
+164<br/>
+165<br/>
+166<br/>
+167<br/>
+168<br/>
+169<br/>
+170<br/>
+171<br/>
+172<br/>
+173<br/>
+174<br/>
+175<br/>
+176<br/>
+177<br/>
+178<br/>
+179<br/>
+180<br/>
+181<br/>
+182<br/>
+183<br/>
+184<br/>
+185<br/>
+186<br/>
+187<br/>
+188<br/>
+189<br/>
+190<br/>
+191<br/>
+192<br/>
+193<br/>
+194<br/>
+195<br/>
+196<br/>
+197<br/>
+198<br/>
+199<br/>
+200<br/>
+201<br/>
+202<br/>
+203<br/>
+204<br/>
+205<br/>
+206<br/>
+207<br/>
+208<br/>
+209<br/>
+210<br/>
+211<br/>
+212<br/>
+213<br/>
+214<br/>
+215<br/>
+216<br/>
+217<br/>
+218<br/>
+219<br/>
+220<br/>
+221<br/>
+222<br/>
+223<br/>
+224<br/>
+225<br/>
+226<br/>
+227<br/>
+228<br/>
+229<br/>
+230<br/>
+231<br/>
+232<br/>
+233<br/>
+234<br/>
+235<br/>
+236<br/>
+237<br/>
+238<br/>
+239<br/>
+240<br/>
+241<br/>
+242<br/>
+243<br/>
+244<br/>
+245<br/>
+246<br/>
+247<br/>
+248<br/>
+249<br/>
+250<br/>
+251<br/>
+252<br/>
+253<br/>
+254<br/>
+255<br/>
+256<br/>
+257<br/>
+258<br/>
+259<br/>
+260<br/>
+261<br/>
+262<br/>
+263<br/>
+264<br/>
+265<br/>
+266<br/>
+267<br/>
+268<br/>
+269<br/>
+270<br/>
+271<br/>
+272<br/>
+273<br/>
+274<br/>
+275<br/>
+276<br/>
+277<br/>
+278<br/>
+279<br/>
+280<br/>
+281<br/>
+282<br/>
+283<br/>
+284<br/>
+285<br/>
+286<br/>
+287<br/>
+288<br/>
+289<br/>
+290<br/>
+291<br/>
+292<br/>
+293<br/>
+294<br/>
+295<br/>
+296<br/>
+297<br/>
+298<br/>
+299<br/>
+300<br/>
+301<br/>
+302<br/>
+303<br/>
+304<br/>
+305<br/>
+306<br/>
+307<br/>
+308<br/>
+309<br/>
+310<br/>
+311<br/>
+312<br/>
+313<br/>
+314<br/>
+315<br/>
+316<br/>
+317<br/>
+318<br/>
+319<br/>
+320<br/>
+321<br/>
+322<br/>
+323<br/>
+324<br/>
+325<br/>
+326<br/>
+327<br/>
+328<br/>
+329<br/>
+330<br/>
+331<br/>
+332<br/>
+333<br/>
+334<br/>
+335<br/>
+336<br/>
+337<br/>
+338<br/>
+339<br/>
+340<br/>
+341<br/>
+342<br/>
+343<br/>
+344<br/>
+345<br/>
+346<br/>
+347<br/>
+348<br/>
+349<br/>
+350<br/>
+351<br/>
+352<br/>
+353<br/>
+354<br/>
+355<br/>
+356<br/>
+357<br/>
+358<br/>
+359<br/>
+360<br/>
+361<br/>
+362<br/>
+363<br/>
+364<br/>
+365<br/>
+366<br/>
+367<br/>
+368<br/>
+369<br/>
+370<br/>
+371<br/>
+372<br/>
+373<br/>
+374<br/>
+375<br/>
+376<br/>
+377<br/>
+378<br/>
+379<br/>
+380<br/>
+381<br/>
+382<br/>
+383<br/>
+384<br/>
+385<br/>
+386<br/>
+387<br/>
+388<br/>
+389<br/>
+390<br/>
+391<br/>
+392<br/>
+393<br/>
+394<br/>
+395<br/>
+396<br/>
+397<br/>
+398<br/>
+399<br/>
+400<br/>
+401<br/>
+402<br/>
+403<br/>
+404<br/>
+405<br/>
+406<br/>
+407<br/>
+408<br/>
+409<br/>
+410<br/>
+411<br/>
+412<br/>
+413<br/>
+414<br/>
+415<br/>
+416<br/>
+417<br/>
+418<br/>
+419<br/>
+420<br/>
+421<br/>
+422<br/>
+423<br/>
+424<br/>
+425<br/>
+426<br/>
+427<br/>
+428<br/>
+429<br/>
+430<br/>
+431<br/>
+432<br/>
+433<br/>
+434<br/>
+435<br/>
+436<br/>
+437<br/>
+438<br/>
+439<br/>
+440<br/>
+441<br/>
+442<br/>
+443<br/>
+444<br/>
+445<br/>
+446<br/>
+447<br/>
+448<br/>
+449<br/>
+450<br/>
+451<br/>
+452<br/>
+453<br/>
+454<br/>
+455<br/>
+456<br/>
+457<br/>
+458<br/>
+459<br/>
+460<br/>
+461<br/>
+462<br/>
+463<br/>
+464<br/>
+465<br/>
+466<br/>
+467<br/>
+468<br/>
+469<br/>
+470<br/>
+471<br/>
+472<br/>
+473<br/>
+474<br/>
+475<br/>
+476<br/>
+477<br/>
+478<br/>
+479<br/>
+480<br/>
+481<br/>
+482<br/>
+483<br/>
+484<br/>
+485<br/>
+486<br/>
+487<br/>
+488<br/>
+489<br/>
+490<br/>
+491<br/>
+492<br/>
+493<br/>
+494<br/>
+495<br/>
+496<br/>
+497<br/>
+498<br/>
+499<br/>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_drag_scrollbar_hittest.html b/gfx/layers/apz/test/mochitest/helper_drag_scrollbar_hittest.html
new file mode 100644
index 0000000000..7a1cab6dba
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_drag_scrollbar_hittest.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test that the scrollbar thumb remains under the cursor during scrollbar dragging</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="text/javascript">
+
+const utils = SpecialPowers.getDOMWindowUtils(window);
+
+async function test() {
+ // This test largely attempts to replicate the STR from bug 1826947
+ // (which demonstrates the same issue as bug 1818721 but with a more
+ // reduced test case).
+
+ is(await getResolution(), 1.0, "should not be zoomed");
+
+ // Zoom in. This is part of the bug 1826947 STR.
+ let resolution = 5.0;
+ utils.setResolutionAndScaleTo(resolution);
+ await promiseApzFlushedRepaints();
+
+ // Scroll horizontally to the middle of the visual viewport. This was
+ // determined empirically to be needed to reproduce the bug (and also
+ // is what happens automatically if you pinch-zoom in using an actual
+ // gesture rather than setResolutionAndScaleTo()).
+ utils.scrollToVisual(document.scrollingElement.clientWidth / 2,
+ 0,
+ utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_INSTANT);
+ await promiseApzFlushedRepaints();
+
+ // Install a scroll event listener. This is part of the usage of
+ // promiseVerticalScrollbarDrag(), we need to wait for a scroll event
+ // before awaiting dragFinisher().
+ // In this test, since the scroll range comes from zooming in, we need
+ // to listen for a *visual viewport* scroll event.
+ let visualScrollPromise = new Promise(resolve => {
+ window.visualViewport.addEventListener("scroll", resolve, {once: true});
+ });
+
+ // Start the animation in the SVG document. This is needed to reproduce
+ // the bug. (See below for why we do it dynamically.)
+ document.getElementsByTagName("animateTransform")[0].setAttribute("repeatCount", "indefinite");
+ document.getElementsByTagName("animateTransform")[0].beginElement();
+
+ // Drag the vertical scrollbar thumb downward.
+ // Do the scroll in one increment so that when the scroll event fires
+ // we're done all the scrolling we're going to do.
+ let distance = 20;
+ let increment = distance;
+ var dragFinisher = await promiseVerticalScrollbarDrag(window, distance, increment);
+ await visualScrollPromise;
+ await dragFinisher();
+
+ // Check that at the end of the drag, the thumb is still under the cursor.
+ // This is done using hitTest(). To compute the point to pass to hitTest(),
+ // use scrollbarDragStart() to compute the ending mouse position of the
+ // drag the way promiseVerticalScrollbarDrag() does.
+ // However, since we are passing the point to hitTest() which expects
+ // coordinates relative to the layout viewport (whereas scrollbarDragStart()
+ // returns coordinates relative to the visual viewport), we need to translate
+ // by the relative offset.
+ let dragStartPoint = await scrollbarDragStart(window, 1);
+ let hitTestPoint = {x: dragStartPoint.x, y: dragStartPoint.y + distance};
+ const relativeOffset = getRelativeViewportOffset(window);
+ hitTestPoint.x += relativeOffset.x;
+ hitTestPoint.y += relativeOffset.y;
+ let result = hitTest(hitTestPoint);
+ ok((result.hitInfo & APZHitResultFlags.SCROLLBAR_THUMB) != 0,
+ "Thumb should be under the cursor");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <!--
+ This is the testcase from bug 1826947 comment 0, except that the animation's
+ repeatCount is initially set to 0, and only changed to "indefinite" dynamically
+ during the test. This is to prevent an issue where the promiseAllPaintsDone()
+ call in waitUntilApzStable() can get into an infinite loop if we schedule
+ new frames of the animation faster than we paint them.
+ -->
+ <svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 100 100">
+ <filter id="THE_FILTER" x="0" y="0" width="10" height="100">
+ <feTurbulence id="turbulence" baseFrequency="10" numOctaves="5"/>
+ </filter>
+ <rect x="0" y="0" width="1" height="1" style="filter: url(#THE_FILTER);">
+ <animateTransform attributeName="transform" type="rotate" from="360" to="340" dur="5s" repeatCount="0"/>
+ </rect>
+ </svg>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_empty.html b/gfx/layers/apz/test/mochitest/helper_empty.html
new file mode 100644
index 0000000000..68cd9179f5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_empty.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<script src="apz_test_utils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_oopif.html b/gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_oopif.html
new file mode 100644
index 0000000000..d45a3bb046
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_oopif.html
@@ -0,0 +1,194 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for scrolled out of view animation optimization in an OOPIF</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+async function setup_in_oopif() {
+ const setup = function() {
+ // Load utility functions for animation stuff.
+ const script = document.createElement("script");
+ script.setAttribute("src", "/tests/dom/animation/test/testcommon.js");
+ document.head.appendChild(script);
+
+ const extraStyle = document.createElement("style");
+ document.head.appendChild(extraStyle);
+ // an animation doesn't cause any geometric changes and doesn't run on the
+ // compositor either
+ extraStyle.sheet.insertRule("@keyframes anim { from { color: red; } to { color: blue; } }", 0);
+
+ const div = document.createElement("div");
+ // Position an element for animation at top: 20px.
+ div.style = "position: absolute; top: 40px; animation: anim 1s infinite; box-shadow: 0px -20px red;";
+ div.setAttribute("id", "target");
+ div.innerHTML = "hello";
+ document.body.appendChild(div);
+ script.onload = () => {
+ // Force to flush the first style to avoid the first style is observed.
+ target.getAnimations()[0];
+ // FIXME: Bug 1578309 use anim.ready instead.
+ promiseFrame().then(() => {
+ FissionTestHelper.fireEventInEmbedder("OOPIF:SetupDone", true);
+ });
+ }
+ return true;
+ }
+
+ const iframePromise = promiseOneEvent(window, "OOPIF:SetupDone", null);
+
+ await FissionTestHelper.sendToOopif(testframe, `(${setup})()`);
+ await iframePromise;
+}
+
+async function observe_styling_in_oopif(aFrameCount) {
+ const observe_styling = function(frameCount) {
+ // Start in a rAF callback.
+ waitForAnimationFrames(1).then(() => {
+ observeStyling(frameCount).then((counter) => {
+ FissionTestHelper.fireEventInEmbedder("OOPIF:StyleCount", counter);
+ });
+ });
+
+ return true;
+ }
+
+ const iframePromise = promiseOneEvent(window, "OOPIF:StyleCount", null);
+ await FissionTestHelper.sendToOopif(testframe, `(${observe_styling})(${aFrameCount})`);
+
+ const styleCountData = await iframePromise;
+ return styleCountData.data;
+}
+
+async function promiseScrollInfoArrivalInOOPIF() {
+ const scrollPromise = new Promise(resolve => {
+ scroller.addEventListener("scroll", resolve, { once: true });
+ });
+
+ const transformReceivedPromise = SpecialPowers.spawn(testframe, [], async () => {
+ await SpecialPowers.contentTransformsReceived(content);
+ });
+
+ await Promise.all([scrollPromise, transformReceivedPromise]);
+}
+
+function isWindows11() {
+ return getPlatform() == "windows" &&
+ SpecialPowers.Services.sysinfo.getProperty("version", null) == "10.0" &&
+ SpecialPowers.Services.sysinfo.getProperty("build", null) == "22621";
+}
+
+// The actual test
+
+function assertThrottledRestyles(restyleCount, msg) {
+ // Allow 1 restyle count on Windows 11 since on our CIs there's something
+ // causing force flushing.
+ if (isWindows11()) {
+ ok(restyleCount <= 1, msg);
+ } else {
+ is(restyleCount, 0, msg);
+ }
+}
+
+async function test() {
+ // Generate an infinite animation which is initially clipped out by
+ // overflow: hidden style in the out-of-process iframe.
+ await setup_in_oopif();
+
+ let restyleCount = await observe_styling_in_oopif(5);
+ assertThrottledRestyles(
+ restyleCount,
+ "Animation in an out-of-process iframe which is initially clipped out " +
+ "due to 'overflow: hidden' should be throttled");
+
+ // Scroll synchronously to a position where the iframe gets visible.
+ scroller.scrollTo(0, 1000);
+ await promiseScrollInfoArrivalInOOPIF();
+
+ // Wait for a frame to make sure the notification of the last scroll position
+ // from APZC reaches the iframe process
+ await observe_styling_in_oopif(1);
+
+ restyleCount = await observe_styling_in_oopif(5);
+ is(restyleCount, 5,
+ "Animation in an out-of-process iframe which is no longer clipped out " +
+ "should NOT be throttled");
+
+ // Scroll synchronously to a position where the iframe is invisible again.
+ scroller.scrollTo(0, 0);
+ await promiseScrollInfoArrivalInOOPIF();
+
+ // Wait for a frame to make sure the notification of the last scroll position
+ // from APZC reaches the iframe process
+ await observe_styling_in_oopif(1);
+
+ restyleCount = await observe_styling_in_oopif(5);
+ assertThrottledRestyles(
+ restyleCount,
+ "Animation in an out-of-process iframe which is clipped out again " +
+ "should be throttled again");
+
+ // ===== Asyncronous scrolling tests =====
+ scroller.style.overflow = "scroll";
+ // Scroll asynchronously to a position where the animating element gets
+ // visible.
+ scroller.scrollTo({ left: 0, top: 750, behavior: "smooth"});
+
+ // Wait for the asyncronous scroll finish. `60` frames is the same number in
+ // helper_fission_scroll_oopif.html
+ await observe_styling_in_oopif(60);
+
+ restyleCount = await observe_styling_in_oopif(5);
+ is(restyleCount, 5,
+ "Animation in an out-of-process iframe which is now visible by " +
+ "asynchronous scrolling should NOT be throttled");
+
+ // Scroll asynchronously to a position where the iframe is still visible but
+ // the animating element gets invisible.
+ scroller.scrollTo({ left: 0, top: 720, behavior: "smooth"});
+
+ // Wait for the asyncronous scroll finish.
+ await observe_styling_in_oopif(60);
+
+ restyleCount = await observe_styling_in_oopif(5);
+ assertThrottledRestyles(
+ restyleCount,
+ "Animation in an out-of-process iframe which is scrolled out of view by " +
+ "asynchronous scrolling should be throttled");
+
+ // Scroll asynchronously to a position where the animating element gets
+ // visible again.
+ scroller.scrollTo({ left: 0, top: 750, behavior: "smooth"});
+
+ // Wait for the asyncronous scroll finish.
+ await observe_styling_in_oopif(60);
+
+ restyleCount = await observe_styling_in_oopif(5);
+ is(restyleCount, 5,
+ "Animation in an out-of-process iframe appeared by the asynchronous " +
+ "scrolling should be NOT throttled");
+}
+
+ </script>
+</head>
+<div style="width: 300px; height: 300px; overflow: hidden;" id="scroller">
+ <div style="width: 100%; height: 1000px;"></div>
+ <!-- I am not sure it's worth setting scrolling="no" and pointer-events: none. -->
+ <!-- I just want to make sure that HitTestingTreeNode is generated even with these properties. -->
+ <iframe scrolling="no" style="pointer-events: none;" id="testframe"></iframe>
+</div>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_transformed_oopif.html b/gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_transformed_oopif.html
new file mode 100644
index 0000000000..5a846cedcc
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_transformed_oopif.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for scrolled out of view animation optimization in an OOPIF transformed by rotate(45deg)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+async function setup_in_oopif() {
+ const setup = function() {
+ // Load utility functions for animation stuff.
+ const script = document.createElement("script");
+ script.setAttribute("src", "/tests/dom/animation/test/testcommon.js");
+ document.head.appendChild(script);
+
+ const extraStyle = document.createElement("style");
+ document.head.appendChild(extraStyle);
+ // an animation doesn't affect any geometric changes and doesn't run on the
+ // compositor either
+ extraStyle.sheet.insertRule("@keyframes anim { from { color: red; } to { color: blue; } }", 0);
+
+ const animation = document.createElement("div");
+ animation.style = "animation: anim 1s infinite;";
+ animation.innerHTML = "hello";
+ document.body.appendChild(animation);
+ script.onload = () => {
+ const rect = animation.getBoundingClientRect();
+
+ FissionTestHelper.fireEventInEmbedder("OOPIF:SetupDone",
+ [rect.right, rect.bottom]);
+ }
+ return true;
+ }
+
+ const iframePromise = promiseOneEvent(window, "OOPIF:SetupDone", null);
+
+ await FissionTestHelper.sendToOopif(testframe, `(${setup})()`);
+ const rectData = await iframePromise;
+ return rectData.data;
+}
+
+async function observe_styling_in_oopif(aFrameCount) {
+ const observe_styling = function(frameCount) {
+ // Start in a rAF callback.
+ waitForAnimationFrames(1).then(() => {
+ observeStyling(frameCount).then((counter) => {
+ FissionTestHelper.fireEventInEmbedder("OOPIF:StyleCount", counter);
+ });
+ });
+
+ return true;
+ }
+
+ const iframePromise = promiseOneEvent(window, "OOPIF:StyleCount", null);
+ await FissionTestHelper.sendToOopif(testframe, `(${observe_styling})(${aFrameCount})`);
+
+ const styleCountData = await iframePromise;
+ return styleCountData.data;
+}
+
+// The actual test
+
+async function test() {
+ // Generate an infinite animation which is initially scrolled out of view.
+ // setup_in_oopif() returns the right bottom position of the animating element
+ // on the iframe coodinate system.
+ const [right, bottom] = await setup_in_oopif();
+
+ let restyleCount = await observe_styling_in_oopif(5);
+ is(restyleCount, 0,
+ "Animation in an out-of-process iframe which is initially scrolled out " +
+ "of view should be throttled");
+
+ const topPositionOfIFrame = testframe.getBoundingClientRect().top -
+ scroller.clientHeight;
+ // Scroll asynchronously to a position where the animating element gets
+ // visible.
+ scroller.scrollTo({ left: 0, top: topPositionOfIFrame + 1, behavior: "smooth"});
+
+ // Wait for the asyncronous scroll finish. `60` frames is the same number in
+ // helper_fission_scroll_oopif.html
+ await observe_styling_in_oopif(60);
+
+ restyleCount = await observe_styling_in_oopif(5);
+ is(restyleCount, 5,
+ "Animation in an out-of-process iframe which is no longer scrolled out " +
+ "of view should NOT be throttled");
+
+ // Calculate the right bottom position of the animation which is in an iframe
+ // rotated by `rotate(45deg)`
+ const rightBottomPositionOfAnimation =
+ right / Math.sqrt(2) + bottom / Math.sqrt(2);
+
+ // Scroll asynchronously to a position where the animating element gets
+ // invisible again.
+ scroller.scrollTo({ left: 0,
+ top: topPositionOfIFrame + scroller.clientHeight + rightBottomPositionOfAnimation,
+ behavior: "smooth"});
+
+ // Wait for the asyncronous scroll finish.
+ await observe_styling_in_oopif(60);
+
+ restyleCount = await observe_styling_in_oopif(5);
+ is(restyleCount, 0,
+ "Animation in an out-of-process iframe which is scrolled out of view " +
+ "again should be throttled");
+}
+
+ </script>
+</head>
+<div style="width: 300px; height: 300px; overflow: scroll;" id="scroller">
+ <div style="width: 100%; height: 1000px;"></div>
+ <div style="transform: rotate(45deg);">
+ <iframe scrolling="no" style="pointer-events: none;" id="testframe" frameborder="0"></iframe>
+ </div>
+ <div style="width: 100%; height: 1000px;"></div>
+</div>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_basic.html b/gfx/layers/apz/test/mochitest/helper_fission_basic.html
new file mode 100644
index 0000000000..dbc41477b9
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_basic.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Basic sanity test that runs inside a fission-enabled window</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+
+// The actual test
+
+async function test() {
+ let iframeElement = document.getElementById("testframe");
+ ok(SpecialPowers.wrap(window)
+ .docShell
+ .QueryInterface(SpecialPowers.Ci.nsILoadContext)
+ .useRemoteSubframes,
+ "OOP iframe is actually OOP");
+ let iframeResult = await FissionTestHelper.sendToOopif(iframeElement, "20 + 22");
+ is(iframeResult, 42, "Basic content fission test works");
+}
+
+ </script>
+</head>
+<body>
+<iframe id="testframe"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_checkerboard_severity.html b/gfx/layers/apz/test/mochitest/helper_fission_checkerboard_severity.html
new file mode 100644
index 0000000000..ef7943b29f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_checkerboard_severity.html
@@ -0,0 +1,138 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>
+ A test to make sure checkerboard severity isn't reported for non-scrollable
+ OOP iframe
+ </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+
+// The actual test
+
+let code_for_oopif_to_run = function() {
+ document.addEventListener("click", function(e) {
+ dump(`OOPIF got click at ${e.clientX},${e.clientY}\n`);
+ let result = { x: e.clientX, y: e.clientY };
+ FissionTestHelper.fireEventInEmbedder("OOPIF:ClickData", result);
+ });
+ dump("OOPIF registered click listener\n");
+ return true;
+};
+
+async function getIframeDisplayport(iframe) {
+ let oopif_displayport = function() {
+ let result = getLastContentDisplayportFor("fission_empty_docelement", false);
+ FissionTestHelper.fireEventInEmbedder("OOPIF:Displayport", result);
+ return true;
+ };
+
+ let iframePromise = promiseOneEvent(window, "OOPIF:Displayport", null);
+ ok(await FissionTestHelper.sendToOopif(iframe, `(${oopif_displayport})()`));
+ let iframeResponse = await iframePromise;
+ dump("OOPIF response for Displayport: " +
+ JSON.stringify(iframeResponse.data) + "\n");
+
+ return iframeResponse.data;
+}
+
+async function getIframeScrollMax(iframe) {
+ let oopif_scroll_max = function() {
+ let result = {
+ scrollMaxX: window.scrollMaxX,
+ scrollMaxY: window.scrollMaxY
+ };
+ FissionTestHelper.fireEventInEmbedder("OOPIF:ScrollMax", result);
+ return true;
+ };
+
+ let iframePromise = promiseOneEvent(window, "OOPIF:ScrollMax", null);
+ ok(await FissionTestHelper.sendToOopif(iframe, `(${oopif_scroll_max})()`));
+ let iframeResponse = await iframePromise;
+ dump("OOPIF response for ScrollMax: " +
+ JSON.stringify(iframeResponse.data) + "\n");
+
+ return iframeResponse.data;
+}
+
+async function test() {
+ await SpecialPowers.spawnChrome([], () => {
+ Services.telemetry.getHistogramById("CHECKERBOARD_SEVERITY").clear();
+ });
+
+ const iframe = document.getElementById("testframe");
+
+ // Make sure the iframe content is not scrollable.
+ const { scrollMaxX, scrollMaxY } = await getIframeScrollMax(iframe);
+ is(scrollMaxX, 0, "The iframe content should not be scrollable");
+ is(scrollMaxY, 0, "The iframe content should not be scrollable");
+
+ // Since bug 1709460 any visible OOP iframe initially has set the displayport.
+ let displayport = await getIframeDisplayport(iframe);
+ is(displayport.width, 400);
+ is(displayport.height, 300);
+
+ let iframeResponse =
+ await FissionTestHelper.sendToOopif(iframe, `(${code_for_oopif_to_run})()`);
+ dump("OOPIF response: " + JSON.stringify(iframeResponse) + "\n");
+ ok(iframeResponse, "code_for_oopif_to_run successfully installed");
+
+ // Click on the iframe via APZ so that it triggers a RequestContentRepaint
+ // call then it sets zero display port margins for the iframe's root scroller,
+ // thus as a result it will report a checkerboard event if there had been
+ // checkerboarding.
+ iframePromise = promiseOneEvent(window, "OOPIF:ClickData", null);
+ await synthesizeNativeMouseEventWithAPZ(
+ { type: "click", target: iframe, offsetX: 10, offsetY: 10 },
+ () => dump("Finished synthesizing click, waiting for OOPIF message...\n")
+ );
+ iframeResponse = await iframePromise;
+ dump("OOPIF response: " + JSON.stringify(iframeResponse.data) + "\n");
+
+ // Now the displayport size should have been set.
+ displayport = await getIframeDisplayport(iframe);
+ is(displayport.width, 400, "The displayport size should be same as the iframe size");
+ is(displayport.height, 300, "The displayport size should be same as the iframe size");
+
+ // Wait 100ms to give a chance to deliver the checkerboard event.
+ await new Promise(resolve => {
+ setTimeout(resolve, 100);
+ });
+
+ const hasCheckerboardSeverity = await SpecialPowers.spawnChrome([], () => {
+ const histograms = Services.telemetry.getSnapshotForHistograms(
+ "main",
+ true /* clear the histograms after taking this snapshot*/).parent;
+
+ return histograms.hasOwnProperty("CHECKERBOARD_SEVERITY");
+ });
+ ok(!hasCheckerboardSeverity, "there should be no checkerboard severity data");
+}
+ </script>
+ <style>
+ iframe {
+ width: 400px;
+ height: 300px;
+ border: none;
+ }
+ </style>
+</head>
+<body>
+<iframe id="testframe"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_empty.html b/gfx/layers/apz/test/mochitest/helper_fission_empty.html
new file mode 100644
index 0000000000..6a95f76339
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_empty.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html id="fission_empty_docelement">
+ <meta charset="utf-8">
+ <style>
+ html,body {
+ /* Convenient for calculation of element positions */
+ margin: 0;
+ padding: 0;
+ }
+ </style>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+// This is an empty document that serves as a OOPIF content document that be
+// reused by different fission subtests. The subtest can eval stuff in this
+// document using the sendToOopif helper and thereby populate this document
+// with whatever is needed. This allows the subtest to more "contained" in a
+// single file and avoids having to create new dummy files for each subtest.
+async function loaded() {
+ window.dispatchEvent(new Event("FissionTestHelper:Init"));
+ // Wait a couple of animation frames before sending the load, to ensure that
+ // this OOPIF's layer tree has been sent to the compositor. We use this
+ // instead of things like promiseOnlyApzControllerFlushed and/or promiseAllPaintsDone because
+ // this page is running without SpecialPowers and I couldn't figure out a good
+ // way to get a hold of a things like Services.obs or DOMWindowUtils easily.
+ await promiseFrame();
+ await promiseFrame();
+ FissionTestHelper.fireEventInEmbedder("OOPIF:Load", {content: window.location.href});
+}
+ </script>
+ <body onload="loaded()">
+ </body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_event_region_override.html b/gfx/layers/apz/test/mochitest/helper_fission_event_region_override.html
new file mode 100644
index 0000000000..82f529bebd
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_event_region_override.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Ensure the event region override flags work properly</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+
+// The actual test
+
+let code_for_oopif_to_run = function() {
+ document.body.innerHTML = '<div style="height: 5000px">scrollable content</div>';
+ document.addEventListener("wheel", function(e) {
+ dump(`OOPIF got wheel at ${e.clientX},${e.clientY}\n`);
+ let result = { x: e.clientX, y: e.clientY };
+ FissionTestHelper.fireEventInEmbedder("OOPIF:WheelData", result);
+ }, { passive: true });
+ document.addEventListener("scroll", function(e) {
+ dump(`OOPIF got scroll to ${window.scrollX},${window.scrollY}\n`);
+ let result = { x: window.scrollX, y: window.scrollY };
+ FissionTestHelper.fireEventInEmbedder("OOPIF:Scrolled", result);
+ });
+ dump("OOPIF registered wheel and scroll listeners\n");
+ return true;
+};
+
+async function test() {
+ let iframeElement = document.getElementById("testframe");
+
+ let iframeResponse = await FissionTestHelper.sendToOopif(iframeElement, `(${code_for_oopif_to_run})()`);
+ dump("OOPIF response: " + JSON.stringify(iframeResponse) + "\n");
+ ok(iframeResponse, "code_for_oopif_to_run successfully installed");
+
+ let wheeled = false;
+ let scrolled = false;
+ window.addEventListener("OOIF:WheelData", function listener(e) {
+ dump("OOPIF:WheelData received with data: " + JSON.stringify(e.data) + "\n");
+ wheeled = true;
+ });
+ window.addEventListener("OOPIF:Scrolled", function listener(e) {
+ dump("OOPIF:Scrolled received with data: " + JSON.stringify(e.data) + "\n");
+ scrolled = true;
+ });
+
+ await synthesizeNativeWheel(iframeElement, 10, 10, 0, -50);
+
+ // Advance a bunch of frames. The only goal here is to ensure enough time
+ // passes so that if the OOPIF does scroll, we find out about it via the
+ // OOPIF:Scrolled messaging.
+ // If we don't wait long enough we might end up finishing the test before
+ // that scroll message gets received here, and so we might wrongly pass the
+ // test.
+ await SpecialPowers.promiseTimeout(0);
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ for (var i = 0; i < 5; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ utils.restoreNormalRefresh();
+ await promiseOnlyApzControllerFlushed();
+
+ ok(!wheeled, "OOPIF correctly did not get wheel event");
+ ok(!scrolled, "OOPIF correctly did not scroll");
+}
+
+ </script>
+</head>
+<body>
+<iframe id="testframe"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_force_empty_hit_region.html b/gfx/layers/apz/test/mochitest/helper_fission_force_empty_hit_region.html
new file mode 100644
index 0000000000..7baa552c37
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_force_empty_hit_region.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Ensure the ForceEmptyHitRegion flag works properly</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe1", "helper_fission_empty.html"))
+ .then(loadOOPIFrame("testframe2", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+
+// The actual test
+
+let code_for_oopif_to_run = function() {
+ document.body.style.backgroundColor = 'green'; // To ensure opaqueness
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ dump("OOPIF got layersId: " + utils.getLayersId() +
+ ", scrollId: " + utils.getViewId(document.scrollingElement) + "\n");
+ return JSON.stringify({
+ layersId: utils.getLayersId(),
+ viewId: utils.getViewId(document.scrollingElement)
+ });
+};
+
+let iframe_compositor_test_data = function() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ let result = JSON.stringify(utils.getCompositorAPZTestData());
+ dump("OOPIF got compositor APZ data: " + result + "\n");
+ return result;
+};
+
+async function test() {
+ let iframe1 = document.getElementById("testframe1");
+ let iframe2 = document.getElementById("testframe2");
+
+ let iframeResponse = await FissionTestHelper.sendToOopif(iframe1, `(${code_for_oopif_to_run})()`);
+ dump("OOPIF response: " + iframeResponse + "\n");
+ ok(iframeResponse, "code_for_oopif_to_run successfully installed in frame1");
+
+ iframeResponse = await FissionTestHelper.sendToOopif(iframe2, `(${code_for_oopif_to_run})()`);
+ dump("OOPIF response: " + iframeResponse + "\n");
+ ok(iframeResponse, "code_for_oopif_to_run successfully installed in frame2");
+ let iframe2Expected = JSON.parse(iframeResponse);
+
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ // Hit-testing the iframe with pointer-events:none should end up hitting the
+ // document containing the iframe instead (i.e. this document).
+ checkHitResult(await fissionHitTest(centerOf(iframe1), iframe1),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "center of pointer-events:none iframe should hit parent doc");
+
+ // Hit-testing the iframe that doesn't have pointer-events:none should end up
+ // hitting that iframe.
+ checkHitResult(await fissionHitTest(centerOf(iframe2), iframe2),
+ APZHitResultFlags.VISIBLE,
+ iframe2Expected.viewId,
+ iframe2Expected.layersId,
+ "center of regular iframe should hit iframe doc");
+}
+
+ </script>
+</head>
+<body>
+<iframe id="testframe1" style="pointer-events:none"></iframe>
+<iframe id="testframe2"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_positionedcontent.html b/gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_positionedcontent.html
new file mode 100644
index 0000000000..fc43ace0f1
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_positionedcontent.html
@@ -0,0 +1,120 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Ensure positioned content inside inactive scollframes but on top of OOPIFs hit-test properly</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+let make_oopif_scrollable = function() {
+ // ensure the oopif is scrollable, and wait for the paint so that the
+ // compositor also knows it's scrollable.
+ document.body.style.height = "200vh";
+ promiseApzFlushedRepaints().then(() => {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ let result = {
+ layersId: utils.getLayersId(),
+ viewId: utils.getViewId(document.scrollingElement)
+ };
+ dump(`OOPIF computed IDs ${JSON.stringify(result)}\n`);
+ FissionTestHelper.fireEventInEmbedder("OOPIF:Scrollable", result);
+ });
+ return true;
+};
+
+async function test() {
+ let iframe = document.getElementById("testframe");
+
+ let oopifScrollerIdsPromise = promiseOneEvent(window, "OOPIF:Scrollable", null);
+ ok(await FissionTestHelper.sendToOopif(iframe, `(${make_oopif_scrollable})()`),
+ "Ran code to make OOPIF scrollable");
+ let oopifScrollerIds = (await oopifScrollerIdsPromise).data;
+
+ let config = getHitTestConfig();
+ let utils = config.utils;
+
+ // The #scroller div is (a) inactive, and (b) under the OOPIF. However, it
+ // also contains a positioned element with a high z-index (#abspos). #abspos
+ // therefore sits on top of the OOPIF. Hit-testing on #abspos should hit
+ // #scroller, but anywhere else within the OOPIF box should hit the OOPIF.
+
+ checkHitResult(await fissionHitTest(centerOf("abspos"), iframe),
+ APZHitResultFlags.VISIBLE |
+ (config.activateAllScrollFrames ? 0 : APZHitResultFlags.INACTIVE_SCROLLFRAME),
+ config.activateAllScrollFrames ?
+ utils.getViewId(document.getElementById("scroller")) :
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "abspos element on top of OOPIF should hit parent doc hosting the OOPIF");
+
+ // If the fix for the bug this test is for is not active (as indicated by
+ // config.activateAllScrollFrames) then we just accept the wrong answer. As
+ // of writing this comment the fix will only be active if fission is pref'ed
+ // on, not just enabled for this window, ie the test suite is run in fission
+ // mode.
+ checkHitResult(await fissionHitTest(centerOf("scroller"), iframe),
+ APZHitResultFlags.VISIBLE |
+ (config.activateAllScrollFrames ? 0 : APZHitResultFlags.INACTIVE_SCROLLFRAME),
+ config.activateAllScrollFrames ?
+ oopifScrollerIds.viewId :
+ utils.getViewId(document.scrollingElement),
+ config.activateAllScrollFrames ?
+ oopifScrollerIds.layersId :
+ utils.getLayersId(),
+ "Part of OOPIF sitting on top of the inactive scrollframe should hit OOPIF");
+
+ checkHitResult(await fissionHitTest({x: 250, y: 100}, iframe),
+ APZHitResultFlags.VISIBLE,
+ oopifScrollerIds.viewId,
+ oopifScrollerIds.layersId,
+ "part of OOPIF outside the inactive scfollframe rect should hit the OOPIF");
+}
+
+ </script>
+</head>
+<body>
+<style>
+html, body {
+ margin: 0;
+}
+body {
+ /* Ensure root document is scrollable so that #scroller is inactive by
+ default */
+ height: 200vh;
+}
+iframe {
+ position: absolute;
+ width: 300px;
+ height: 200px;
+}
+
+#scroller {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 200px;
+ height: 200px;
+ background-color: transparent;
+ overflow-y: scroll;
+}
+</style>
+<div id="scroller">
+ <div style="height:500px">inside scroller</div>
+ <div id="abspos" style="position: absolute; z-index: 5; left: 0; width: 80px; top: 20px; height: 80px; background-color: green">abspos inside scroller</div>
+</div>
+<iframe id="testframe"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_under_oopif.html b/gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_under_oopif.html
new file mode 100644
index 0000000000..c3099f52ef
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_under_oopif.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Ensure inactive scollframes under OOPIFs hit-test properly</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+let make_oopif_scrollable = function() {
+ // ensure the oopif is scrollable, and wait for the paint so that the
+ // compositor also knows it's scrollable.
+ document.body.style.height = "200vh";
+ promiseApzFlushedRepaints().then(() => {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ let result = {
+ layersId: utils.getLayersId(),
+ viewId: utils.getViewId(document.scrollingElement)
+ };
+ dump(`OOPIF computed IDs ${JSON.stringify(result)}\n`);
+ FissionTestHelper.fireEventInEmbedder("OOPIF:Scrollable", result);
+ });
+ return true;
+};
+
+async function test() {
+ let iframe = document.getElementById("testframe");
+
+ let letScrollerIdPromise = promiseOneEvent(window, "OOPIF:Scrollable", null);
+ ok(await FissionTestHelper.sendToOopif(iframe, `(${make_oopif_scrollable})()`),
+ "Ran code to make OOPIF scrollable");
+ let oopifScrollerIds = (await letScrollerIdPromise).data;
+
+ // The #scroller div is (a) inactive, and (b) under the OOPIF. Hit-testing
+ // against it should hit the OOPIF.
+
+ checkHitResult(await fissionHitTest(centerOf("scroller"), iframe),
+ APZHitResultFlags.VISIBLE,
+ oopifScrollerIds.viewId,
+ oopifScrollerIds.layersId,
+ "Part of OOPIF sitting on top of the inactive scrollframe should hit OOPIF");
+}
+
+ </script>
+</head>
+<body>
+<style>
+html, body {
+ margin: 0;
+}
+body {
+ /* Ensure root document is scrollable so that #scroller is inactive by
+ default */
+ height: 200vh;
+}
+iframe {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 300px;
+ height: 200px;
+}
+
+#scroller {
+ width: 200px;
+ height: 200px;
+ background-color: transparent;
+ overflow-y: scroll;
+}
+</style>
+<div id="scroller">
+ <div style="height:500px">inside scroller</div>
+</div>
+<iframe id="testframe"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_initial_displayport.html b/gfx/layers/apz/test/mochitest/helper_fission_initial_displayport.html
new file mode 100644
index 0000000000..bb03ad4eb7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_initial_displayport.html
@@ -0,0 +1,105 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test that OOP iframe's displayport is initially set</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("visible-testframe", "helper_fission_empty.html"))
+ .then(loadOOPIFrame("invisible-testframe", "helper_fission_empty.html"))
+ .then(loadOOPIFrame("scrolled-out-testframe", "helper_fission_empty.html"))
+ .then(loadOOPIFrame("clipped-out-testframe", "helper_fission_empty.html"))
+ .then(loadOOPIFrame("partially-scrolled-out-testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+
+// The actual test
+
+async function getIframeDisplayport(iframeElement) {
+ let oopif_displayport = function() {
+ let result = getLastContentDisplayportFor("fission_empty_docelement", false);
+ FissionTestHelper.fireEventInEmbedder("OOPIF:Displayport", result);
+ return true;
+ };
+
+ let iframePromise = promiseOneEvent(window, "OOPIF:Displayport", null);
+ ok(await FissionTestHelper.sendToOopif(iframeElement, `(${oopif_displayport})()`));
+ let iframeResponse = await iframePromise;
+ dump("OOPIF response for Displayport: " +
+ JSON.stringify(iframeResponse.data) + "\n");
+
+ return iframeResponse.data;
+}
+
+async function test() {
+ const visibleIframeElement = document.getElementById("visible-testframe");
+
+ // Fully visible iframe.
+ let displayport = await getIframeDisplayport(visibleIframeElement);
+ is(displayport.width, 400, "The displayport size should be same as the iframe size");
+ is(displayport.height, 300, "The displayport size should be same as the iframe size");
+
+ // Fully invisible iframe (inside `overflow: hidden` parent element)
+ const invisibleIframeElement = document.getElementById("invisible-testframe");
+ displayport = await getIframeDisplayport(invisibleIframeElement);
+ ok(!displayport, "The displayport shouldn't have set for invisible iframe");
+
+ // Scrolled out iframe.
+ const scrolledOutIframeElement = document.getElementById("scrolled-out-testframe");
+ displayport = await getIframeDisplayport(scrolledOutIframeElement);
+ ok(!displayport,
+ "The displayport shouldn't have set for iframe far away from the parent displayport");
+
+ // Partially invisible iframe (inside `overflow: hidden` parent element)
+ const clippedOutIframeElement = document.getElementById("clipped-out-testframe");
+ displayport = await getIframeDisplayport(clippedOutIframeElement);
+ is(displayport.width, 400, "The displayport width should be same as the iframe width");
+ ok(displayport.height > 0, "The displayport height should be greater than zero");
+ ok(displayport.height < 300, "The displayport height should be less than the iframe height");
+
+ const partiallyScrolledOutIframeElement = document.getElementById("partially-scrolled-out-testframe");
+ displayport = await getIframeDisplayport(partiallyScrolledOutIframeElement);
+ is(displayport.width, 400, "The displayport width should be same as the iframe width");
+ ok(displayport.height > 0, "The displayport height should be greater than zero");
+ ok(displayport.height < 300, "The displayport height should be less than the iframe height");
+}
+
+ </script>
+ <style>
+ iframe {
+ width: 400px;
+ height: 300px;
+ border: none;
+ }
+ </style>
+</head>
+<body>
+<iframe id="visible-testframe"></iframe>
+<div style="width: 300px; height: 300px; overflow: hidden;">
+ <div style="width: 100%; height: 1000px;"></div>
+ <iframe id="invisible-testframe"></iframe>
+</div>
+<div style="width: 300px; height: 300px; overflow: scroll;">
+ <div style="width: 100%; height: 10000px;"></div>
+ <iframe id="scrolled-out-testframe"></iframe>
+</div>
+<div style="width: 300px; height: 300px; overflow: hidden;">
+ <div style="width: 100%; height: 200px;"></div>
+ <iframe id="clipped-out-testframe"></iframe>
+</div>
+<div style="width: 300px; height: 300px; overflow: scroll;">
+ <div style="width: 100%; height: 200px;"></div>
+ <iframe id="partially-scrolled-out-testframe"></iframe>
+</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_irregular_areas.html b/gfx/layers/apz/test/mochitest/helper_fission_irregular_areas.html
new file mode 100644
index 0000000000..8ef3367c06
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_irregular_areas.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Ensure irregular areas on top of OOPIFs hit-test properly</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+let make_oopif_scrollable = function() {
+ // ensure the oopif is scrollable, and wait for the paint so that the
+ // compositor also knows it's scrollable.
+ document.body.style.height = "200vh";
+ promiseApzFlushedRepaints().then(() => {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ let result = {
+ layersId: utils.getLayersId(),
+ viewId: utils.getViewId(document.scrollingElement)
+ };
+ dump(`OOPIF computed IDs ${JSON.stringify(result)}\n`);
+ FissionTestHelper.fireEventInEmbedder("OOPIF:Scrollable", result);
+ });
+ return true;
+};
+
+async function test() {
+ let iframe = document.getElementById("testframe");
+
+ let oopifScrollerIds = promiseOneEvent(window, "OOPIF:Scrollable", null);
+ ok(await FissionTestHelper.sendToOopif(iframe, `(${make_oopif_scrollable})()`),
+ "Ran code to make OOPIF scrollable");
+ oopifScrollerIds = (await oopifScrollerIds).data;
+
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ // The triangle_overlay div overlays a part of the iframe. We do 3 hit-tests:
+ // - one that hits the opaque part of the overlay
+ // - one that hits the clipped-away part of the overlay div but is still
+ // inside the bounding box
+ // - one that is not on the overlay at all, but on the part of the iframe not
+ // covered by the overlay.
+ // For the latter two, we expect the hit-test to hit the OOPIF.
+
+ checkHitResult(await fissionHitTest({x: 20, y: 100}, iframe),
+ APZHitResultFlags.VISIBLE | APZHitResultFlags.IRREGULAR_AREA,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "opaque part of overlay should hit parent doc hosting the OOPIF");
+
+ checkHitResult(await fissionHitTest({x: 180, y: 100}, iframe),
+ APZHitResultFlags.VISIBLE,
+ oopifScrollerIds.viewId,
+ oopifScrollerIds.layersId,
+ "clipped-away part of overlay should hit OOPIF");
+
+ checkHitResult(await fissionHitTest({x: 250, y: 100}, iframe),
+ APZHitResultFlags.VISIBLE,
+ oopifScrollerIds.viewId,
+ oopifScrollerIds.layersId,
+ "part of OOPIF outside the overlay bounding rect should hit the OOPIF");
+}
+
+ </script>
+</head>
+<body>
+<style>
+html, body {
+ margin: 0;
+}
+iframe {
+ position: absolute;
+ width: 300px;
+ height: 200px;
+}
+
+#triangle_overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 200px;
+ height: 200px;
+ background-color: green;
+ clip-path: polygon(0% 0%, 100% 100%, 0% 100%);
+}
+</style>
+<iframe id="testframe"></iframe>
+<div id="triangle_overlay"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_large_subframe.html b/gfx/layers/apz/test/mochitest/helper_fission_large_subframe.html
new file mode 100644
index 0000000000..3d5595f48e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_large_subframe.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html id="rcd_docelement">
+<head>
+ <meta charset="utf-8">
+ <title>Test that large OOPIF does not get a too-large displayport</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+// Code that will run inside the iframe.
+let get_iframe_displayport = function() {
+ // Give the page a scroll range. This will make the unclamped displayport
+ // even taller.
+ document.body.style.height = "200vh";
+ // Do a round-trip to APZ to make sure the scroll range is reflected
+ // in the displayport it sets.
+ promiseApzFlushedRepaints().then(() => {
+ // Query the composed displayport and report it to the embedder.
+ let result = getLastContentDisplayportFor("fission_empty_docelement");
+ FissionTestHelper.fireEventInEmbedder("OOPIF:Displayport", result);
+ });
+ return true;
+};
+
+async function test() {
+ let iframeElement = document.getElementById("testframe");
+
+ // Give the page a scroll range and make sure APZ sets non-empty displayport margins.
+ document.body.style.height = "500vh";
+ await promiseApzFlushedRepaints();
+
+ // Query the iframe's displayport.
+ let displayportPromise = promiseOneEvent(window, "OOPIF:Displayport", null);
+ ok(await FissionTestHelper.sendToOopif(iframeElement, `(${get_iframe_displayport})()`), "Gotten iframe displayport");
+ let iframeDp = (await displayportPromise).data;
+ dump("iframe displayport is " + JSON.stringify(iframeDp) + "\n");
+
+ // Query the page's displayport.
+ let dp = getLastContentDisplayportFor("rcd_docelement");
+ dump("page displayport is " + JSON.stringify(dp) + "\n");
+
+ // Check that the iframe's displayport is no more than twice as tall as
+ // the page's displayport. The reason it can be up to twice as tall is
+ // described in bug 1690697; we may be able to assert a tighter bound
+ // after making improvements in that bug.
+ ok(iframeDp.height <= (dp.height * 2), "iframe displayport should be no more than twice as tall as page displayport");
+}
+
+ </script>
+</head>
+<body>
+<!-- Make the iframe's viewport very tall -->
+<iframe style="margin-top: 200px" id="testframe" height="10000px"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_scroll_handoff.html b/gfx/layers/apz/test/mochitest/helper_fission_scroll_handoff.html
new file mode 100644
index 0000000000..3f75706778
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_scroll_handoff.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>scroll handoff</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+async function test() {
+ let scrollEventPromise = new Promise(resolve => {
+ window.addEventListener("scroll", resolve, { once: true });
+ });
+
+ let iframe = document.getElementById("testframe");
+
+ await synthesizeNativeWheel(iframe, 100, 100, 0, -50);
+ await scrollEventPromise;
+
+ ok(window.scrollY > 0,
+ "Mouse wheel scrolling on OOP iframes in position:fixed subtree " +
+ "should be handed off to the parent");
+}
+
+ </script>
+</head>
+<style>
+iframe {
+ position: fixed;
+ width: 500px;
+ height: 500px;
+}
+</style>
+<body>
+<iframe id="testframe"></iframe>
+<div style="height:1000vh"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_scroll_oopif.html b/gfx/layers/apz/test/mochitest/helper_fission_scroll_oopif.html
new file mode 100644
index 0000000000..2911b1eaf0
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_scroll_oopif.html
@@ -0,0 +1,158 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for async-scrolling an OOPIF and ensuring hit-testing still works</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+
+let code_for_oopif_to_run = function() {
+ document.addEventListener("click", function(e) {
+ dump(`OOPIF got click at ${e.clientX},${e.clientY}\n`);
+ let result = { x: e.clientX, y: e.clientY };
+ FissionTestHelper.fireEventInEmbedder("OOPIF:ClickData", result);
+ });
+ dump("OOPIF registered click listener\n");
+ return true;
+};
+
+async function clickOnIframe(x, y) {
+ let iframePromise = promiseOneEvent(window, "OOPIF:ClickData", null);
+ await synthesizeNativeMouseEventWithAPZ(
+ { type: "click", target: document.body, offsetX: x, offsetY: y },
+ () => dump("Finished synthesizing click, waiting for OOPIF message...\n")
+ );
+ let iframeResponse = await iframePromise;
+ dump("OOPIF response: " + JSON.stringify(iframeResponse.data) + "\n");
+ return iframeResponse.data;
+}
+
+let oopif_scroll_pos = function() {
+ dump(`OOPIF scroll position is y=${window.scrollY}\n`);
+ let result = { y: window.scrollY };
+ FissionTestHelper.fireEventInEmbedder("OOPIF:ScrollPos", result);
+ return true;
+};
+
+async function getIframeScrollY() {
+ let iframeElement = document.getElementById("testframe");
+ let iframePromise = promiseOneEvent(window, "OOPIF:ScrollPos", null);
+ ok(await FissionTestHelper.sendToOopif(iframeElement, `(${oopif_scroll_pos})()`), "Sent scrollY request");
+ let iframeResponse = await iframePromise;
+ dump("OOPIF response for scrollPos: " + JSON.stringify(iframeResponse.data) + "\n");
+ return iframeResponse.data.y;
+}
+
+let make_oopif_scrollable = function() {
+ // ensure the oopif is scrollable, and wait for the paint so that the
+ // compositor also knows it's scrollable.
+ document.body.style.height = "200vh";
+ promiseApzFlushedRepaints().then(() => {
+ let result = { y: window.scrollMaxY };
+ FissionTestHelper.fireEventInEmbedder("OOPIF:Scrollable", result);
+ });
+ // Also register a scroll listener for when it actually gets scrolled.
+ window.addEventListener("scroll", function(e) {
+ dump(`OOPIF got scroll event, now at ${window.scrollY}\n`);
+ let result = { y: window.scrollY };
+ FissionTestHelper.fireEventInEmbedder("OOPIF:Scrolled", result);
+ }, {once: true});
+ return true;
+};
+
+function failsafe(eventType) {
+ // Catch and fail faster on the case where the event ends up not going to
+ // the iframe like it should. Otherwise the test hangs until timeout which
+ // is more painful.
+ document.addEventListener(eventType, function(e) {
+ dump(`${location.href} got ${e.type} at ${e.clientX},${e.clientY}\n`);
+ ok(false, `The OOPIF hosting page should not have gotten the ${eventType}`);
+ setTimeout(FissionTestHelper.subtestFailed, 0);
+ }, {once: true});
+}
+
+// The actual test
+
+async function test() {
+ ok(SpecialPowers.getBoolPref("apz.paint_skipping.enabled"),
+ "paint-skipping is expected to be enabled for this test to be meaningful");
+
+ let iframeElement = document.getElementById("testframe");
+
+ let iframeResponse = await FissionTestHelper.sendToOopif(iframeElement, `(${code_for_oopif_to_run})()`);
+ dump("OOPIF response: " + JSON.stringify(iframeResponse) + "\n");
+ ok(iframeResponse, "code_for_oopif_to_run successfully installed");
+
+ is(window.scrollY, 0, "window is at 0 scroll position");
+
+ // hit-test into the iframe before scrolling
+ let oldClickPoint = await clickOnIframe(50, 250);
+
+ // do an APZ scroll and wait for the main-thread to get the repaint request,
+ // and queue up a paint-skip scroll notification back to APZ.
+ await promiseMoveMouseAndScrollWheelOver(document.body, 10, 10);
+
+ // The wheel scroll might have started an APZ animation, so run that to the end
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ for (var i = 0; i < 60; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ utils.restoreNormalRefresh();
+ // Let the repaint requests get processed
+ await promiseOnlyApzControllerFlushed();
+ await promiseAllPaintsDone();
+
+ ok(window.scrollY > 5, "window has scrolled by " + window.scrollY + " pixels");
+
+ // hit-test into the iframe after scrolling. The coordinates here are the
+ // same relative to the body as before, but get computed to be different
+ // relative to the window/screen.
+ let newClickPoint = await clickOnIframe(50, 250);
+
+ is(newClickPoint.x, oldClickPoint.x, "x-coord of old and new match");
+ is(newClickPoint.y, oldClickPoint.y, "y-coord of old and new match");
+
+ // Also check that we can send scroll events to the OOPIF. Any wheel events
+ // delivered to this page after this point should result in a failure.
+ failsafe("wheel");
+
+ let iframeY = await getIframeScrollY();
+ is(iframeY, 0, "scrollY of iframe should be 0 initially");
+
+ // Ensure the OOPIF is scrollable.
+ let scrollablePromise = promiseOneEvent(window, "OOPIF:Scrollable", null);
+ ok(await FissionTestHelper.sendToOopif(iframeElement, `(${make_oopif_scrollable})()`), "Made OOPIF scrollable");
+ let oopifScrollMaxY = (await scrollablePromise).data.y;
+ ok(oopifScrollMaxY > 0, "Confirmed that oopif is scrollable");
+
+ // Now scroll over the OOP-iframe (we know it must be under the 50,250 point
+ // because we just checked that above). Note that listening for wheel/scroll
+ // events is trickier because they will fire in the OOPIF, so we can't just
+ // use promiseMoveMouseAndScrollWheelOver directly.
+ let scrolledPromise = promiseOneEvent(window, "OOPIF:Scrolled", null);
+ await synthesizeNativeWheel(document.body, 50, 250, 0, -10);
+ iframeY = (await scrolledPromise).data.y;
+ ok(iframeY > 0, "scrollY of iframe should be >0 after scrolling");
+}
+
+ </script>
+</head>
+<body onload="failsafe('click')">
+<iframe style="margin-top: 200px" id="testframe"></iframe>
+<div style="height: 5000px">tall div to make the page scrollable</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_setResolution.html b/gfx/layers/apz/test/mochitest/helper_fission_setResolution.html
new file mode 100644
index 0000000000..6bcf3fa2ce
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_setResolution.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>setResolutionAndScaleTo is properly delivered to OOP iframes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+async function test() {
+ let iframeElement = document.getElementById("testframe");
+
+ const scale = 2.0;
+
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(scale);
+ await promiseApzFlushedRepaints(window);
+ await waitUntilApzStable();
+
+ const originalWidth = originalHeight = 100;
+ // eslint-disable-next-line no-unused-vars
+ let { _x, _y, width, height } = await SpecialPowers.spawn(
+ iframeElement,
+ [originalWidth, originalHeight],
+ // eslint-disable-next-line no-shadow
+ (width, height) => {
+ // nsIDOMWindowUtils.toScreenRect uses the iframe's transform which should
+ // have been informed from APZ.
+ return SpecialPowers.DOMWindowUtils.toScreenRect(0, 0, width, height);
+ }
+ );
+
+ is(width, scale * originalWidth,
+ "The resolution value should be properly delivered into OOP iframes");
+ is(height, scale * originalHeight,
+ "The resolution value should be properly delivered into OOP iframes");
+}
+
+ </script>
+ <style>
+ body, html {
+ margin: 0;
+ }
+ </style>
+</head>
+<body>
+<iframe id="testframe"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_tap.html b/gfx/layers/apz/test/mochitest/helper_fission_tap.html
new file mode 100644
index 0000000000..fab04bab26
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_tap.html
@@ -0,0 +1,87 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test to ensure events get untransformed properly for OOP iframes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+// Copied from helper_fission_transforms.html, except for the
+// synthesis function.
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+let code_for_oopif_to_run = function() {
+ document.addEventListener("click", function(e) {
+ dump(`OOPIF got click at ${e.clientX},${e.clientY}\n`);
+ let result = { x: e.clientX, y: e.clientY };
+ FissionTestHelper.fireEventInEmbedder("OOPIF:ClickData", result);
+ }, {once: true});
+ dump("OOPIF registered click listener\n");
+ return true;
+};
+
+function failsafe() {
+ // Catch and fail faster on the case where the click ends up not going to
+ // the iframe like it should. Otherwise the test hangs until timeout which
+ // is more painful.
+ document.addEventListener("click", function(e) {
+ dump(`${location.href} got click at ${e.clientX},${e.clientY}\n`);
+ ok(false, "The OOPIF hosting page should not have gotten the click");
+ setTimeout(FissionTestHelper.subtestDone, 0);
+ }, {once: true});
+}
+
+async function test() {
+ let iframeElement = document.getElementById("testframe");
+
+ let iframeResponse = await FissionTestHelper.sendToOopif(iframeElement, `(${code_for_oopif_to_run})()`)
+ dump("OOPIF response: " + JSON.stringify(iframeResponse) + "\n");
+ ok(iframeResponse, "code_for_oopif_to_run successfully installed");
+
+ iframePromise = promiseOneEvent(window, "OOPIF:ClickData", null);
+ await synthesizeNativeTap(document.body, 400, 400, function() {
+ dump("Finished synthesizing click, waiting for OOPIF message...\n");
+ });
+ iframeResponse = await iframePromise;
+ dump("OOPIF response: " + JSON.stringify(iframeResponse.data) + "\n");
+
+ let expected_coord = 200 / Math.sqrt(2); // because the iframe is rotated 45 deg
+ ok(Math.abs(iframeResponse.data.x - expected_coord) < 3,
+ `x-coord ${iframeResponse.data.x} landed near expected value ${expected_coord}`);
+ ok(Math.abs(iframeResponse.data.y - expected_coord) < 3,
+ `y-coord ${iframeResponse.data.y} landed near expected value ${expected_coord}`);
+}
+
+ </script>
+ <style>
+ body, html {
+ margin: 0;
+ }
+ div {
+ transform-origin: top left;
+ transform: translateX(400px) scale(2) rotate(45deg);
+ width: 500px;
+ }
+ iframe {
+ width: 400px;
+ height: 300px;
+ border: solid 1px black;
+ }
+ </style>
+</head>
+<body onload="failsafe()">
+<div><iframe id="testframe"></iframe></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_tap_in_nested_iframe_on_zoomed.html b/gfx/layers/apz/test/mochitest/helper_fission_tap_in_nested_iframe_on_zoomed.html
new file mode 100644
index 0000000000..47a9c4bf8e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_tap_in_nested_iframe_on_zoomed.html
@@ -0,0 +1,106 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test to ensure events get delivered properly for a nested OOP iframe</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+// Copied from helper_fission_tap_on_zoomed.html. In this test
+// SpecialPowers.spawn is used instead of FissionTestHelper.sendToOopif to
+// handle scripts in a nested OOP iframe.
+
+fission_subtest_init();
+
+SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+function failsafe() {
+ // Catch and fail faster on the case where the click ends up not going to
+ // the iframe like it should. Otherwise the test hangs until timeout which
+ // is more painful.
+ document.addEventListener("click", function(e) {
+ dump(`${location.href} got click at ${e.clientX},${e.clientY}\n`);
+ ok(false, "The OOPIF hosting page should not have gotten the click");
+ setTimeout(FissionTestHelper.subtestDone, 0);
+ }, {once: true});
+}
+
+async function test() {
+ let iframeElement = document.getElementById("testframe");
+
+ // Load another OOP document in the parent OOP iframe.
+ await SpecialPowers.spawn(iframeElement, [], async () => {
+ const iframe = content.document.createElement("iframe");
+ iframe.src =
+ "https://example.org/browser/gfx/layers/apz/test/mochitest/helper_fission_empty.html";
+ iframe.style.width = "400px";
+ iframe.style.height = "300px";
+ iframe.style.border = "none";
+ content.document.body.appendChild(iframe);
+ await new Promise(resolve => {
+ iframe.addEventListener("load", resolve, {once: true});
+ });
+ await SpecialPowers.spawn(iframe, [], async () => {
+ await content.wrappedJSObject.promiseApzFlushedRepaints(content.window);
+ });
+ });
+
+ // Set a click event listener in the nested OOP document.
+ const iframePromise = SpecialPowers.spawn(iframeElement, [], async () => {
+ const iframe = content.document.querySelector("iframe");
+ const result = await SpecialPowers.spawn(iframe, [], async () => {
+ return new Promise(resolve => {
+ content.document.addEventListener("click", e => {
+ dump(`OOPIF got click at ${e.clientX},${e.clientY}\n`);
+ resolve({ x: e.clientX, y: e.clientY });
+ }, {once: true});
+ });
+ });
+ return result;
+ });
+
+ await synthesizeNativeTap(document.documentElement, 200, 200, function() {
+ dump("Finished synthesizing click, waiting for OOPIF message...\n");
+ });
+ let iframeResponse = await iframePromise;
+ dump("OOPIF response: " + JSON.stringify(iframeResponse) + "\n");
+
+ let expected_coord = 100; // because the parent iframe is offseted by (100, 100).
+ ok(Math.abs(iframeResponse.x - expected_coord) < 3,
+ `x-coord ${iframeResponse.x} landed near expected value ${expected_coord}`);
+ ok(Math.abs(iframeResponse.y - expected_coord) < 3,
+ `y-coord ${iframeResponse.y} landed near expected value ${expected_coord}`);
+}
+
+ </script>
+ <style>
+ body, html {
+ margin: 0;
+ }
+ div {
+ margin-left: 100px;
+ margin-top: 100px;
+ width: 500px;
+ }
+ iframe {
+ width: 400px;
+ height: 300px;
+ border: solid 1px black;
+ }
+ </style>
+</head>
+<body onload="failsafe()">
+<div><iframe id="testframe"></iframe></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_tap_on_zoomed.html b/gfx/layers/apz/test/mochitest/helper_fission_tap_on_zoomed.html
new file mode 100644
index 0000000000..5d99b972c2
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_tap_on_zoomed.html
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test to ensure events get delivered properly for an OOP iframe</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+// Copied from helper_fission_touch.html, differences are 1) the iframe is not
+// transformed instead it's offseted by margin values, 2) the top level document
+// is zoomed by 2.0, 3) using documentElement instead of body to query
+// getBoundingClientRect() because margin collapsing happens between the body
+// and the offseted div (i.e. getBoundingClientRect() for body returns 100px top
+// value).
+
+fission_subtest_init();
+
+SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+let code_for_oopif_to_run = function() {
+ document.addEventListener("click", function(e) {
+ dump(`OOPIF got click at ${e.clientX},${e.clientY}\n`);
+ let result = { x: e.clientX, y: e.clientY };
+ FissionTestHelper.fireEventInEmbedder("OOPIF:ClickData", result);
+ }, {once: true});
+ dump("OOPIF registered click listener\n");
+ return true;
+};
+
+function failsafe() {
+ // Catch and fail faster on the case where the click ends up not going to
+ // the iframe like it should. Otherwise the test hangs until timeout which
+ // is more painful.
+ document.addEventListener("click", function(e) {
+ dump(`${location.href} got click at ${e.clientX},${e.clientY}\n`);
+ ok(false, "The OOPIF hosting page should not have gotten the click");
+ setTimeout(FissionTestHelper.subtestDone, 0);
+ }, {once: true});
+}
+
+async function test() {
+ let iframeElement = document.getElementById("testframe");
+
+ let iframeResponse = await FissionTestHelper.sendToOopif(iframeElement, `(${code_for_oopif_to_run})()`)
+ dump("OOPIF response: " + JSON.stringify(iframeResponse) + "\n");
+ ok(iframeResponse, "code_for_oopif_to_run successfully installed");
+
+ iframePromise = promiseOneEvent(window, "OOPIF:ClickData", null);
+ await synthesizeNativeTap(document.documentElement, 200, 200, function() {
+ dump("Finished synthesizing click, waiting for OOPIF message...\n");
+ });
+ iframeResponse = await iframePromise;
+ dump("OOPIF response: " + JSON.stringify(iframeResponse.data) + "\n");
+
+ let expected_coord = 100; // because the iframe is offseted by (100, 100).
+ ok(Math.abs(iframeResponse.data.x - expected_coord) < 3,
+ `x-coord ${iframeResponse.data.x} landed near expected value ${expected_coord}`);
+ ok(Math.abs(iframeResponse.data.y - expected_coord) < 3,
+ `y-coord ${iframeResponse.data.y} landed near expected value ${expected_coord}`);
+}
+
+ </script>
+ <style>
+ body, html {
+ margin: 0;
+ }
+ div {
+ margin-left: 100px;
+ margin-top: 100px;
+ width: 500px;
+ }
+ iframe {
+ width: 400px;
+ height: 300px;
+ border: solid 1px black;
+ }
+ </style>
+</head>
+<body onload="failsafe()">
+<div><iframe id="testframe"></iframe></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_touch.html b/gfx/layers/apz/test/mochitest/helper_fission_touch.html
new file mode 100644
index 0000000000..fa317b9f1f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_touch.html
@@ -0,0 +1,99 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test to ensure touch events for OOP iframes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+
+let code_for_oopif_to_run = function() {
+ let listener = function(e) {
+ let result = { type: e.type, touches: [] };
+ dump(`OOPIF got ${e.type}\n`);
+ for (let touch of e.touches) {
+ result.touches.push({
+ identifier: touch.identifier,
+ clientX: touch.clientX,
+ clientY: touch.clientY
+ });
+ dump(` identifier ${touch.identifier} at ${touch.clientX},${touch.clientY}\n`);
+ }
+ FissionTestHelper.fireEventInEmbedder("OOPIF:TouchEvent", result);
+ };
+ document.addEventListener("touchstart", listener, {once: true});
+ document.addEventListener("touchmove", listener, {once: true});
+ document.addEventListener("touchend", listener, {once: true});
+ dump("OOPIF registered touch listener\n");
+ return true;
+};
+
+function failsafe() {
+ let failListener = function(e) {
+ dump(`${location.href} got ${e.type}\n`);
+ ok(false, `The OOPIF hosting page should not have gotten the ${e.type}`);
+ setTimeout(FissionTestHelper.subtestDone, 0);
+ };
+ // Catch and fail faster on the case where the touch event ends up not going
+ // to the iframe like it should. Otherwise the test hangs until timeout which
+ // is more painful.
+ document.addEventListener("touchstart", failListener, {once: true});
+ document.addEventListener("touchmove", failListener, {once: true});
+ document.addEventListener("touchend", failListener, {once: true});
+}
+
+function waitForTouchEvent(aType) {
+ return promiseOneEvent(window, "OOPIF:TouchEvent", function(e) {
+ return e.data.type === aType;
+ });
+}
+
+async function test() {
+ let iframeElement = document.getElementById("testframe");
+
+ let iframeResponse = await FissionTestHelper.sendToOopif(iframeElement, `(${code_for_oopif_to_run})()`);
+ dump("OOPIF response: " + JSON.stringify(iframeResponse) + "\n");
+ ok(iframeResponse, "code_for_oopif_to_run successfully installed");
+
+ iframePromise = Promise.all([waitForTouchEvent("touchstart"),
+ waitForTouchEvent("touchmove"),
+ waitForTouchEvent("touchend")]);
+ await synthesizeNativeTouchSequences(document.body,
+ [[{x: 100, y: 100}], [{x: 150, y: 150}], [{x: 150, y: 150}]], function() {
+ dump("Finished synthesizing touch tap, waiting for OOPIF message...\n");
+ });
+ await iframePromise;
+}
+
+ </script>
+ <style>
+ body, html {
+ margin: 0;
+ }
+ div {
+ width: 500px;
+ }
+ iframe {
+ width: 400px;
+ height: 300px;
+ border: solid 1px black;
+ }
+ </style>
+</head>
+<body onload="failsafe()">
+<div><iframe id="testframe"></iframe></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_transforms.html b/gfx/layers/apz/test/mochitest/helper_fission_transforms.html
new file mode 100644
index 0000000000..193b5650fd
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_transforms.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test to ensure events get untransformed properly for OOP iframes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+
+let code_for_oopif_to_run = function() {
+ document.addEventListener("click", function(e) {
+ dump(`OOPIF got click at ${e.clientX},${e.clientY}\n`);
+ let result = { x: e.clientX, y: e.clientY };
+ FissionTestHelper.fireEventInEmbedder("OOPIF:ClickData", result);
+ }, {once: true});
+ dump("OOPIF registered click listener\n");
+ return true;
+};
+
+function failsafe() {
+ // Catch and fail faster on the case where the click ends up not going to
+ // the iframe like it should. Otherwise the test hangs until timeout which
+ // is more painful.
+ document.addEventListener("click", function(e) {
+ dump(`${location.href} got click at ${e.clientX},${e.clientY}\n`);
+ ok(false, "The OOPIF hosting page should not have gotten the click");
+ setTimeout(FissionTestHelper.subtestDone, 0);
+ }, {once: true});
+}
+
+async function test() {
+ let iframeElement = document.getElementById("testframe");
+
+ let iframeResponse = await FissionTestHelper.sendToOopif(iframeElement, `(${code_for_oopif_to_run})()`);
+ dump("OOPIF response: " + JSON.stringify(iframeResponse) + "\n");
+ ok(iframeResponse, "code_for_oopif_to_run successfully installed");
+
+ iframePromise = promiseOneEvent(window, "OOPIF:ClickData", null);
+ await synthesizeNativeMouseEventWithAPZ(
+ { type: "click", target: document.body, offsetX: 400, offsetY: 400 },
+ () => dump("Finished synthesizing click, waiting for OOPIF message...\n")
+ );
+ iframeResponse = await iframePromise;
+ dump("OOPIF response: " + JSON.stringify(iframeResponse.data) + "\n");
+
+ let expected_coord = 200 / Math.sqrt(2); // because the iframe is rotated 45 deg
+ ok(Math.abs(iframeResponse.data.x - expected_coord) < 3,
+ `x-coord ${iframeResponse.data.x} landed near expected value ${expected_coord}`);
+ ok(Math.abs(iframeResponse.data.y - expected_coord) < 3,
+ `y-coord ${iframeResponse.data.y} landed near expected value ${expected_coord}`);
+}
+
+ </script>
+ <style>
+ body, html {
+ margin: 0;
+ }
+ div {
+ transform-origin: top left;
+ transform: translateX(400px) scale(2) rotate(45deg) translate(-50px, -50px);
+ width: 500px;
+ }
+ iframe {
+ width: 400px;
+ height: 300px;
+ position: absolute;
+ top: 50px;
+ left: 50px;
+ border: solid 1px black;
+ }
+ </style>
+</head>
+<body onload="failsafe()">
+<div><iframe id="testframe"></iframe></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_utils.js b/gfx/layers/apz/test/mochitest/helper_fission_utils.js
new file mode 100644
index 0000000000..ddcab62253
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_utils.js
@@ -0,0 +1,130 @@
+// loadOOPIFrame expects apz_test_utils.js to be loaded as well, for promiseOneEvent.
+/* import-globals-from apz_test_utils.js */
+
+function fission_subtest_init() {
+ // Silence SimpleTest warning about missing assertions by having it wait
+ // indefinitely. We don't need to give it an explicit finish because the
+ // entire window this test runs in will be closed after subtestDone is called.
+ SimpleTest.waitForExplicitFinish();
+
+ // This is the point at which we inject the ok, is, subtestDone, etc. functions
+ // into this window. In particular this function should run after SimpleTest.js
+ // is imported, otherwise SimpleTest.js will clobber the functions with its
+ // own versions. This is implicitly enforced because if we call this function
+ // before SimpleTest.js is imported, the above line will throw an exception.
+ window.dispatchEvent(new Event("FissionTestHelper:Init"));
+}
+
+/**
+ * Starts loading the given `iframePage` in the iframe element with the given
+ * id, and waits for it to load.
+ * Note that calling this function doesn't do the load directly; instead it
+ * returns an async function which can be added to a thenable chain.
+ */
+function loadOOPIFrame(iframeElementId, iframePage) {
+ return async function () {
+ if (window.location.href.startsWith("https://example.com/")) {
+ dump(
+ `WARNING: Calling loadOOPIFrame from ${window.location.href} so the iframe may not be OOP\n`
+ );
+ ok(false, "Current origin is not example.com:443");
+ }
+
+ let url =
+ "https://example.com/browser/gfx/layers/apz/test/mochitest/" + iframePage;
+ let loadPromise = promiseOneEvent(window, "OOPIF:Load", function (e) {
+ return typeof e.data.content == "string" && e.data.content == url;
+ });
+ let elem = document.getElementById(iframeElementId);
+ elem.src = url;
+ await loadPromise;
+ };
+}
+
+/**
+ * This is similar to the hitTest function in apz_test_utils.js, in that it
+ * does a hit-test for a point and returns the result. The difference is that
+ * in the fission world, the hit-test may land on an OOPIF, which means the
+ * result information will be in the APZ test data for the OOPIF process. This
+ * function checks both the current process and OOPIF process to see which one
+ * got a hit result, and returns the result regardless of which process got it.
+ * The caller is expected to check the layers id which will allow distinguishing
+ * the two cases.
+ */
+async function fissionHitTest(point, iframeElement) {
+ let get_iframe_compositor_test_data = function () {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ return JSON.stringify(utils.getCompositorAPZTestData());
+ };
+
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ // Get the test data before doing the actual hit-test, to get a baseline
+ // of what we can ignore.
+ let oldParentTestData = utils.getCompositorAPZTestData();
+ let oldIframeTestData = JSON.parse(
+ await FissionTestHelper.sendToOopif(
+ iframeElement,
+ `(${get_iframe_compositor_test_data})()`
+ )
+ );
+
+ // Now do the hit-test
+ dump(`Hit-testing point (${point.x}, ${point.y}) in fission context\n`);
+ utils.sendMouseEvent(
+ "MozMouseHittest",
+ point.x,
+ point.y,
+ 0,
+ 0,
+ 0,
+ true,
+ 0,
+ 0,
+ true,
+ true
+ );
+
+ // Collect the new test data
+ let newParentTestData = utils.getCompositorAPZTestData();
+ let newIframeTestData = JSON.parse(
+ await FissionTestHelper.sendToOopif(
+ iframeElement,
+ `(${get_iframe_compositor_test_data})()`
+ )
+ );
+
+ // See which test data has new hit results
+ let hitResultCount = function (testData) {
+ return Object.keys(testData.hitResults).length;
+ };
+
+ let hitIframe =
+ hitResultCount(newIframeTestData) > hitResultCount(oldIframeTestData);
+ let hitParent =
+ hitResultCount(newParentTestData) > hitResultCount(oldParentTestData);
+
+ // Extract the results from the appropriate test data
+ let lastHitResult = function (testData) {
+ let lastHit =
+ testData.hitResults[Object.keys(testData.hitResults).length - 1];
+ return {
+ hitInfo: lastHit.hitResult,
+ scrollId: lastHit.scrollId,
+ layersId: lastHit.layersId,
+ };
+ };
+ if (hitIframe && hitParent) {
+ throw new Error(
+ "Both iframe and parent got hit-results, that is unexpected!"
+ );
+ } else if (hitIframe) {
+ return lastHitResult(newIframeTestData);
+ } else if (hitParent) {
+ return lastHitResult(newParentTestData);
+ } else {
+ throw new Error(
+ "Neither iframe nor parent got the hit-result, that is unexpected!"
+ );
+ }
+}
diff --git a/gfx/layers/apz/test/mochitest/helper_fixed_html_hittest.html b/gfx/layers/apz/test/mochitest/helper_fixed_html_hittest.html
new file mode 100644
index 0000000000..27add6debb
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fixed_html_hittest.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Hittest position:fixed zoomed scroll</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html {
+ position: fixed;
+ }
+ body {
+ margin: 0;
+ }
+ #fixed {
+ position: fixed;
+ top: 100px;
+ left: 100px;
+ }
+ </style>
+</head>
+<body>
+ <div id="fixed"><input type="button" value="Button" /></div>
+ <script>
+ async function test() {
+ // Create an offset between the visual and layout viewports.
+ // The offset is 50 CSS pixels = 100 screen pixels at 2x zoom
+ // in either direction.
+ let transformEndPromise = promiseTransformEnd();
+ await synthesizeNativeTouchDrag(document.body, 10, 10, -50, -50);
+ await transformEndPromise;
+
+ await promiseApzFlushedRepaints();
+
+ let clickPromise = new Promise(resolve => {
+ window.addEventListener("click", resolve);
+ });
+ let input = document.querySelector("input");
+ // Provide the input in window-relative coordinates,
+ // otherwise coordinatesRelativeToScreen() will run into the
+ // same bug as the hit-test, and the two bugs cancel out.
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: window,
+ // The visual viewport is already offset (50, 50) CSS pixels
+ // into the layout viewport. An additional (50, 50) CSS pixels
+ // gives us (100, 100), the offset of #fixed, in total.
+ offsetX: 55,
+ offsetY: 55
+ });
+ let e = await clickPromise;
+ is(e.target, input, "got click");
+ }
+
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fixed_pos_displayport.html b/gfx/layers/apz/test/mochitest/helper_fixed_pos_displayport.html
new file mode 100644
index 0000000000..adae691096
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fixed_pos_displayport.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; minimum-scale=1.0">
+ <title>position:fixed display port sizing</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html, body {
+ margin: 0;
+ /* This makes sure the `height: 1000%` on #scrolled actually has an effect. */
+ height: 100%;
+ }
+ #fixed {
+ position: fixed;
+ left: 0;
+ height: 100%;
+ width: 300px;
+ background: linear-gradient(135deg, white, black);
+ }
+ /* This makes sure we have a layout scroll range. */
+ #scrolled {
+ width: 300px;
+ height: 1000%;
+ }
+ </style>
+</head>
+<body>
+ <div id="fixed"></div>
+ <div id="scrolled"></div>
+ <script>
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ let vv = window.visualViewport;
+
+ // Get the displayport of the fixed-position element as of the last paint.
+ function getCurrentFixedPosDisplayport() {
+ let data = convertEntries(utils.getContentAPZTestData().additionalData);
+ let key = "fixedPosDisplayport";
+ ok(key in data, "should have computed a fixed-pos display port");
+ return parseRect(data[key]);
+ }
+
+ async function scrollToVisual(targetX, targetY) {
+ let scrollPromise = new Promise(resolve => {
+ vv.addEventListener("scroll", resolve, { once: true });
+ });
+ utils.scrollToVisual(targetX, targetY, utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_INSTANT);
+ await scrollPromise;
+ await promiseApzFlushedRepaints();
+ // Allow up to 1 pixel discrepancy due to floating-point error.
+ isfuzzy(vv.pageLeft, targetX, 1, "visual-scrolled horizontally as expected");
+ isfuzzy(vv.pageTop, targetY, 1, "visual-scrolled vertically as expected");
+ }
+
+ // Check that the size and position of the fixed-pos displayport matches
+ // our expectations.
+ function checkFixedPosDisplayport() {
+ let fixedPosDisplayport = getCurrentFixedPosDisplayport();
+
+ // First, check check that we don't expand the displayport to the entire layout viewport
+ // even if we are zoomed in a lot.
+ ok(fixedPosDisplayport.width < window.innerWidth, "fixed-pos displayport is too wide");
+ ok(fixedPosDisplayport.height < window.innerHeight, "fixed-pos displayport is too tall");
+
+ // Now, check the position. We want it to track the visual scroll position
+ // relative to the layout viewport (but not relative to the page), since
+ // fixed-position elements are attached to the layout viewport.
+ // This is accomplished by checking the fixed-pos display port contains
+ // the visual viewport rect as expressed relative to the layout viewport.
+ let vvRect = { x: vv.offsetLeft, // offsets relative to layout viewport
+ y: vv.offsetTop,
+ width: vv.width,
+ height: vv.height };
+ assertRectContainment(fixedPosDisplayport, "fixed-pos displayport",
+ vvRect, "visual viewport");
+ }
+
+ async function test() {
+ // First, check size and position on page load.
+ checkFixedPosDisplayport();
+
+ // Scroll the visual viewport within the layout viewport, without
+ // scrolling the layout viewport itself, and check the size and
+ // position again.
+ await scrollToVisual(vv.width * 3, vv.height * 3);
+ checkFixedPosDisplayport();
+
+ // Finally, scroll the visual viewport farther so as to cause the
+ // layout viewport to scroll as well, and check the size and position
+ // once more.
+ await scrollToVisual(vv.width * 3, vv.height * 30);
+ checkFixedPosDisplayport();
+ }
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(8.0);
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fixed_position_scroll_hittest.html b/gfx/layers/apz/test/mochitest/helper_fixed_position_scroll_hittest.html
new file mode 100644
index 0000000000..05cc2d0262
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fixed_position_scroll_hittest.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Hittest position:fixed zoomed scroll</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ margin: 0;
+ }
+ #fixed {
+ position: fixed;
+ height: 30px;
+ width: 100%;
+ background: linear-gradient(135deg, white, black);
+ }
+ #fixed > input {
+ position: absolute;
+ top: 0;
+ right: 0;
+ height: 100%;
+ }
+ </style>
+</head>
+<body>
+ <div id="fixed"><input type="button" value="Button" /></div>
+ <script>
+ async function test() {
+ let transformEndPromise = promiseTransformEnd();
+ await synthesizeNativeTouchDrag(document.body, 10, 10, -2000, 0);
+ await transformEndPromise;
+
+ await promiseApzFlushedRepaints();
+
+ let clickPromise = new Promise(resolve => {
+ window.addEventListener("click", resolve);
+ });
+ let input = document.querySelector("input");
+ await synthesizeNativeMouseEventWithAPZ({ type: "click", target: input, offsetX: 10, offsetY: 10 });
+ let e = await clickPromise;
+ is(e.target, input, "got click");
+ }
+
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fullscreen.html b/gfx/layers/apz/test/mochitest/helper_fullscreen.html
new file mode 100644
index 0000000000..32de4979f2
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fullscreen.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Tests that layout viewport is not larger than visual viewport on fullscreen</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ margin: 0;
+ padding: 0;
+ overflow-x: hidden;
+ }
+ </style>
+</head>
+<body>
+ <div style="background: blue; width: 100%; height: 100%;"></div>
+ <div style="background: red; width: 200%; height: 100px;">overflowed element</div>
+ <div id="target" style="background: green; width: 100px; height: 100px;"></div>
+ <script type="application/javascript">
+ const utils = SpecialPowers.getDOMWindowUtils(window);
+
+ function waitForFullscreenChange() {
+ return new Promise(resolve => {
+ document.addEventListener("fullscreenchange", resolve);
+ });
+ }
+
+ async function test(testDriver) {
+ target.requestFullscreen();
+
+ await waitForFullscreenChange();
+
+ is(document.fullscreenElement, target,
+ "The target element should have been fullscreen-ed");
+
+ // Try to move rightward, but it should NOT happen.
+ utils.scrollToVisual(200, 0, utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_INSTANT);
+
+ await waitUntilApzStable();
+
+ is(visualViewport.offsetLeft, 0,
+ "The visual viewport offset should never be moved");
+
+ document.exitFullscreen();
+ }
+
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_backface_hidden.html b/gfx/layers/apz/test/mochitest/helper_hittest_backface_hidden.html
new file mode 100644
index 0000000000..0e84282e54
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_backface_hidden.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ hit-testing with backface-visibility:hidden</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ body,html{
+ height: 100%;
+ }
+ body{
+ margin: 0;
+ transform-style: preserve-3d;
+ }
+ #back, #front{
+ backface-visibility: hidden;
+ position: absolute;
+ width: 100%;
+ height: 100%
+ }
+ #front{
+ overflow-y:auto;
+ }
+ #content{
+ width: 100%;
+ height: 200%;
+ background: linear-gradient(blue, green);
+ }
+ #back{
+ transform: rotateY(180deg);
+ }
+ </style>
+</head>
+<body>
+ <div id="front">
+ <div id="content"></div>
+ </div>
+ <div id="back"></div></body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+
+ var subframe = document.getElementById("front");
+
+ // Set a displayport to ensure the subframe is layerized.
+ // This is not required for exercising the behavior we want to test,
+ // but it's needed to be able to assert the results reliably.
+ config.utils.setDisplayPortForElement(0, 0, 1000, 1000, subframe, 1);
+ await promiseApzFlushedRepaints();
+
+ var subframeViewId = config.utils.getViewId(subframe);
+
+ var {scrollId} = hitTest(centerOf(subframe));
+
+ is(scrollId, subframeViewId,
+ "hit the scroll frame behind the backface-visibility:hidden element");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_basic.html b/gfx/layers/apz/test/mochitest/helper_hittest_basic.html
new file mode 100644
index 0000000000..a9e9f0c07f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_basic.html
@@ -0,0 +1,141 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Various tests to exercise the APZ hit-testing codepaths</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+</head>
+<body>
+ <div id="scroller" style="width: 300px; height: 300px; overflow:scroll; margin-top: 100px; margin-left: 50px">
+ <div id="contents" style="width: 500px; height: 500px; background-image: linear-gradient(blue,red)">
+ <div id="apzaware" style="position: relative; width: 100px; height: 100px; top: 300px; background-color: red" onwheel="return false;"></div>
+ </div>
+ </div>
+ <div id="make_root_scrollable" style="height: 5000px"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ var scroller = document.getElementById("scroller");
+ var apzaware = document.getElementById("apzaware");
+
+ let expectedHitInfo = APZHitResultFlags.VISIBLE;
+ if (!config.activateAllScrollFrames) {
+ expectedHitInfo |= APZHitResultFlags.INACTIVE_SCROLLFRAME;
+ }
+ checkHitResult(hitTest(centerOf(scroller)),
+ expectedHitInfo,
+ (config.activateAllScrollFrames ? utils.getViewId(scroller)
+ : utils.getViewId(document.scrollingElement)),
+ utils.getLayersId(),
+ "inactive scrollframe");
+
+ // The apz-aware div (which has a non-passive wheel listener) is not visible
+ // and so the hit-test should just return the root scrollframe area that's
+ // covering it
+ checkHitResult(hitTest(centerOf(apzaware)),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "inactive scrollframe - apzaware block");
+
+ // Hit test where the scroll thumbs should be.
+ hitTestScrollbar({
+ element: scroller,
+ directions: { vertical: true, horizontal: true },
+ expectedScrollId: utils.getViewId(document.scrollingElement),
+ expectedLayersId: utils.getLayersId(),
+ trackLocation: ScrollbarTrackLocation.START,
+ expectThumb: true,
+ layerState: LayerState.INACTIVE,
+ });
+
+ // activate the scrollframe but keep the main-thread scroll position at 0.
+ // also apply a async scroll offset in the y-direction such that the
+ // scrollframe scrolls to the bottom of its range.
+ utils.setDisplayPortForElement(0, 0, 500, 500, scroller, 1);
+ await promiseApzFlushedRepaints();
+ var scrollY = scroller.scrollTopMax;
+ utils.setAsyncScrollOffset(scroller, 0, scrollY);
+ // Tick the refresh driver once to make sure the compositor has applied the
+ // async scroll offset (for WebRender hit-testing we need to make sure WR has
+ // the latest info).
+ utils.advanceTimeAndRefresh(16);
+ utils.restoreNormalRefresh();
+
+ var scrollerViewId = utils.getViewId(scroller);
+
+ // Now we again test the middle of the scrollframe, which is now active
+ checkHitResult(hitTest(centerOf(scroller)),
+ APZHitResultFlags.VISIBLE,
+ scrollerViewId,
+ utils.getLayersId(),
+ "active scrollframe");
+
+ // Test the apz-aware block
+ var apzawarePosition = centerOf(apzaware); // main thread position
+ apzawarePosition.y -= scrollY; // APZ position
+ checkHitResult(hitTest(apzawarePosition),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.APZ_AWARE_LISTENERS,
+ scrollerViewId,
+ utils.getLayersId(),
+ "active scrollframe - apzaware block");
+
+ // Test the scrollbars. Note that this time the vertical scrollthumb is
+ // going to be at the bottom of the track. We'll test both the top and the
+ // bottom.
+
+ // top of scrollbar track
+ hitTestScrollbar({
+ element: scroller,
+ directions: { vertical: true },
+ expectedScrollId: scrollerViewId,
+ expectedLayersId: utils.getLayersId(),
+ trackLocation: ScrollbarTrackLocation.START,
+ expectThumb: false,
+ layerState: LayerState.ACTIVE,
+ });
+ // bottom of scrollbar track (scrollthumb)
+ hitTestScrollbar({
+ element: scroller,
+ directions: { vertical: true },
+ expectedScrollId: scrollerViewId,
+ expectedLayersId: utils.getLayersId(),
+ trackLocation: ScrollbarTrackLocation.END,
+ expectThumb: true,
+ layerState: LayerState.ACTIVE,
+ });
+ // left part of scrollbar track (has scrollthumb)
+ hitTestScrollbar({
+ element: scroller,
+ directions: { horizontal: true },
+ expectedScrollId: scrollerViewId,
+ expectedLayersId: utils.getLayersId(),
+ trackLocation: ScrollbarTrackLocation.START,
+ expectThumb: true,
+ layerState: LayerState.ACTIVE,
+ });
+ // right part of scrollbar track
+ hitTestScrollbar({
+ element: scroller,
+ directions: { horizontal: true },
+ expectedScrollId: scrollerViewId,
+ expectedLayersId: utils.getLayersId(),
+ trackLocation: ScrollbarTrackLocation.END,
+ expectThumb: false,
+ layerState: LayerState.ACTIVE,
+ });
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1119497.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1119497.html
new file mode 100644
index 0000000000..1d1e0a922f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1119497.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>A hit testing test for the scenario in bug 1119497</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ #scrollbox {
+ width: 200px;
+ height: 200px;
+ overflow: auto;
+ }
+ #scrolled {
+ width: 400px;
+ height: 400px;
+ }
+ #cover {
+ width: 300px;
+ height: 300px;
+ position: fixed;
+ left: 0px;
+ top: 0px;
+ }
+ </style>
+</head>
+<body>
+ <div id="scrollbox">
+ <div id="scrolled"></div>
+ </div>
+ <div id="cover"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // Check that hit-testing on an element that's not scrolled by
+ // anything ("cover") hits the root scroller.
+ checkHitResult(hitTest({x: 50, y: 50}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "root scroller");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1257288.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1257288.html
new file mode 100644
index 0000000000..d9b813dddf
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1257288.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>A hit testing test for the scenario in bug 1257288</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ html {
+ background: radial-gradient(circle at 80px 450px, blue 5px, transparent 0) gray no-repeat;
+ height: 100%;
+ }
+
+ #scrollbox {
+ border: 1px solid black;
+ width: 400px;
+ height: 400px;
+ margin: 20px;
+ overflow: auto;
+ background-color: white;
+ }
+
+ #scrolled {
+ padding-top: 300px;
+ padding-bottom: 300px;
+ }
+
+ #clip {
+ overflow: hidden;
+ margin: 10px;
+ }
+
+ #transform {
+ background-color: red;
+ width: 200px;
+ height: 200px;
+ will-change: transform;
+ transform: rotate(45deg);
+ position: relative;
+ left: -100px;
+ }
+ </style>
+</head>
+<body>
+ <div id="scrollbox">
+ <div id="scrolled">
+ <div id="clip">
+ <div id="transform"></div>
+ </div>
+ </div>
+ </div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // Check that hit-testing on the blue circle (located at (80, 450))
+ // hits the root, not the subframe.
+ checkHitResult(hitTest({x: 80, y: 450}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "root scroller should be hit, not subframe");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1715187.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715187.html
new file mode 100644
index 0000000000..6cb58b6413
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715187.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check hittesting fission oop iframe with transform and pinch zoom works bug 1715187</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function test() {
+ let initial_resolution = await getResolution();
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+
+ // Zoom in
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0*initial_resolution);
+ await promiseApzFlushedRepaints();
+
+ let resolution = await getResolution();
+ ok(resolution > 1.5*initial_resolution,
+ "The resolution is " + resolution + ", after zooming in");
+
+
+ let clickPromise = new Promise(resolve => {
+ window.addEventListener("message", event => {
+ if (event.data == "gotclick") {
+ ok(true, "got click");
+ resolve();
+ }
+ })
+ });
+
+
+ let thetarget = document.getElementById("theiframe");
+ await synthesizeNativeMouseEventWithAPZ({ type: "click", target: thetarget, offsetX: 5, offsetY: 5 });
+ info("sent click");
+
+ await clickPromise;
+
+ ok(true, "must have got click");
+
+ // Restore
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(initial_resolution);
+ await promiseApzFlushedRepaints();
+
+ resolution = await getResolution();
+ ok(resolution == initial_resolution,
+ "The resolution is " + resolution + ", after restoring");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+<style>
+body {
+ padding-left: 200px;
+}
+</style>
+</head>
+<body>
+<div style="position: absolute; left: 350px; width: 400px; height: 400px; transform: scale(2,1)">
+ <iframe id="theiframe" style="border: 1px;" frameborder="1" src="http://example.org/tests/gfx/layers/apz/test/mochitest/helper_hittest_bug1715187_oopif.html"></iframe>
+</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1715187_oopif.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715187_oopif.html
new file mode 100644
index 0000000000..c0f5baf467
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715187_oopif.html
@@ -0,0 +1,13 @@
+<style>
+body, html { margin: 0; }
+</style>
+<div id="thediv" style="position: absolute; top: 0; left: 0; width: 10px; height: 10px; background: blue;">
+</div>
+<div style="width:10px; height: 500vh;"></div>
+<script>
+let target = document.getElementById("thediv");
+target.addEventListener("click", listener);
+function listener() {
+ parent.postMessage("gotclick", "*");
+}
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369.html
new file mode 100644
index 0000000000..d1128fa946
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check hittesting fission oop iframe with transform works bug 1715369</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function makeActive(x, y, targetId) {
+ let theTarget = document.getElementById(targetId);
+ await promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "click",
+ target: theTarget,
+ offsetX: x,
+ offsetY: y,
+ });
+
+ await promiseApzFlushedRepaints();
+
+ ok(isLayerized(targetId), "target should be layerized at this point");
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ let targetScrollId = utils.getViewId(theTarget);
+ ok(targetScrollId > 0, "target should have a scroll id");
+}
+
+async function test() {
+ await makeActive(20, 20, "scrollable");
+
+ let clickPromise = new Promise(resolve => {
+ window.addEventListener("message", event => {
+ if (event.data == "gotclick") {
+ ok(true, "got click");
+ resolve();
+ }
+ })
+ });
+
+
+ let thetarget = document.getElementById("theiframe");
+ await synthesizeNativeMouseEventWithAPZ({ type: "click", target: thetarget, offsetX: 25, offsetY: 25 });
+ info("sent click");
+
+ await clickPromise;
+
+ ok(true, "must have got click");
+
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+<style>
+</style>
+</head>
+<body>
+
+<!--transform
+asr change
+in process iframe/stackingcontext
+oopif-->
+<div style="height: 100px; width: 100px; transform: scale(3); transform-origin: top left;">
+ <div id="scrollable" style="overflow: scroll; height: 200px;">
+ <div id="topspacer" style="height: 50px;"></div>
+ <iframe id="theiframe" style="border: 1px;" frameborder="1" src="helper_hittest_bug1715369_iframe.html"></iframe>
+ <div style="height: 200vh;"></div>
+ </div>
+</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_iframe.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_iframe.html
new file mode 100644
index 0000000000..034eb0429f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_iframe.html
@@ -0,0 +1,13 @@
+<style>
+body, html { margin: 0; }
+</style>
+<script>
+ window.addEventListener("message", event => {
+ if (event.data == "gotclick") {
+ // forward to parent
+ parent.postMessage("gotclick", "*");
+ }
+ });
+</script>
+<iframe style="border: 1px;" frameborder="1" src="http://example.org/tests/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_oopif.html"></iframe>
+<div style="width:10px; height: 500vh;"></div>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_oopif.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_oopif.html
new file mode 100644
index 0000000000..1a6582ce60
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_oopif.html
@@ -0,0 +1,13 @@
+<style>
+body, html { margin: 0; }
+</style>
+<div id="thediv" style="position: absolute; top: 0; left: 0; width: 40px; height: 40px; background: blue;">
+</div>
+<div style="width:10px; height: 500vh;"></div>
+<script>
+let target = document.getElementById("thediv");
+target.addEventListener("click", listener);
+function listener() {
+ parent.postMessage("gotclick", "*");
+}
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-1.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-1.html
new file mode 100644
index 0000000000..b84cb6b634
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-1.html
@@ -0,0 +1,124 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>A simple hit testing test that doesn't involve any transforms</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ body, html {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ }
+ .scrollable-content {
+ width: 200%;
+ height: 200%;
+ }
+ #layer1 {
+ position: absolute;
+ width: 100vw;
+ height: 100vh;
+ background: blue;
+ }
+ #layer2 {
+ position: absolute;
+ top: 100px;
+ left: 100px;
+ width: 100px;
+ height: 100px;
+ background: red;
+ }
+ #layer3 {
+ position: absolute;
+ top: 100px;
+ left: 100px;
+ width: 100px;
+ height: 100px;
+ background: green;
+ overflow: hidden;
+ }
+ #layer4 {
+ position: absolute;
+ top: 50px;
+ left: 50px;
+ width: 100px;
+ height: 100px;
+ background: yellow;
+ overflow: hidden;
+ }
+ </style>
+</head>
+<body>
+ <div id="layer1"></div>
+ <div id="layer2"></div>
+ <div id="layer3">
+ <div class="scrollable-content"></div>
+ </div>
+ <div id="layer4">
+ <div class="scrollable-content"></div>
+ </div>
+ <div class="scrollable-content"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ let subframeHitInfo = APZHitResultFlags.VISIBLE;
+ if (!config.activateAllScrollFrames) {
+ subframeHitInfo |= APZHitResultFlags.INACTIVE_SCROLLFRAME;
+ }
+
+ // Initially, the only thing scrollable is the root.
+ checkHitResult(hitTest({x: 175, y: 175}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "root scroller");
+
+ // Make layer3 scrollable.
+ layer3.style.overflow = "auto";
+ await promiseApzFlushedRepaints();
+ checkHitResult(hitTest({x: 175, y: 175}),
+ subframeHitInfo,
+ (config.activateAllScrollFrames ? utils.getViewId(layer3)
+ : utils.getViewId(document.scrollingElement)),
+ utils.getLayersId(),
+ "subframe layer3");
+
+ // At (125, 125), layer4 obscures layer3. layer4 is not scrollable yet,
+ // so we hit the root.
+ checkHitResult(hitTest({x: 125, y: 125}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "root scroller");
+
+ // Make layer4 scrollable as well. Now (125, 125) should hit it.
+ layer4.style.overflow = "auto";
+ await promiseApzFlushedRepaints();
+ checkHitResult(hitTest({x: 125, y: 125}),
+ subframeHitInfo,
+ (config.activateAllScrollFrames ? utils.getViewId(layer4)
+ : utils.getViewId(document.scrollingElement)),
+ utils.getLayersId(),
+ "subframe layer4");
+
+ // Hit-test outside the reach of layer[3,4] but inside root.
+ checkHitResult(hitTest({x: 225, y: 225}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "root scroller");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-2.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-2.html
new file mode 100644
index 0000000000..8cd3388534
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-2.html
@@ -0,0 +1,157 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>A more involved hit testing test that involves css and async transforms</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ body, html {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ }
+ .scrollable-content {
+ width: 200%;
+ height: 200%;
+ }
+ #layer1 {
+ position: absolute;
+ top: 20px;
+ left: 20px;
+ width: 100px;
+ height: 100px;
+ background: red;
+ overflow: auto;
+ }
+ #layer2 {
+ position: absolute;
+ top: 140px;
+ left: 40px;
+ width: 100px;
+ height: 100px;
+ background: green;
+ transform: scale(2.0, 1.0);
+ transform-origin: 0 0;
+ }
+ #layer3 {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 100px;
+ height: 100px;
+ background: yellow;
+ overflow: auto;
+ }
+ </style>
+</head>
+<body>
+ <div id="layer1">
+ <div class="scrollable-content"></div>
+ </div>
+ <div id="layer2">
+ <div id="layer3">
+ <div class="scrollable-content"></div>
+ </div>
+ </div>
+ <div class="scrollable-content"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ let subframeHitInfo = APZHitResultFlags.VISIBLE;
+ if (!config.activateAllScrollFrames) {
+ subframeHitInfo |= APZHitResultFlags.INACTIVE_SCROLLFRAME;
+ }
+
+ // Hit an area that's clearly on the root and not any of the child layers.
+ checkHitResult(hitTest({x: 150, y: 70}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "root scroller");
+
+ // Hit an area on the root that would be on layer3 if layer2 weren't transformed.
+ checkHitResult(hitTest({x: 30, y: 190}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "root scroller (area revealed by transform)");
+
+ // Hit an area on layer1.
+ checkHitResult(hitTest({x: 70, y: 70}),
+ subframeHitInfo,
+ (config.activateAllScrollFrames ? utils.getViewId(layer1)
+ : utils.getViewId(document.scrollingElement)),
+ utils.getLayersId(),
+ "layer1");
+
+ // Hit an area on layer3.
+ checkHitResult(hitTest({x: 60, y: 190}),
+ subframeHitInfo,
+ (config.activateAllScrollFrames ? utils.getViewId(layer3)
+ : utils.getViewId(document.scrollingElement)),
+ utils.getLayersId(),
+ "layer3");
+
+ // Hit an area on layer3 that would be on the root if layer2 weren't transformed.
+ checkHitResult(hitTest({x: 150, y: 190}),
+ subframeHitInfo,
+ (config.activateAllScrollFrames ? utils.getViewId(layer3)
+ : utils.getViewId(document.scrollingElement)),
+ utils.getLayersId(),
+ "layer3");
+
+ // Scroll the root upwards by 120 pixels. This scrolls layer1 out of view.
+ utils.setAsyncScrollOffset(document.scrollingElement, 0, 120);
+ // Give WebRender a chance to sample the test async scroll offset.
+ utils.advanceTimeAndRefresh(16);
+ utils.restoreNormalRefresh();
+
+ // Hit where layers3 used to be. It should now hit the root.
+ checkHitResult(hitTest({x: 60, y: 190}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "root scroller (after async scroll)");
+
+ // Hit where layers1 used to be and where layers3 should now be.
+ checkHitResult(hitTest({x: 70, y: 70}),
+ subframeHitInfo,
+ (config.activateAllScrollFrames ? utils.getViewId(layer3)
+ : utils.getViewId(document.scrollingElement)),
+ utils.getLayersId(),
+ "layer3 (after async scroll)");
+
+ // Scroll the root upwards by an additional 120 pixels.
+ utils.setAsyncScrollOffset(document.scrollingElement, 0, 240);
+ // Give WebRender a chance to sample the test async scroll offset.
+ utils.advanceTimeAndRefresh(16);
+ utils.restoreNormalRefresh();
+
+ // Hit where layers3 used to be. It should now hit the root.
+ checkHitResult(hitTest({x: 60, y: 190}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "root scroller (after second async scroll)");
+
+ // Hit where layers2 used to be. It should now hit the root.
+ checkHitResult(hitTest({x: 70, y: 70}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "root scroller (after second async scroll)");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-3.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-3.html
new file mode 100644
index 0000000000..0ae01864a5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-3.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>A hit testing test involving a scenario with a scale transform</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ body, html {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ }
+ #layer1 {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 100px;
+ height: 100px;
+ background: red;
+ overflow: scroll;
+ transform: scale(2.0);
+ transform-origin: 0 0;
+ }
+ </style>
+</head>
+<body>
+ <!-- Neither the root nor layer1 actually have room to scroll -->
+ <div id="layer1"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // Force an APZC for layer1 in spite of it having no scroll range.
+ utils.setDisplayPortForElement(0, 0, 100, 100, layer1, 1);
+ await promiseApzFlushedRepaints();
+
+ // Hit an area on layer1 that would be on the root if layer1 weren't transformed.
+ checkHitResult(hitTest({x: 150, y: 150}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(layer1),
+ utils.getLayersId(),
+ "layer1");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-4.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-4.html
new file mode 100644
index 0000000000..26ec487b3e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-4.html
@@ -0,0 +1,194 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>A hit testing test involving a scenario with a scale transform</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ /*
+ * This test tries to approximate the layer structure of
+ * APZHitTestingTester.ComplexMultiLayerTree from TestHitTesting.cpp.
+ *
+ * Notes:
+ * - The elements are named after the layers in the testcase
+ * (e.g. "layer1"), but where multiple elements share an APZC,
+ * new elements named "scroller1" etc. are introduced to be
+ * the scroll containers.
+ * - overflow: hidden is used to avoid spurious scrollbar layers.
+ * To trigger APZC creation, the test code explicitly sets display
+ * port margins.
+ * - Perspective transforms are used to force an element to be
+ * layerized if it otherwise wouldn't.
+ * - One difference is that the entire contents of the APZC that
+ * scrolls layer{4,6,8} is shifted to the right by 100px.
+ * Otherwise, the dimensions of the layers make it such that
+ * this APZC's composition bounds covers up layers{1,2,3}
+ * and those cannot be hit.
+ */
+ body, html {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ }
+ #scroller1 {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 250px;
+ height: 350px;
+ overflow: hidden;
+ }
+ #layer1 {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ width: 100px;
+ height: 100px;
+ background: red;
+ }
+ #layer2 {
+ position: absolute;
+ left: 50px;
+ top: 50px;
+ width: 200px;
+ height: 300px;
+ background: yellow;
+ transform: scale(1.0);
+ perspective: 10px;
+ }
+ #layer3 {
+ position: absolute;
+ left: 10px;
+ top: 10px;
+ width: 10px;
+ height: 10px;
+ background: green;
+ transform: scale(1.0);
+ perspective: 10px;
+ }
+ #scroller2 {
+ position: absolute;
+ left: 100px;
+ top: 0px;
+ width: 300px;
+ height: 400px;
+ overflow: hidden;
+ }
+ #layer4 {
+ position: absolute;
+ left: 0px;
+ top: 200px;
+ width: 100px;
+ height: 100px;
+ background: purple;
+ }
+ #layer5 {
+ position: absolute;
+ left: 200px;
+ top: 0px;
+ width: 100px;
+ height: 400px;
+ background: pink;
+ transform: scale(1.0);
+ perspective: 10px;
+ }
+ #layer6 {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ width: 100px;
+ height: 200px;
+ background: orange;
+ }
+ #layer7 {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ width: 100px;
+ height: 200px;
+ background: navy;
+ overflow: hidden;
+ }
+ #layer8 {
+ position: absolute;
+ left: 0px;
+ top: 200px;
+ width: 100px;
+ height: 100px;
+ background: lightgreen;
+ transform: scale(1.0);
+ perspective: 10px;
+ }
+ #layer9 {
+ position: absolute;
+ left: 0px;
+ top: 300px;
+ width: 100px;
+ height: 100px;
+ background: turquoise;
+ overflow: hidden;
+ }
+ </style>
+</head>
+<body>
+ <div id="scroller1">
+ <div id="layer1"></div>
+ <div id="layer2">
+ <div id="layer3"></div>
+ </div>
+ </div>
+ <div id="scroller2">
+ <div id="layer4"></div>
+ <div id="layer5">
+ <div id="layer6">
+ <div id="layer7"></div>
+ </div>
+ <div id="layer8"></div>
+ <div id="layer9"></div>
+ </div>
+ </div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // Trigger APZC creation.
+ utils.setDisplayPortForElement(0, 0, 250, 350, scroller1, 1);
+ await promiseApzFlushedRepaints();
+ utils.setDisplayPortForElement(0, 0, 300, 400, scroller2, 1);
+ await promiseApzFlushedRepaints();
+ utils.setDisplayPortForElement(0, 0, 100, 200, layer7, 1);
+ await promiseApzFlushedRepaints();
+ utils.setDisplayPortForElement(0, 0, 100, 200, layer9, 1);
+ await promiseApzFlushedRepaints();
+
+ checkHitResult(hitTest({x: 25, y: 25}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(scroller1),
+ utils.getLayersId(),
+ "scroller1");
+
+ checkHitResult(hitTest({x: 350, y: 100}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(layer7),
+ utils.getLayersId(),
+ "layer7");
+
+ checkHitResult(hitTest({x: 375, y: 375}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(layer9),
+ utils.getLayersId(),
+ "layer7");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_checkerboard.html b/gfx/layers/apz/test/mochitest/helper_hittest_checkerboard.html
new file mode 100644
index 0000000000..eb2d583276
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_checkerboard.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ hit-testing over a checkerboarded area</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+</head>
+<body>
+ <div id="scroller" style="width: 300px; height: 300px; overflow:scroll; margin-top: 100px; margin-left: 50px">
+ <!-- Make the contents tall enough to be sure we can checkerboard -->
+ <div id="contents" style="width: 100%; height: 5000px; background-image: linear-gradient(blue,red)">
+ </div>
+ </div>
+ <div id="make_root_scrollable" style="height: 5000px"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ var scroller = document.getElementById("scroller");
+
+ // Activate the scrollframe but keep the main-thread scroll position at 0.
+ // Also apply an async scroll offset in the y-direction such that the
+ // scrollframe scrolls all the way to the bottom of its range, where it's
+ // sure to checkerboard.
+ utils.setDisplayPortForElement(0, 0, 300, 1000, scroller, 1);
+ await promiseApzFlushedRepaints();
+ var scrollY = scroller.scrollTopMax;
+ utils.setAsyncScrollOffset(scroller, 0, scrollY);
+ // Tick the refresh driver once to make sure the compositor has applied the
+ // async scroll offset (for WebRender hit-testing we need to make sure WR has
+ // the latest info).
+ utils.advanceTimeAndRefresh(16);
+ utils.restoreNormalRefresh();
+
+ var scrollerViewId = utils.getViewId(scroller);
+
+ // Hit-test the middle of the scrollframe, which is now inside the
+ // checkerboarded region, and check that we hit the scrollframe and
+ // not its parent.
+ checkHitResult(hitTest(centerOf(scroller)),
+ APZHitResultFlags.VISIBLE,
+ scrollerViewId,
+ utils.getLayersId(),
+ "active scrollframe");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_clippath.html b/gfx/layers/apz/test/mochitest/helper_hittest_clippath.html
new file mode 100644
index 0000000000..0f8cfca519
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_clippath.html
@@ -0,0 +1,118 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Hit-testing an iframe covered by an element with a clip-path</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+<style>
+ html, body { margin: 0; }
+ #clipped {
+ width: 400px;
+ height: 400px;
+ background-color: green;
+ position: absolute;
+ top: 100px;
+ left: 100px;
+ clip-path: circle(150px);
+ }
+ iframe {
+ width: 400px;
+ height: 300px;
+ border: 0px solid black;
+ }
+</style>
+</head>
+<body style="height: 5000px">
+<iframe id="sub" srcdoc="<!DOCTYPE html><body style='height: 5000px'><div style='position: absolute; top: 150px; left: 150px; width: 300px; height: 300px; background-color: blue;'></div>
+when this page loads, the blue rect should be behind the green circle. mousing over the area with the blue rect and scrolling with the wheel or trackpad should cause the iframe to scroll."></iframe>
+<div id="clipped"></div>
+<script>
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // layerize the iframe
+ var subwindow = document.getElementById("sub").contentWindow;
+ var subscroller = subwindow.document.scrollingElement;
+ var subutils = SpecialPowers.getDOMWindowUtils(subwindow);
+ subutils.setDisplayPortForElement(0, 0, 400, 1000, subscroller, 1);
+ await promiseApzFlushedRepaints();
+
+ var rootViewId = utils.getViewId(document.scrollingElement);
+ var iframeViewId = subutils.getViewId(subscroller);
+ var layersId = utils.getLayersId();
+ is(subutils.getLayersId(), layersId, "iframe is not OOP");
+
+ checkHitResult(hitTest({ x: 10, y: 10 }),
+ APZHitResultFlags.VISIBLE,
+ iframeViewId,
+ layersId,
+ `(simple) uninteresting point inside the iframe`);
+ checkHitResult(hitTest({ x: 500, y: 10 }),
+ APZHitResultFlags.VISIBLE,
+ rootViewId,
+ layersId,
+ `(simple) uninteresting point in the root scroller`);
+ checkHitResult(hitTest({ x: 110, y: 110 }),
+ APZHitResultFlags.VISIBLE,
+ iframeViewId,
+ layersId,
+ `(simple) point in the iframe behind overlaying div, but outside the bounding box of the clip path`);
+ checkHitResult(hitTest({ x: 160, y: 160 }),
+ APZHitResultFlags.VISIBLE,
+ iframeViewId,
+ layersId,
+ `(simple) point in the iframe behind overlaying div, inside the bounding box of the clip path, but outside the actual clip shape`);
+ checkHitResult(hitTest({ x: 300, y: 200 }),
+ APZHitResultFlags.VISIBLE,
+ rootViewId,
+ layersId,
+ `(simple) point inside the clip shape of the overlaying div`);
+
+ // Now we turn the "simple" clip-path that WR can handle into a more complex
+ // one that needs a mask. Then run the checks again; the expected results for
+ // WR are slightly different
+ document.getElementById("clipped").style.clipPath = "polygon(" +
+ "50px 200px, 75px 75px, 200px 50px, 205px 55px, 210px 50px, " +
+ "215px 55px, 220px 50px, 225px 55px, 230px 50px, 235px 55px, " +
+ "240px 50px, 245px 55px, 250px 50px, 255px 55px, 260px 50px, " +
+ "265px 55px, 270px 50px, 275px 55px, 280px 50px, 350px 200px, " +
+ "200px 350px)";
+ await promiseApzFlushedRepaints();
+
+ checkHitResult(hitTest({ x: 10, y: 10 }),
+ APZHitResultFlags.VISIBLE,
+ iframeViewId,
+ layersId,
+ `(complex) uninteresting point inside the iframe`);
+ checkHitResult(hitTest({ x: 500, y: 10 }),
+ APZHitResultFlags.VISIBLE,
+ rootViewId,
+ layersId,
+ `(complex) uninteresting point in the root scroller`);
+ checkHitResult(hitTest({ x: 110, y: 110 }),
+ APZHitResultFlags.VISIBLE,
+ iframeViewId,
+ layersId,
+ `(complex) point in the iframe behind overlaying div, but outside the bounding box of the clip path`);
+ checkHitResult(hitTest({ x: 160, y: 160 }),
+ APZHitResultFlags.VISIBLE,
+ iframeViewId,
+ layersId,
+ `(complex) point in the iframe behind overlaying div, inside the bounding box of the clip path, but outside the actual clip shape`);
+ checkHitResult(hitTest({ x: 300, y: 200 }),
+ APZHitResultFlags.VISIBLE,
+ iframeViewId,
+ layersId,
+ `(complex) point inside the clip shape of the overlaying div`);
+}
+
+waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+</script>
+</body></html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_clipped_fixed_modal.html b/gfx/layers/apz/test/mochitest/helper_hittest_clipped_fixed_modal.html
new file mode 100644
index 0000000000..122863967a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_clipped_fixed_modal.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Hit-testing on content covered by a fullscreen fixed-position item clipped away</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+<style>
+.modal
+{
+ position:fixed;
+ z-index:10;
+ width:100%;
+ height:100%;
+ left:0;
+ top:0;
+ clip:rect(1px,1px,1px,1px);
+}
+.modal__content
+{
+ overflow:auto;
+ position:fixed;
+ top:0;
+ left:0;
+ width:100%;
+ height:100%;
+}
+.modal__body
+{
+ position:absolute;
+ top:0;
+ left:0;
+ width:100%;
+ height:100%;
+}
+.content
+{
+ position:fixed;
+ top:0;
+ left:0;
+ width:100%;
+ height:100%;
+ overflow-y:auto
+}
+</style>
+</head>
+<body>
+<div class="content">
+ <div style="height: 5000px; background-image: linear-gradient(red,blue)">
+ Filler to make the content div scrollable
+ </div>
+</div>
+<div class="modal">
+ <div class="modal__content">
+ <div class="modal__body">
+ </div>
+ </div>
+</div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // layerize the scrollable frame
+ var subframe = document.querySelector(".content");
+ utils.setDisplayPortForElement(0, 0, 800, 2000, subframe, 1);
+ await promiseApzFlushedRepaints();
+
+ var target = document.querySelector(".content");
+ checkHitResult(hitTest(centerOf(target)),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(subframe),
+ utils.getLayersId(),
+ "content covered by a clipped fixed div");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_deep_scene_stack.html b/gfx/layers/apz/test/mochitest/helper_hittest_deep_scene_stack.html
new file mode 100644
index 0000000000..a04b1d3e83
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_deep_scene_stack.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Exercising the APZ/WR hit-test with a deep scene that produces many results</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+<style>
+body {
+ transform-style: preserve-3d;
+}
+
+div {
+ height: 100px;
+ background-color: rgba(0, 255, 0, 0.1);
+ transform: translateX(1px);
+}
+</style>
+</head>
+<body>
+<script>
+
+// Create a 1000-deep set of nested divs with some transparency and transforms.
+// This ensures that the WR hit-test will return all of the divs at the tested
+// point, rather than just the topmost one. We set a touch-action property on
+// this div so that we can ensure we're hit-testing at the right spot.
+var div = document.createElement('div');
+div.id = "innermost";
+div.style.touchAction = "pan-x pan-y";
+div.style.width = "2px";
+
+for (var i = 3; i < 1000; i++) {
+ var container = document.createElement('div');
+ container.style.width = i + "px";
+ container.appendChild(div);
+ div = container;
+}
+document.body.appendChild(div);
+
+async function test(testDriver) {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // Hit-test at the deepest point of divs.
+ checkHitResult(hitTest(centerOf(document.getElementById("innermost"))),
+ APZHitResultFlags.VISIBLE | APZHitResultFlags.PINCH_ZOOM_DISABLED | APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "innermost div");
+}
+
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+
+</script>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_fixed-2.html b/gfx/layers/apz/test/mochitest/helper_hittest_fixed-2.html
new file mode 100644
index 0000000000..0f20719d46
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_fixed-2.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ hit-testing of fixed content when async-scrolled</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ html, body {
+ margin: 0;
+ }
+ #fixed {
+ position: fixed;
+ height: 300px;
+ width: 100%;
+ top: 0;
+ background: blue;
+ }
+ #target {
+ margin-top: 100px;
+ margin-left: 100px;
+ height: 20px;
+ width: 20px;
+ background: red;
+ }
+ </style>
+</head>
+<body>
+ <div id="fixed">
+ <div id="target"></div>
+ </div>
+ <div id="make_root_scrollable" style="height: 5000px"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ // Async scroll the page by 50 pixels using the mouse-wheel.
+ await promiseMoveMouseAndScrollWheelOver(document.body, 10, 10,
+ /* waitForScroll = */ false, /* scrollDelta = */ 50);
+
+ let clickPromise = new Promise(resolve => {
+ target.addEventListener("click", e => {
+ ok(true, "Target was hit");
+ e.stopPropagation(); // do not propagate event to |fixed| ancestor
+ resolve();
+ });
+ fixed.addEventListener("click", e => {
+ // Since target's listener calls stopPropagation(), if we get here
+ // then the coordinates of the click event did not correspond to
+ // |target|, but somewhere else on |fixed|.
+ ok(false, "Fixed ancestor should not be hit");
+ resolve();
+ });
+ });
+
+ // Synthesize a click at (110, 110), which should hit |target| (a
+ // descendant of |fixed|) regardless of the async scroll.
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: window,
+ offsetX: 110,
+ offsetY: 110
+ });
+
+ await clickPromise;
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_fixed-3.html b/gfx/layers/apz/test/mochitest/helper_hittest_fixed-3.html
new file mode 100644
index 0000000000..2004ea9ae4
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_fixed-3.html
@@ -0,0 +1,113 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ hit-testing of fixed content when async-scrolled</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ html, body {
+ margin: 0;
+ }
+ iframe {
+ width: 100%;
+ height: 400px;
+ margin-top: 100px;
+ padding: 0px;
+ border: 0px;
+ display: block;
+ }
+ </style>
+</head>
+<body>
+ <iframe id="subdoc" srcdoc="<!DOCTYPE HTML>
+ <html>
+ <style>
+ html, body {
+ margin: 0;
+ }
+ #fixed {
+ position: fixed;
+ height: 300px;
+ width: 100%;
+ top: 0;
+ background: blue;
+ }
+ #target {
+ margin-top: 100px;
+ margin-left: 100px;
+ height: 20px;
+ width: 20px;
+ background: red;
+ }
+ </style>
+ <div id='fixed'>
+ <div id='target'></div>
+ </div>
+ <div id='make_scrollable' style='height: 5000px'></div>
+ </html>
+ "></iframe>
+ <div id="make_root_scrollable" style="height: 5000px"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ // Async scroll the root content document by 50 pixels.
+ // This test uses a touch-drag (with relevant prefs set in
+ // test_group_hittest-2.html to e.g. disable flings)
+ // rather than mousewheel so that we have control over the
+ // precise amount scrolled.
+ let transformEndPromise = promiseTransformEnd();
+ await synthesizeNativeTouchDrag(document.documentElement, 10, 10, 0, -50);
+ await transformEndPromise;
+
+ // Set up listeners that pass the test if we correctly hit |target|
+ // but fail it if we hit something else.
+ let target = subdoc.contentWindow.target;
+ let fixed = subdoc.contentWindow.fixed;
+ let clickPromise = new Promise(resolve => {
+ target.addEventListener("click", e => {
+ ok(true, "Target was hit");
+ e.stopPropagation(); // do not propagate event to ancestors
+ resolve();
+ });
+ fixed.addEventListener("click", e => {
+ // Since target's listener calls stopPropagation(), if we get here
+ // then the coordinates of the click event did not correspond to
+ // |target|, but somewhere else on |fixed|.
+ //
+ // TODO(bug 1786369): Ensure the parent is not hit once this is
+ // no longer an intermittent failure.
+ todo(false, "Fixed ancestor should not be hit");
+ resolve();
+ });
+ window.addEventListener("click", e => {
+ // Similarly, the root content document's window should not be hit.
+ ok(false, "Root document should not be hit");
+ resolve();
+ });
+ });
+
+ // Synthesize a click which should hit |target|.
+ // The y-coordinate relative to the window is:
+ // 100 pixel margin of iframe from top of root content doc
+ // + 100 pixel margin of target from top of iframe
+ // - 50 pixels async scrolled
+ // + 10 pixels to get us to the middle of the 20px-width target
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: window,
+ offsetX: 110,
+ offsetY: 160
+ });
+
+ await clickPromise;
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_fixed.html b/gfx/layers/apz/test/mochitest/helper_hittest_fixed.html
new file mode 100644
index 0000000000..530c53fd7a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_fixed.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ hit-testing of fixed content when async-scrolled</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ html, body {
+ margin: 0;
+ }
+ #fixed {
+ position: fixed;
+ height: 300px;
+ width: 100%;
+ top: 0;
+ background: blue;
+ }
+ #target {
+ margin-top: 100px;
+ margin-left: 100px;
+ height: 20px;
+ width: 20px;
+ background: red;
+ }
+ </style>
+</head>
+<body>
+ <div id="fixed">
+ <div id="target"></div>
+ </div>
+ <div id="make_root_scrollable" style="height: 5000px"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // Async scroll the page by 50 pixels. The scroll does not move
+ // the fixed element.
+ utils.setAsyncScrollOffset(document.documentElement, 0, 50);
+ // Tick the refresh driver once to make sure the compositor has applied the
+ // async scroll offset (for WebRender hit-testing we need to make sure WR has
+ // the latest info).
+ utils.advanceTimeAndRefresh(16);
+ utils.restoreNormalRefresh();
+
+ let clickPromise = new Promise(resolve => {
+ target.addEventListener("click", e => {
+ ok(true, "Target was hit");
+ e.stopPropagation(); // do not propagate event to |fixed| ancestor
+ resolve();
+ });
+ fixed.addEventListener("click", e => {
+ // Since target's listener calls stopPropagation(), if we get here
+ // then the coordinates of the click event did not correspond to
+ // |target|, but somewhere else on |fixed|.
+ ok(false, "Fixed ancestor should not be hit");
+ resolve();
+ });
+ });
+
+ // Synthesize a click at (110, 110), which should hit |target| (a
+ // descendant of |fixed|) regardless of the async scroll.
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: window,
+ offsetX: 110,
+ offsetY: 110
+ });
+
+ await clickPromise;
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_fixed_bg.html b/gfx/layers/apz/test/mochitest/helper_hittest_fixed_bg.html
new file mode 100644
index 0000000000..1eae84305d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_fixed_bg.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Hit-testing of fixed background image</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html,
+ body {
+ height: 100vh;
+ margin: 0px;
+ padding: 0px;
+ overflow-x: hidden;
+ }
+ .bg-gradient {
+ background: linear-gradient(white, green) fixed;
+ height: 1000px;
+ width: 100%;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="bg-gradient"></div>
+ <div style="height: 1000px; background-color: green;"></div>
+ </body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ var body = document.querySelector("body");
+ utils.setDisplayPortForElement(0, 0, 800, 2000, body, 1);
+ await promiseApzFlushedRepaints();
+
+ var target = document.querySelector(".bg-gradient");
+ checkHitResult(hitTest(centerOf(target)),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(body),
+ utils.getLayersId(),
+ "fixed bg image");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_fixed_in_scrolled_transform.html b/gfx/layers/apz/test/mochitest/helper_hittest_fixed_in_scrolled_transform.html
new file mode 100644
index 0000000000..93d1e6064d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_fixed_in_scrolled_transform.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Hit-testing on the special setup from fixed-pos-scrolled-clip-3.html</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+<style>
+body {
+ margin: 0;
+ height: 4000px;
+}
+
+.transform {
+ transform: translate(10px, 10px);
+ width: 500px;
+}
+
+.subframe {
+ height: 600px;
+ overflow: auto;
+ box-shadow: 0 0 0 2px black;
+}
+
+.scrolled {
+ height: 4000px;
+ position: relative;
+}
+
+.absoluteClip {
+ position: absolute;
+ top: 300px;
+ left: 100px;
+ width: 200px;
+ height: 200px;
+ background: red;
+ clip: rect(auto auto auto auto);
+}
+
+.fixed {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ background: linear-gradient(lime, lime) black 0 100px no-repeat;
+ background-size: 100% 200px;
+}
+</style>
+</head>
+<body>
+<!-- This is lifted from layout/reftests/async-scrolling/fixed-pos-scrolled-clip-3.html -->
+<div class="transform">
+ <div class="subframe">
+ <div class="scrolled">
+ <div class="absoluteClip">
+ <div class="fixed"></div>
+ </div>
+ </div>
+ </div>
+</div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // layerize the scrollable frame
+ var subframe = document.querySelector(".subframe");
+ utils.setDisplayPortForElement(0, 0, 800, 2000, subframe, 1);
+ await promiseApzFlushedRepaints();
+
+ var target = document.querySelector(".absoluteClip");
+ // The fixed item is fixed with respect to the transform, which is
+ // outside the subframe, so we should scroll the root scroll frame,
+ // not the subframe.
+ checkHitResult(hitTest(centerOf(target)),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "fixed item inside a scrolling transform");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_fixed_item_over_oop_iframe.html b/gfx/layers/apz/test/mochitest/helper_hittest_fixed_item_over_oop_iframe.html
new file mode 100644
index 0000000000..a8b66cb835
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_fixed_item_over_oop_iframe.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Hit-testing of positioned item on top of oop iframe</title>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<!-- https://bugzilla.mozilla.org/show_bug.cgi?id=1747409 -->
+<style>
+body, html {
+ width:100%;
+ height:100%;
+ margin:0;
+}
+
+iframe {
+ width:100%;
+ height:100%;
+ position:fixed;
+ z-index:1;
+}
+
+.over {
+ position: fixed;
+ right: 50px;
+ bottom: 50px;
+ z-index: 9999;
+}
+</style>
+<div class="over">
+ <a id="target" href="#">Link</a>
+</div>
+<iframe src="https://example.com"></iframe>
+<script>
+async function test() {
+ let config = getHitTestConfig();
+ let utils = config.utils;
+ let iframe = document.querySelector("iframe");
+ let target = document.getElementById("target");
+
+ try {
+ iframe.contentWindow.document;
+ ok(false, "Should be a cross-origin iframe");
+ } catch (ex) {}
+
+ await promiseApzFlushedRepaints();
+
+ checkHitResult(hitTest(centerOf(target)),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "fixed element with z-index");
+}
+
+
+onload = function() {
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+};
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_float_bug1434846.html b/gfx/layers/apz/test/mochitest/helper_hittest_float_bug1434846.html
new file mode 100644
index 0000000000..986c7b8477
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_float_bug1434846.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ hit-testing with floated subframe</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ #float {
+ float: left;
+ }
+ #subframe {
+ overflow: scroll;
+ height: 300px;
+ }
+ #subframe-content {
+ width: 300px;
+ height: 2000px;
+ background: cyan;
+ }
+ #make-root-scrollable {
+ height: 5000px;
+ }
+ </style>
+</head>
+<body>
+ <div id="float">
+ <div id="subframe">
+ <div id="subframe-content"></div>
+ </div>
+ </div>
+ <div id="make-root-scrollable"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var utils = getHitTestConfig().utils;
+
+ hitTestScrollbar({
+ element: document.getElementById("subframe"),
+ directions: { vertical: true },
+ expectedScrollId: utils.getViewId(document.scrollingElement),
+ expectedLayersId: utils.getLayersId(),
+ trackLocation: ScrollbarTrackLocation.START,
+ expectThumb: true,
+ layerState: LayerState.INACTIVE,
+ });
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_float_bug1443518.html b/gfx/layers/apz/test/mochitest/helper_hittest_float_bug1443518.html
new file mode 100644
index 0000000000..0e2e754375
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_float_bug1443518.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ hit-testing with floated subframe</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ #div1 {
+ position: relative;
+ }
+ #div2 {
+ width: 300px;
+ float: left;
+ }
+ #subframe {
+ overflow: auto;
+ }
+ #make-root-scrollable {
+ height: 5000px;
+ }
+ </style>
+</head>
+<body>
+ <div id="div1">
+ <div id="div2">
+ <div id="subframe">
+ <pre>A line of text that overflows because it's sufficiently long</pre>
+ </div>
+ </div>
+ </div>
+ <div id="make-root-scrollable"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var utils = getHitTestConfig().utils;
+
+ hitTestScrollbar({
+ element: document.getElementById("subframe"),
+ directions: { horizontal: true },
+ expectedScrollId: utils.getViewId(document.scrollingElement),
+ expectedLayersId: utils.getLayersId(),
+ trackLocation: ScrollbarTrackLocation.START,
+ expectThumb: true,
+ layerState: LayerState.INACTIVE,
+ });
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_hidden_inactive_scrollframe.html b/gfx/layers/apz/test/mochitest/helper_hittest_hidden_inactive_scrollframe.html
new file mode 100644
index 0000000000..0abed82156
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_hidden_inactive_scrollframe.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ hit-testing with an inactive scrollframe that is visibility:hidden (bug 1673505)</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+</head>
+<body style="height: 110vh">
+ <div style="position:fixed; top:0px; bottom:0px; left:0px; right:0px; visibility:hidden">
+ <div style="overflow-y: scroll; height: 100vh" id="nested">
+ <div style="height: 200vh; background-color: red">
+ The body of this document is scrollable and is the main scrollable
+ element. On top of that we have a hidden fixed-pos item containing another
+ scrollframe, but this nested scrollframe is inactive.
+ Since the fixed-pos item is hidden, the nested scrollframe is hidden
+ too and shouldn't be the target of hit-testing. However, because it is
+ an inactive scrollframe, code to generate the "this is an inactive
+ scrollframe" area was marking it as hit-testable. This bug led to hit-
+ tests being mis-targeted to the nested scrollframe's layers id instead
+ of whatever was underneath.
+ </div>
+ </div>
+ </div>
+</body>
+<script type="application/javascript">
+
+function test(testDriver) {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ let hasViewId;
+ try {
+ utils.getViewId(document.getElementById("nested"));
+ hasViewId = true;
+ } catch (e) {
+ hasViewId = false;
+ }
+ if (!config.activateAllScrollFrames) {
+ ok(!hasViewId, "The nested scroller should be inactive and not have a view id");
+ }
+
+ checkHitResult(
+ hitTest(centerOf(document.body)),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "hit went through the hidden scrollframe");
+}
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_hoisted_scrollinfo.html b/gfx/layers/apz/test/mochitest/helper_hittest_hoisted_scrollinfo.html
new file mode 100644
index 0000000000..3427c8da47
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_hoisted_scrollinfo.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Hit-testing on a scrollframe forced to be inactive by being inside a filter</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+<style>
+ #withfilter {
+ filter: url(#menushadow);
+ }
+
+ #scroller {
+ width: 300px;
+ height: 500px;
+ overflow: scroll;
+ }
+
+ .spacer {
+ height: 1000px;
+ background-image: linear-gradient(red, blue);
+ }
+</style>
+</head>
+<body>
+ <div id="withfilter">
+ <div id="scroller">
+ <div class="spacer"></div>
+ </div>
+ </div>
+<!-- the SVG below copied directly from the Gecko Profiler code that
+ demonstrated the original bug. It basically generates a bit of a "drop
+ shadow" effect on the div it's applied to. Original SVG can be found at
+ https://github.com/firefox-devtools/profiler/blame/624f71bce5469cf4f8b2be720e929ba69fa6bfdc/res/img/svg/shadowfilter.svg -->
+ <svg xmlns="http://www.w3.org/2000/svg">
+ <defs>
+ <filter id="menushadow" color-interpolation-filters="sRGB" x="-10" y="-10" width="30" height="30">
+ <feComponentTransfer in="SourceAlpha">
+ <feFuncA type="linear" slope="0.3"/>
+ </feComponentTransfer>
+ <feGaussianBlur stdDeviation="5"/>
+ <feOffset dy="10" result="shadow"/>
+ <feComponentTransfer in="SourceAlpha">
+ <feFuncA type="linear" slope="0.1"/>
+ </feComponentTransfer>
+ <feMorphology operator="dilate" radius="0.5" result="rim"/>
+ <feMerge><feMergeNode in="shadow"/><feMergeNode in="rim"/></feMerge>
+ <feComposite operator="arithmetic" in2="SourceAlpha" k2="1" k3="-0.1"/>
+ <feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge>
+ </filter>
+ </defs>
+ </svg>
+</body>
+<script type="application/javascript">
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // layerize the scrollable frame. It's inside the filter so this
+ // shouldn't actually change the fact that it will still be main-thread
+ // scrolled.
+ var scroller = document.querySelector("#scroller");
+ utils.setDisplayPortForElement(0, 0, 300, 500, scroller, 1);
+ await promiseApzFlushedRepaints();
+
+ var expectedHitFlags =
+ APZHitResultFlags.VISIBLE | APZHitResultFlags.INACTIVE_SCROLLFRAME;
+ checkHitResult(hitTest(centerOf(scroller)),
+ expectedHitFlags,
+ utils.getViewId(scroller),
+ utils.getLayersId(),
+ "scrollable content inside a filter");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-2.html b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-2.html
new file mode 100644
index 0000000000..9838a02aa9
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-2.html
@@ -0,0 +1,69 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test that events are delivered to the correct document near an iframe inide a perspective transform</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ div {
+ position: absolute;
+ }
+ .outer {
+ left: 300px;
+ top: 300px;
+ transform: translate3d(0px, 0px, -500px) perspective(500px);
+ }
+ .inner {
+ left: -100px;
+ top: -100px;
+ }
+ iframe {
+ border: 0;
+ }
+ </style>
+</head>
+<body>
+ <div class="outer">
+ <div class="inner">
+ <iframe id="iframe" width="300px" height="200px" src="https://example.com/tests/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective_child.html"></iframe>
+ </div>
+ </div>
+ <script type="application/javascript">
+async function test() {
+ // Wait for iframe to receive content transforms.
+ await SpecialPowers.spawn(iframe, [], async () => {
+ await SpecialPowers.contentTransformsReceived(content.window);
+ });
+
+ let eventPromise = new Promise(resolve => {
+ window.addEventListener("message", event => {
+ let data = JSON.parse(event.data);
+ if ("type" in data && data.type == "got-mouse-down") {
+ ok(false, "Child document should not have received mouse-down");
+ resolve();
+ }
+ });
+
+ window.addEventListener("mousedown", event => {
+ ok(true, "Parent document should have received mouse-down");
+ resolve();
+ });
+ });
+
+ // Click a bit above the iframe, and check the event is delivered
+ // to the parent document, not the iframe.
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: document.documentElement,
+ offsetX: 350,
+ offsetY: 175
+ });
+ await eventPromise;
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-3.html b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-3.html
new file mode 100644
index 0000000000..7fb423ca1c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-3.html
@@ -0,0 +1,70 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test that events are delivered with correct coordinates to an iframe inide a perspective transform</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html, body {
+ margin: 0;
+ padding: 0;
+ }
+ #outer {
+ margin-top: 50px;
+ margin-left: 50px;
+ perspective: 500px;
+ }
+ #inner {
+ transform: translate3d(0,0,-100px);
+ transform-style: preserve-3d;
+ }
+ iframe {
+ border: 0;
+ background-color: blue;
+ transform: translate3d(0,0,100px);
+ }
+ </style>
+</head>
+<body>
+ <div id="outer">
+ <div id="inner">
+ <iframe id="iframe" src="https://example.com/tests/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective_child.html"></iframe>
+ </div>
+ </div>
+ <script type="application/javascript">
+async function test() {
+ // Wait for iframe to receive content transforms.
+ await SpecialPowers.spawn(iframe, [], async () => {
+ await SpecialPowers.contentTransformsReceived(content.window);
+ });
+
+ let clickCoordsInChild = {
+ offsetX: 0,
+ offsetY: 0
+ };
+ let childMessagePromise = new Promise(resolve => {
+ window.addEventListener("message", event => {
+ let data = JSON.parse(event.data);
+ if ("type" in data && data.type == "got-mouse-down") {
+ clickCoordsInChild = data.coords;
+ resolve();
+ }
+ })
+ });
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: outer,
+ offsetX: 100,
+ offsetY: 100
+ });
+ await childMessagePromise;
+ is(clickCoordsInChild.offsetX, 100, "x coordinate is correct");
+ is(clickCoordsInChild.offsetY, 100, "y coordinate is correct");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective.html b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective.html
new file mode 100644
index 0000000000..d7858d4b00
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective.html
@@ -0,0 +1,60 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test that events are delivered with correct coordinates to an iframe inide a perspective transform</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ #container {
+ transform: translate3d(0px, 0px, -1000px) perspective(496.281px);
+ width: min-content;
+ }
+ iframe {
+ border: 0;
+ }
+ </style>
+</head>
+<body>
+ <div id="container">
+ <iframe id="iframe" src="https://example.com/tests/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective_child.html"></iframe>
+ </div>
+ <script type="application/javascript">
+async function test() {
+ // Wait for iframe to receive content transforms.
+ await SpecialPowers.spawn(iframe, [], async () => {
+ await SpecialPowers.contentTransformsReceived(content.window);
+ });
+
+ let clickCoordsInChild = {
+ offsetX: 0,
+ offsetY: 0
+ };
+ let childMessagePromise = new Promise(resolve => {
+ window.addEventListener("message", event => {
+ let data = JSON.parse(event.data);
+ if ("type" in data && data.type == "got-mouse-down") {
+ clickCoordsInChild = data.coords;
+ resolve();
+ }
+ })
+ });
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: container,
+ offsetX: 100,
+ offsetY: 100
+ });
+ await childMessagePromise;
+ // Need to use fuzzy comparisons because the combination of floating-point
+ // matrix multiplication and rounding to integer coordinates can result in
+ // small discrepancies.
+ isfuzzy(clickCoordsInChild.offsetX, 100, 1, "x coordinate is correct");
+ isfuzzy(clickCoordsInChild.offsetY, 100, 1, "y coordinate is correct");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective_child.html b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective_child.html
new file mode 100644
index 0000000000..37f20ad725
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective_child.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<script>
+ window.addEventListener("mousedown", event => {
+ let data = JSON.stringify({
+ type: "got-mouse-down",
+ coords: {
+ offsetX: event.clientX,
+ offsetY: event.clientY
+ }
+ });
+ window.parent.postMessage(data, "*");
+ });
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_nested_transforms_bug1459696.html b/gfx/layers/apz/test/mochitest/helper_hittest_nested_transforms_bug1459696.html
new file mode 100644
index 0000000000..9d0a9fa50c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_nested_transforms_bug1459696.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ hit-testing with nested inactive transforms (bug 1459696)</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ .pane {
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ }
+ .left {
+ left: 0;
+ right: 66vw;
+ overflow: auto;
+ }
+ .content {
+ width: 100%;
+ height: 200%;
+ background-image: linear-gradient(blue, green);
+ }
+ .right {
+ left: 34vw;
+ right: 0;
+ }
+ .list {
+ overflow: hidden;
+ transform: translate3d(0, 0, 0);
+ height: 100%;
+ }
+ .track {
+ height: 100%;
+ width: 2000px;
+ transform: translate3d(-856px, 0px, 0px);
+ }
+ .slide {
+ float: left;
+ height: 100%;
+ width: 856px;
+ background-image: linear-gradient(red, yellow);
+ }
+ </style>
+</head>
+<body>
+ <div class="left pane" id="left-pane">
+ <div class="content"></div>
+ </div>
+ <div class="right pane">
+ <div class="list">
+ <div class="track">
+ <div class="slide"></div>
+ <div class="slide"></div>
+ </div>
+ </div>
+ </div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var utils = getHitTestConfig().utils;
+
+ var leftPane = document.getElementById("left-pane");
+
+ checkHitResult(
+ hitTest(centerOf(leftPane)),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(leftPane),
+ utils.getLayersId(),
+ "left pane was successfully hit");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_obscuration.html b/gfx/layers/apz/test/mochitest/helper_hittest_obscuration.html
new file mode 100644
index 0000000000..377b191359
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_obscuration.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test hit-testing on content which is revealed by async scrolling</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ #parent {
+ width: 400px;
+ height: 400px;
+ overflow: auto;
+ }
+ #child {
+ margin-top: 200px;
+ width: 100%;
+ height: 100px;
+ overflow: auto;
+ }
+ #obscurer {
+ position: absolute;
+ top: 200px;
+ width: 400px;
+ height: 200px;
+ background: blue;
+ }
+ .spacer {
+ width: 100%;
+ height: 1000px;
+ background: green;
+ }
+ </style>
+</head>
+<body>
+ <div id="parent">
+ <div id="child">
+ <div class="spacer"></div>
+ </div>
+ <div class="spacer"></div>
+ </div>
+ <div id="obscurer"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // Create APZCs for parent and child scrollers.
+ let parent = document.getElementById("parent"); // otherwise parent refers to window.parent
+ utils.setDisplayPortForElement(0, 0, 400, 1000, parent, 1);
+ utils.setDisplayPortForElement(0, 0, 400, 1000, child, 1);
+ await promiseApzFlushedRepaints();
+
+ // Async-scroll the parent scroller by 100 pixels, thereby revealing
+ // the child which was previous covered by "obscurer".
+ utils.setAsyncScrollOffset(parent, 0, 100);
+ // Give WebRender a chance to sample the test async scroll offset.
+ utils.advanceTimeAndRefresh(16);
+ utils.restoreNormalRefresh();
+
+ // Check that hit-testing on the region where the child scroller's
+ // contents are now revealed, successfully hits the scroller.
+ checkHitResult(hitTest({x: 200, y: 150}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(child),
+ utils.getLayersId(),
+ "child scroller");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_overscroll.html b/gfx/layers/apz/test/mochitest/helper_hittest_overscroll.html
new file mode 100644
index 0000000000..c245258b68
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_overscroll.html
@@ -0,0 +1,249 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test APZ hit-testing while overscrolled</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ html, body {
+ margin: 0;
+ padding: 0;
+ }
+ .spacer {
+ height: 5000px;
+ }
+ #target {
+ margin-left: 100px;
+ margin-top: 2px;
+ height: 4px;
+ width: 4px;
+ background: red;
+ }
+ #fixedtarget {
+ left: 300px;
+ height: 20px;
+ width: 5px;
+ top: 2px;
+ background: green;
+ position: fixed;
+ }
+ #fixedtargetbutton {
+ height: 10px;
+ width: 5px;
+ padding: 0;
+ margin-top: 10;
+ margin-left: 0;
+ border: 0;
+ }
+ </style>
+</head>
+<body>
+ <div id="target"></div>
+ <div id="fixedtarget">
+ <button id="fixedtargetbutton">
+ </button>
+ </div>
+ <div id="spacer" class="spacer"></div>
+</body>
+<script type="application/javascript">
+
+// Some helper functions for listening for click events in the browser chrome.
+
+// A handle used to interact with the chrome script used to implement
+// [start|stop]ListeningFOrClickEventsInChrome().
+let chromeScriptHandle = null;
+
+function startListeningForClickEventsInChrome() {
+ function chromeScript() {
+ /* eslint-env mozilla/chrome-script */
+ let topWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!topWin) {
+ topWin = Services.wm.getMostRecentWindow("navigator:geckoview");
+ }
+ let chromeReceivedClick = false;
+ function chromeListener(e) {
+ chromeReceivedClick = true;
+ }
+ topWin.addEventListener("click", chromeListener);
+ function queryClicked() {
+ sendAsyncMessage("query-clicked-response", { chromeReceivedClick });
+ }
+ function cleanup() {
+ topWin.removeEventListener("click", chromeListener);
+ removeMessageListener("query-clicked", queryClicked);
+ removeMessageListener("cleanup", cleanup);
+ }
+ addMessageListener("query-clicked", queryClicked);
+ addMessageListener("cleanup", cleanup);
+ }
+ chromeScriptHandle = SpecialPowers.loadChromeScript(chromeScript);
+}
+
+async function didChromeReceiveClick() {
+ chromeScriptHandle.sendAsyncMessage("query-clicked", null);
+ let response = await chromeScriptHandle.promiseOneMessage("query-clicked-response");
+ ok(response && ("chromeReceivedClick" in response),
+ "Received a well-formed response from chrome script");
+ return response.chromeReceivedClick;
+}
+
+function stopListeningForClickEventsInChrome() {
+ chromeScriptHandle.sendAsyncMessage("cleanup", null);
+ chromeScriptHandle.destroy();
+}
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // Overscroll the root scroll frame at the top, creating a gutter.
+ // Note that the size of the gutter will only be 8px, because
+ // setAsyncScrollOffset() applies the overscroll as a single delta,
+ // and current APZ logic that transforms a delta into an overscroll
+ // amount limits each delta to at most 8px.
+ utils.setAsyncScrollOffset(document.documentElement, 0, -200);
+
+ // Check that the event hits the root scroll frame in APZ.
+ // This is important because additional pan-gesture events in the gutter
+ // should continue to be handled and cause further overscroll (or
+ // relieving overscroll, depending on their direction).
+ let hitResult = hitTest({x: 100, y: 4});
+ let rootViewId = utils.getViewId(document.documentElement);
+ checkHitResult(hitResult,
+ APZHitResultFlags.VISIBLE,
+ rootViewId,
+ utils.getLayersId(),
+ "APZ hit-test in the root gutter");
+
+ // Now, perform a click in the gutter and check that APZ prevents
+ // the event from reaching Gecko.
+ // To be sure that no event was dispatched to Gecko, install listeners
+ // on both the browser chrome window and the content window.
+ // This makes sure we catch the case where the overscroll transform causes
+ // the event to incorrectly target the browser chrome.
+
+ //// Util function to perform mouse events in the gutter. Used to ensure these
+ //// events are not dispatched to the content.
+ async function clickInGutter(xOffset, yOffset) {
+ startListeningForClickEventsInChrome();
+ let contentReceivedClick = false;
+ let contentListener = function(e) {
+ info("event clientX = " + e.clientX);
+ info("event clientY = " + e.clientY);
+ info("event target id: " + e.target.id);
+ contentReceivedClick = true;
+ };
+ document.addEventListener("click", contentListener);
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: window,
+ offsetX: xOffset,
+ offsetY: yOffset,
+ });
+ // Wait 10 frames for the event to maybe arrive, and if it
+ // hasn't, assume it won't.
+ for (let i = 0; i < 10; i++) {
+ await promiseFrame();
+ }
+ info("Finished waiting around for click event");
+ let chromeReceivedClick = await didChromeReceiveClick();
+ ok(!chromeReceivedClick,
+ "Gecko received click event in browser chrome when it shouldn't have");
+ ok(!contentReceivedClick,
+ "Gecko received click event targeting web content when it shouldn't have");
+ stopListeningForClickEventsInChrome();
+ document.removeEventListener("click", contentListener);
+ }
+
+ // Perform the test
+ await clickInGutter(100, 4);
+
+ // Finally, while still overscrolled, perform a click not in the gutter.
+ // This event should successfully go through to the web content, and
+ // be untransformed by the overscroll transform (such that it hits the
+ // content that is visually under the cursor).
+ let clickPromise = new Promise(resolve => {
+ document.addEventListener("click", function(e) {
+ info("event clientX = " + e.clientX);
+ info("event clientY = " + e.clientY);
+ is(e.target, target, "Click while overscrolled hit intended target");
+ resolve();
+ }, { once: true });
+ });
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: window,
+ offsetX: 102,
+ offsetY: 12,
+ });
+ await clickPromise;
+
+ // Test that mouse events targetting the fixed content are dispatched
+ // to the content.
+ async function clickFixed(yOffset, expectedTarget) {
+ let clickFixedPromise = new Promise(resolve => {
+ document.addEventListener("click", function(e) {
+ info("event clientX = " + e.clientX);
+ info("event clientY = " + e.clientY);
+ info("event target id: " + e.target.id);
+ is(e.target, expectedTarget, "Click while overscrolled hit intended target");
+ resolve();
+ }, { once: true });
+ });
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: window,
+ offsetX: 302,
+ offsetY: yOffset,
+ });
+ await clickFixedPromise;
+ }
+
+ // This hit is technically in the gutter created by the overscroll, but we
+ // should still dispatch to gecko due to the fixed element extending into
+ // the gutter.
+ await clickFixed(4, fixedtarget, false);
+ // Perform various mouse events to ensure the fixed element has not moved
+ await clickFixed(10, fixedtarget, false);
+ await clickFixed(14, fixedtargetbutton, false);
+ await clickFixed(18, fixedtargetbutton, false);
+
+ let clickFixedPromise = new Promise(resolve => {
+ document.addEventListener("click", function(e) {
+ info("event clientX = " + e.clientX);
+ info("event clientY = " + e.clientY);
+ info("event target id: " + e.target.id);
+ // TODO(dlrobertson): What exists directly below the fixed element?
+ // Should there be a gutter below the fixed element? Or should events
+ // directed below the fixed element be dispatched normally. In the
+ // transform of the mouse event, the hit will not have any side bits
+ // set and we will transform the hit result. As a result, 25 will be
+ // transformed to ~17, and the event will be dispatched to the fixed
+ // element.
+ todo(false,
+ "Click while overscrolled hit intended target below fixed content");
+ resolve();
+ }, { once: true });
+ });
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: window,
+ offsetX: 302,
+ offsetY: 25,
+ });
+ await clickFixedPromise;
+
+ // Click above the fixed element, but in the gutter.
+ await clickInGutter(302, 1);
+ // Click left of the the fixed element, but in the gutter.
+ await clickInGutter(298, 4);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_overscroll_contextmenu.html b/gfx/layers/apz/test/mochitest/helper_hittest_overscroll_contextmenu.html
new file mode 100644
index 0000000000..8aff3103dd
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_overscroll_contextmenu.html
@@ -0,0 +1,129 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test APZ hit-testing while overscrolled</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ html, body {
+ margin: 0;
+ padding: 0;
+ }
+ .spacer {
+ height: 5000px;
+ }
+ #target {
+ margin-left: 100px;
+ margin-top: 2px;
+ height: 4px;
+ width: 4px;
+ background: red;
+ }
+ </style>
+</head>
+<body>
+ <div id="target"></div>
+ <div class="spacer"></div>
+</body>
+<script type="application/javascript">
+
+// Some helper functions for listening for contextmenu events in the browser chrome.
+
+// A handle used to interact with the chrome script used to implement
+// [start|stop]ListeningForContextmenuEventsInChrome().
+let chromeScriptHandle = null;
+
+function startListeningForContextmenuEventsInChrome() {
+ function chromeScript() {
+ /* eslint-env mozilla/chrome-script */
+ let topWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!topWin) {
+ topWin = Services.wm.getMostRecentWindow("navigator:geckoview");
+ }
+ let chromeReceivedContextmenu = false;
+ function chromeListener(e) {
+ chromeReceivedContextmenu = true;
+ }
+ topWin.addEventListener("contextmenu", chromeListener);
+ function queryContextmenu() {
+ sendAsyncMessage("query-contextmenu-response", { chromeReceivedContextmenu });
+ }
+ function cleanup() {
+ topWin.removeEventListener("contextmenu", chromeListener);
+ removeMessageListener("query-contextmenu", queryContextmenu);
+ removeMessageListener("cleanup", cleanup);
+ }
+ addMessageListener("query-contextmenu", queryContextmenu);
+ addMessageListener("cleanup", cleanup);
+ }
+ chromeScriptHandle = SpecialPowers.loadChromeScript(chromeScript);
+}
+
+async function didChromeReceiveContextmenu() {
+ chromeScriptHandle.sendAsyncMessage("query-contextmenu", null);
+ let response = await chromeScriptHandle.promiseOneMessage("query-contextmenu-response");
+ ok(response && ("chromeReceivedContextmenu" in response),
+ "Received a well-formed response from chrome script");
+ return response.chromeReceivedContextmenu;
+}
+
+function stopListeningForContextmenuEventsInChrome() {
+ chromeScriptHandle.sendAsyncMessage("cleanup", null);
+ chromeScriptHandle.destroy();
+}
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // Overscroll the root scroll frame at the top, creating a gutter.
+ // Note that the size of the gutter will only be 8px, because
+ // setAsyncScrollOffset() applies the overscroll as a single delta,
+ // and current APZ logic that transforms a delta into an overscroll
+ // amount limits each delta to at most 8px.
+ utils.setAsyncScrollOffset(document.documentElement, 0, -200);
+
+ // Now, perform a right-click in the gutter and check that APZ prevents
+ // the contextevent from reaching Gecko.
+ // To be sure that no event was dispatched to Gecko, install listeners
+ // on both the browser chrome window and the content window.
+ // This makes sure we catch the case where the overscroll transform causes
+ // the event to incorrectly target the browser chrome.
+ let deviceScale = window.devicePixelRatio;
+ let midGutter = 4 / deviceScale; // gutter is 8 *screen* pixels
+ startListeningForContextmenuEventsInChrome();
+ let contentReceivedContextmenu = false;
+ let contentListener = function(e) {
+ contentReceivedContextmenu = true;
+ };
+ document.addEventListener("contextmenu", contentListener);
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ button: 2, // eSecondary (= "right mouse button")
+ target: window,
+ offsetX: 100,
+ offsetY: midGutter
+ });
+ // Wait 10 frames for the event to maybe arrive, and if it
+ // hasn't, assume it won't.
+ for (let i = 0; i < 10; i++) {
+ await promiseFrame();
+ }
+ info("Finished waiting around for contextmenu event");
+ let chromeReceivedContextmenu = await didChromeReceiveContextmenu();
+ ok(!chromeReceivedContextmenu,
+ "Gecko received contextmenu event in browser chrome when it shouldn't have");
+ ok(!contentReceivedContextmenu,
+ "Gecko received contextmenu event targeting web content when it shouldn't have");
+ stopListeningForContextmenuEventsInChrome();
+ document.removeEventListener("contextmenu", contentListener);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_overscroll_subframe.html b/gfx/layers/apz/test/mochitest/helper_hittest_overscroll_subframe.html
new file mode 100644
index 0000000000..36918b3682
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_overscroll_subframe.html
@@ -0,0 +1,132 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test APZ hit-testing while overscrolled</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ html, body {
+ margin: 0;
+ padding: 0;
+ }
+ #subframe {
+ width: 100px;
+ height: 100px;
+ overflow: scroll;
+ }
+ .spacer {
+ height: 5000px;
+ }
+ #target {
+ margin-left: 20px;
+ margin-top: 2px;
+ height: 4px;
+ width: 4px;
+ background: red;
+ }
+ </style>
+</head>
+<body>
+ <div id="subframe">
+ <div id="target"></div>
+ <div class="spacer"></div>
+ </div>
+ <div class="spacer"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // Subframe hit testing of overscrolled APZCs does not yet work with WebRender
+ // (bug 1701831), so bail out early.
+ if (true) {
+ todo(false, "This test does not currently pass with WebRender");
+ return;
+ }
+
+ // Activate the subframe. This both matches reality (if you're
+ // scrolling the subframe, it's active), and makes it easier
+ // to check for expected hit test outcomes.
+ utils.setDisplayPortForElement(0, 0, 500, 500, subframe, 1);
+ await promiseApzFlushedRepaints();
+
+ // Overscroll the subframe at the top, creating a gutter.
+ // Note that the size of the gutter will only be 8px, because
+ // setAsyncScrollOffset() applies the overscroll as a single delta,
+ // and current APZ logic that transforms a delta into an overscroll
+ // amount limits each delta to at most 8px.
+ utils.setAsyncScrollOffset(subframe, 0, -200);
+
+ // Check that the event hits the subframe frame in APZ.
+ // This is important because additional pan-gesture events in the gutter
+ // should continue to be handled and cause further overscroll (or
+ // relieving overscroll, depending on their direction).
+ let subframeBounds = subframe.getBoundingClientRect();
+ hitResult = hitTest({
+ x: subframeBounds.x + 50,
+ y: subframeBounds.y + 4
+ });
+ let subframeViewId = utils.getViewId(subframe);
+ checkHitResult(hitResult,
+ APZHitResultFlags.VISIBLE,
+ subframeViewId,
+ utils.getLayersId(),
+ "APZ hit-test in the subframe gutter");
+
+ // Now, perform a click in the gutter and check that APZ prevents
+ // the event from reaching Gecko.
+ // To be sure that no event was dispatched to Gecko, install the listener
+ // on the document, not the subframe.
+ // This makes sure we catch the case where the overscroll transform causes
+ // the event to incorrectly target the document.
+ let receivedClick = false;
+ let listener = function(e) {
+ receivedClick = true;
+ };
+ document.addEventListener("click", listener);
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: subframe,
+ offsetX: 50,
+ offsetY: 4
+ });
+ // Wait 10 frames for the event to maybe arrive, and if it
+ // hasn't, assume it won't.
+ for (let i = 0; i < 10; i++) {
+ await promiseFrame();
+ }
+ info("Finished waiting around for click event");
+ ok(!receivedClick, "Gecko received click event when it shouldn't have");
+ document.removeEventListener("click", listener);
+
+ // Finally, while still overscrolled, perform a click not in the gutter.
+ // This event should successfully go through to the web content, and
+ // be untransformed by the overscroll transform (such that it hits the
+ // content that is visually under the cursor).
+ let clickPromise = new Promise(resolve => {
+ document.addEventListener("click", function(e) {
+ info("event clientX = " + e.clientX);
+ info("event clientY = " + e.clientY);
+ is(e.target, target, "Click while overscrolled hit intended target");
+ resolve();
+ }, { once: true });
+ });
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: window,
+ offsetX: 22,
+ offsetY: 12
+ });
+ await clickPromise;
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_pointerevents_svg.html b/gfx/layers/apz/test/mochitest/helper_hittest_pointerevents_svg.html
new file mode 100644
index 0000000000..22b880736d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_pointerevents_svg.html
@@ -0,0 +1,177 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Hit-testing a scrollframe covered by nonrectangular and pointer-events:none things</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+<style>
+ .scroller {
+ overflow: scroll;
+ width: 100px;
+ height: 100px;
+ }
+
+ .scroller > div {
+ height: 200px;
+ background-image: linear-gradient(#fff,#000);
+ }
+</style>
+</head>
+<body>
+<div id="testcase1">
+ <div style="width: 100px;height: 50px;display: inline-block;">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="overflow: visible;background-color: #aa6666;">
+ <circle cx="80" cy="50" r="50"></circle>
+ </svg>
+ </div>
+ <div class="scroller" style="display: inline-block;"><div></div></div>
+ <div style="width: 100px; height: 100px; display: inline-block; position:relative;">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="overflow: visible;background-color: #aa6666;">
+ <circle cx="20" cy="50" r="50"></circle>
+ </svg>
+ </div>
+</div>
+
+<div id="testcase2" style="position:relative; height: 110px;">
+ <div style="width: 100px;height: 100px;position:absolute;pointer-events:none;left: 25px;">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="background-color: #aa6666;pointer-events: none;">
+ <circle cx="75" cy="50" r="50" style="pointer-events: auto;"></circle>
+ </svg>
+ </div>
+ <div class="scroller" style="position: absolute; left: 100px;"><div></div></div>
+ <div style="width: 100px;height: 100px; position:absolute;pointer-events:none;left: 175px;">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="background-color: #aa6666;pointer-events: none;">
+ <circle cx="45" cy="50" r="50" style="pointer-events: auto;"></circle>
+ </svg>
+ </div>
+</div>
+
+<div id="testcase3">
+ <div style="width: 100px;height: 50px;display: inline-block;">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="overflow: visible;background-color: #aa6666;">
+ <rect x="25" y="25" width="100" height="50"></rect>
+ </svg>
+ </div>
+ <div class="scroller" style="display: inline-block;"><div></div></div>
+ <div style="width: 100px; height: 100px; display: inline-block; position:relative;">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="overflow: visible;background-color: #aa6666;">
+ <rect x="-30" y="25" width="100" height="50"></rect>
+ </svg>
+ </div>
+</div>
+
+<div id="testcase4" style="position:relative; height: 110px;">
+ <div style="width: 100px;height: 100px;position:absolute;pointer-events:none;left: 25px;">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="background-color: #aa6666;pointer-events: none;">
+ <rect x="25" y="25" width="100" height="50" style="pointer-events: auto;"></rect>
+ </svg>
+ </div>
+ <div class="scroller" style="position: absolute; left: 100px;"><div></div></div>
+ <div style="width: 100px;height: 100px; position:absolute;pointer-events:none;left: 175px;">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="background-color: #aa6666;pointer-events: none;">
+ <rect x="-25" y="25" width="100" height="50" style="pointer-events: auto;"></rect>
+ </svg>
+ </div>
+</div>
+
+<div style="width: 40em;">
+ Each of the gradients should be scrollable, except where the black stuff on the right cover them.
+ The brown square should not prevent scrolling. Similarly, the content on the left (which goes
+ underneath the scroller) shouldn't matter.
+</div>
+<script>
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // layerize the scrollable frames
+ for (var scroller of document.querySelectorAll(".scroller")) {
+ utils.setDisplayPortForElement(0, 0, 100, 200, scroller, 1);
+ }
+ await promiseApzFlushedRepaints();
+
+ var rootViewId = utils.getViewId(document.scrollingElement);
+ for (var testId = 1; testId <= 4; testId++) {
+ var target = document.querySelector(`#testcase${testId} .scroller`);
+ var scrollerViewId = utils.getViewId(target);
+ checkHitResult(hitTest(centerOf(target)),
+ APZHitResultFlags.VISIBLE,
+ scrollerViewId,
+ utils.getLayersId(),
+ `center of scroller in testcase ${testId}`);
+
+ var bounds = target.getBoundingClientRect();
+ var verticalScrollbarWidth = bounds.width - target.clientWidth;
+ var horizontalScrollbarHeight = bounds.height - target.clientHeight;
+
+ // these points should all hit the target scroller
+ checkHitResult(hitTest({x: bounds.x + 1, y: bounds.y + 1}),
+ APZHitResultFlags.VISIBLE,
+ scrollerViewId,
+ utils.getLayersId(),
+ `top left of scroller in testcase ${testId}`);
+ checkHitResult(hitTest({x: bounds.x + 1, y: bounds.y + (bounds.height / 2)}),
+ APZHitResultFlags.VISIBLE,
+ scrollerViewId,
+ utils.getLayersId(),
+ `middle left of scroller in testcase ${testId}`);
+ checkHitResult(hitTest({x: bounds.x + 1, y: bounds.bottom - horizontalScrollbarHeight - 1}),
+ APZHitResultFlags.VISIBLE,
+ scrollerViewId,
+ utils.getLayersId(),
+ `bottom left (excluding scrollbar) of scroller in testcase ${testId}`);
+ if (horizontalScrollbarHeight > 0) {
+ checkHitResult(hitTest({x: bounds.x + 1, y: bounds.bottom - 1}),
+ APZHitResultFlags.VISIBLE | APZHitResultFlags.SCROLLBAR,
+ scrollerViewId,
+ utils.getLayersId(),
+ `bottom left of scroller in testcase ${testId}`);
+ }
+
+ // With the first two cases (circle masks) both WebRender and non-WebRender
+ // emit dispatch-to-content regions for the right side, so for now we just
+ // test for that. Eventually WebRender should be able to stop emitting DTC
+ // and we can update this test to be more precise in that case.
+ // For the two rectangular test cases we get precise results rather than
+ // dispatch-to-content.
+ if (testId == 1 || testId == 2) {
+ checkHitResult(hitTest({x: bounds.right - verticalScrollbarWidth - 1, y: bounds.y + 1}),
+ APZHitResultFlags.VISIBLE | APZHitResultFlags.IRREGULAR_AREA,
+ rootViewId,
+ utils.getLayersId(),
+ `top right of scroller in testcase ${testId}`);
+ checkHitResult(hitTest({x: bounds.right - verticalScrollbarWidth - 1, y: bounds.bottom - horizontalScrollbarHeight - 1}),
+ APZHitResultFlags.VISIBLE | APZHitResultFlags.IRREGULAR_AREA,
+ rootViewId,
+ utils.getLayersId(),
+ `bottom right of scroller in testcase ${testId}`);
+ } else {
+ checkHitResult(hitTest({x: bounds.right - verticalScrollbarWidth - 1, y: bounds.y + 1}),
+ APZHitResultFlags.VISIBLE,
+ scrollerViewId,
+ utils.getLayersId(),
+ `top right of scroller in testcase ${testId}`);
+ checkHitResult(hitTest({x: bounds.right - verticalScrollbarWidth - 1, y: bounds.bottom - horizontalScrollbarHeight - 1}),
+ APZHitResultFlags.VISIBLE,
+ scrollerViewId,
+ utils.getLayersId(),
+ `bottom right of scroller in testcase ${testId}`);
+ }
+
+ checkHitResult(hitTest({x: bounds.right - 1, y: bounds.y + (bounds.height / 2)}),
+ APZHitResultFlags.VISIBLE | APZHitResultFlags.IRREGULAR_AREA,
+ rootViewId,
+ utils.getLayersId(),
+ `middle right of scroller in testcase ${testId}`);
+ }
+}
+
+waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+</script>
+</body></html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_spam.html b/gfx/layers/apz/test/mochitest/helper_hittest_spam.html
new file mode 100644
index 0000000000..cace5491a5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_spam.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test doing lots of hit-testing on a rapidly changing page</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+</head>
+<style>
+#spamdiv {
+ overflow: scroll;
+ width: 400px;
+ height: 400px;
+}
+#spamdiv div {
+ width: 1000px;
+ height: 1000px;
+}
+</style>
+<body>
+<script type="application/javascript">
+
+var SPAM_LIMIT = 200; // bigger numbers make the test run longer
+
+// This function adds and removes a scrollable div very rapidly (via
+// setTimeout(0) self-scheduling). This causes very frequent layer
+// transactions with a new APZ hit-testing tree from the main thread to APZ.
+// The div is created afresh every time so that the scroll identifier in
+// Gecko is continually increasing, and hit results from a stale tree will
+// not be valid on the new tree.
+var spamCount = 0;
+var spamPoint = null;
+function divSpammer() {
+ spamCount++;
+ if (spamCount >= SPAM_LIMIT) {
+ return;
+ }
+ setTimeout(divSpammer, 0);
+
+ // Remove the div if it exists...
+ var spamdiv = document.getElementById('spamdiv');
+ if (spamdiv) {
+ spamdiv.remove();
+ return;
+ }
+ // ... and add it if it doesn't exist.
+ spamdiv = document.createElement('div');
+ spamdiv.id = 'spamdiv';
+ spamdiv.appendChild(document.createElement('div'));
+ document.body.appendChild(spamdiv);
+ if (spamPoint == null) {
+ spamPoint = centerOf(spamdiv);
+ }
+}
+
+// This function does continuous hit-testing by scheduling itself over and
+// over with setTimeout(0). It hit-tests the same spot and expects to hit
+// either the root scrollframe (if the spamdiv is not present in that
+// instant) or the spamdiv (if it is present). If the spamdiv is hit, it
+// expects the scrollid to be non-decreasing.
+var rootScrollId = null;
+var lastScrollId = -1;
+function hitTestSpammer() {
+ if (spamCount >= SPAM_LIMIT) {
+ subtestDone();
+ return;
+ }
+ setTimeout(hitTestSpammer, 0);
+
+ if (spamPoint == null) {
+ // The very first invocation of this function will have spamPoint as null,
+ // and we use that to pick up the rootScrollId.
+ ok(rootScrollId == null, "This codepath shouldn't get hit twice");
+ rootScrollId = hitTest(centerOf(document.body)).scrollId;
+ ok(true, "Root scroll id detected as " + rootScrollId);
+ return;
+ }
+
+ var scrollId = hitTest(spamPoint).scrollId;
+ if (scrollId == rootScrollId) {
+ ok(true, "Hit test hit the root scroller, spamdiv is not in compositor");
+ } else {
+ is(scrollId >= lastScrollId, true, "spamdiv's scroll id is now " + scrollId);
+ lastScrollId = scrollId;
+ }
+}
+
+function startTest() {
+ // Make sure to run hitTestSpammer first so the first iteration is while
+ // spamPoint is still null.
+ setTimeout(hitTestSpammer, 0);
+ setTimeout(divSpammer, 0);
+}
+
+waitUntilApzStable().then(startTest);
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_sticky_bug1478304.html b/gfx/layers/apz/test/mochitest/helper_hittest_sticky_bug1478304.html
new file mode 100644
index 0000000000..395c688a12
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_sticky_bug1478304.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ hit-testing with sticky element inside a transform (bug 1478304)</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ #subframe {
+ width: 500px;
+ height: 200px;
+ overflow-y: auto;
+ }
+ #transform {
+ transform: translate(0);
+ }
+ #sticky {
+ background-color: white;
+ position: sticky;
+ top: 0;
+ }
+ #spacer {
+ width: 100px;
+ height: 1000px;
+ }
+ </style>
+</head>
+<body>
+ <div id="subframe">
+ <div id="transform">
+ <div id="sticky">sticky with transformed parent (click me or hover me and try a scroll)</div>
+ <div id="spacer"></div>
+ </div>
+ </div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var utils = getHitTestConfig().utils;
+
+ var subframe = document.getElementById("subframe");
+ var sticky = document.getElementById("sticky");
+
+ checkHitResult(
+ hitTest(centerOf(sticky)),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(subframe),
+ utils.getLayersId(),
+ "subframe was successfully hit");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_touchaction.html b/gfx/layers/apz/test/mochitest/helper_hittest_touchaction.html
new file mode 100644
index 0000000000..acabfcc07b
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_touchaction.html
@@ -0,0 +1,353 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Testing APZ hit-test with touch-action boxes</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+.taBox {
+ width: 20px;
+ height: 20px;
+ background-color: green;
+ display: inline-block;
+}
+.taBox > div {
+ /* make sure this doesn't obscure the center of the enclosing taBox */
+ width: 5px;
+ height: 5px;
+ background-color: blue;
+}
+
+.taBigBox {
+ width: 150px;
+ height: 150px;
+ background-color: green;
+ display: inline-block;
+}
+.taBigBox > div {
+ width: 40px;
+ height: 40px;
+ overflow: auto;
+}
+.taBigBox > div > div {
+ width: 100px;
+ height: 100px;
+}
+ </style>
+</head>
+<body>
+<!-- Create a bunch of divs for hit-testing. Some of the divs also
+ contain nested divs to test inheritance/combination of touch-action
+ properties. This is not an exhaustive list of all the possible
+ combinations but it's assorted enough to provide good coverage. -->
+ <div id="taNone" class="taBox" style="touch-action: none">
+ <div id="taInnerNonePanX" style="touch-action: pan-x"></div>
+ <div id="taInnerNoneManip" style="touch-action: manipulation"></div>
+ </div>
+ <div id="taPanX" class="taBox" style="touch-action: pan-x">
+ <div id="taInnerPanXY" style="touch-action: pan-y"></div>
+ <div id="taInnerPanXManip" style="touch-action: manipulation"></div>
+ </div>
+ <div id="taPanY" class="taBox" style="touch-action: pan-y">
+ <div id="taInnerPanYX" style="touch-action: pan-x"></div>
+ <div id="taInnerPanYY" style="touch-action: pan-y"></div>
+ </div>
+ <div id="taPanXY" class="taBox" style="touch-action: pan-x pan-y">
+ <div id="taInnerPanXYNone" style="touch-action: none"></div>
+ </div>
+ <div id="taManip" class="taBox" style="touch-action: manipulation">
+ <div id="taInnerManipPanX" style="touch-action: pan-x"></div>
+ <div id="taInnerManipNone" style="touch-action: none"></div>
+ <div id="taInnerManipListener" ontouchstart="return false;"></div>
+ </div>
+ <div id="taListener" class="taBox" ontouchstart="return false;">
+ <div id="taInnerListenerPanX" style="touch-action: pan-x"></div>
+ </div>
+ <div id="taPinchZoom" class="taBox" style="touch-action: pinch-zoom">
+ </div>
+
+ <br/>
+
+ <!-- More divs for hit-testing. These comprise one big outer div with
+ a touch-action property, then inside is a scrollable div, possibly with
+ a touch-action of its own, and inside that is another div to make the
+ scrollable div scrollable. Note that the touch-action for an element
+ includes the restrictions from all ancestor elements up to and including
+ the element that has the default action for that touch-action property.
+ Panning actions therefore get inherited from the nearest scrollframe
+ downwards, while zooming actions get inherited from the root content
+ element (because that's the only one that has zooming as the default action)
+ downwards. In effect, any pan restrictions on the outer div should not
+ apply to the inner div, but zooming restrictions should. Also, the
+ touch-action on the scrollable div itself should apply to user interaction
+ inside the scrollable div. -->
+ <div id="taOuterPanX" class="taBigBox" style="touch-action: pan-x">
+ <div id="taScrollerPanY" style="touch-action: pan-y">
+ <div></div>
+ </div>
+ <div id="taScroller">
+ <div style="touch-action: pan-y"></div>
+ </div>
+ <div id="taScroller2">
+ <div></div>
+ </div>
+ </div>
+ <div id="taOuterManipulation" class="taBigBox" style="touch-action: manipulation">
+ <div id="taScrollerPanX" style="touch-action: pan-x">
+ <div></div>
+ </div>
+ <div id="taScroller3">
+ <div></div>
+ </div>
+ <div id="taScroller4" style="touch-action: pan-y">
+ <div style="overflow:hidden"></div>
+ </div>
+ </div>
+</body>
+<script type="application/javascript">
+
+var config = getHitTestConfig();
+
+async function test() {
+ for (var scroller of document.querySelectorAll(".taBigBox > div")) {
+ // layerize all the scrollable divs
+ config.utils.setDisplayPortForElement(0, 0, 100, 100, scroller, 1);
+ }
+ await promiseApzFlushedRepaints();
+
+ var scrollId = config.utils.getViewId(document.scrollingElement);
+ var layersId = config.utils.getLayersId();
+
+ checkHitResult(
+ hitTest(centerOf("taNone")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: none");
+ checkHitResult(
+ hitTest(centerOf("taInnerNonePanX")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: pan-x inside touch-action: none");
+ checkHitResult(
+ hitTest(centerOf("taInnerNoneManip")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: manipulation inside touch-action: none");
+
+ checkHitResult(
+ hitTest(centerOf("taPanX")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: pan-x");
+ checkHitResult(
+ hitTest(centerOf("taInnerPanXY")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: pan-y inside touch-action: pan-x");
+ checkHitResult(
+ hitTest(centerOf("taInnerPanXManip")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: manipulation inside touch-action: pan-x");
+
+ checkHitResult(
+ hitTest(centerOf("taPanY")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: pan-y");
+ checkHitResult(
+ hitTest(centerOf("taInnerPanYX")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: pan-x inside touch-action: pan-y");
+ checkHitResult(
+ hitTest(centerOf("taInnerPanYY")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: pan-y inside touch-action: pan-y");
+
+ checkHitResult(
+ hitTest(centerOf("taPanXY")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: pan-x pan-y");
+ checkHitResult(
+ hitTest(centerOf("taInnerPanXYNone")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: none inside touch-action: pan-x pan-y");
+
+ checkHitResult(
+ hitTest(centerOf("taManip")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: manipulation");
+ checkHitResult(
+ hitTest(centerOf("taInnerManipPanX")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: pan-x inside touch-action: manipulation");
+ checkHitResult(
+ hitTest(centerOf("taInnerManipNone")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: none inside touch-action: manipulation");
+ checkHitResult(
+ hitTest(centerOf("taInnerManipListener")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.APZ_AWARE_LISTENERS |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "div with touch listener inside touch-action: manipulation");
+
+ checkHitResult(
+ hitTest(centerOf("taListener")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.APZ_AWARE_LISTENERS,
+ scrollId,
+ layersId,
+ "div with touch listener");
+ checkHitResult(
+ hitTest(centerOf("taInnerListenerPanX")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.APZ_AWARE_LISTENERS |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: pan-x inside div with touch listener");
+
+ checkHitResult(
+ hitTest(centerOf("taPinchZoom")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: pinch-zoom inside div with touch listener");
+
+ checkHitResult(
+ hitTest(centerOf("taScrollerPanY")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ config.utils.getViewId(document.getElementById("taScrollerPanY")),
+ layersId,
+ "touch-action: pan-y on scroller");
+ checkHitResult(
+ hitTest(centerOf("taScroller")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ config.utils.getViewId(document.getElementById("taScroller")),
+ layersId,
+ "touch-action: pan-y on div inside scroller");
+ checkHitResult(
+ hitTest(centerOf("taScroller2")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ config.utils.getViewId(document.getElementById("taScroller2")),
+ layersId,
+ "zooming restrictions from pan-x outside scroller get inherited in");
+
+ checkHitResult(
+ hitTest(centerOf("taScrollerPanX")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ config.utils.getViewId(document.getElementById("taScrollerPanX")),
+ layersId,
+ "touch-action: pan-x on scroller inside manipulation");
+ checkHitResult(
+ hitTest(centerOf("taScroller3")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ config.utils.getViewId(document.getElementById("taScroller3")),
+ layersId,
+ "touch-action: manipulation outside scroller gets inherited in");
+ checkHitResult(
+ hitTest(centerOf("taScroller4")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ config.utils.getViewId(document.getElementById("taScroller4")),
+ layersId,
+ "overflow:hidden div doesn't reset pan-x/pan-y from enclosing scroller");
+}
+
+waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_horizontal_checkerboard.html b/gfx/layers/apz/test/mochitest/helper_horizontal_checkerboard.html
new file mode 100644
index 0000000000..0355243738
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_horizontal_checkerboard.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html lang="en"><head>
+<meta http-equiv="content-type" content="text/html; charset=UTF-8"><meta charset="utf-8">
+<title>Testcase for checkerboarding during horizontal scrolling</title>
+<script type="application/javascript" src="apz_test_utils.js"></script>
+<script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+
+.scrollbox {
+ margin: 50px;
+ border: 2px solid black;
+ background: red;
+ width: 1120px;
+ height: 200px;
+ overflow: auto;
+}
+
+.scrolled {
+ width: 20000px;
+ height: 200px;
+ background: lime;
+}
+
+</style>
+
+</head><body>
+ <div class="scrollbox"><div class="scrolled"></div></div>
+</body>
+
+<script type="application/javascript">
+async function test() {
+ var scroller = document.querySelector(".scrollbox");
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ var scrollerId = utils.getViewId(scroller);
+
+ // This test contains a wide horizontal scroll box and scrolls it horizontally
+ // from right to left. The size of the box is chosen so that the displayport
+ // snapping logic in nsLayoutUtils.cpp would tries an horizontal alignment larger
+ // than the margins. In such a situation we want to make sure the displayport
+ // alignment is adjusted so we don't snap too far which would cause content to
+ // be missed on the right side.
+
+ // The scroll values here just need to be "thorough" enough to exercise the
+ // code at different alignments, so using a non-power-of-two or prime number
+ // for the increment seems like a good idea. The smaller the increment, the
+ // longer the test takes to run (because more iterations) so we don't want it
+ // too small either.
+ // The scroll box is rather wide so we only scroll a portion of it so that the
+ // test doesn't run for too long.
+ var maxX = scroller.scrollLeftMax / 6;
+ for (var x = maxX; x > 0; x -= 71) {
+ dump(`Scrolling scroller to ${x}\n`);
+ scroller.scrollTo(x, 0);
+ await promiseApzFlushedRepaints();
+ assertNotCheckerboarded(utils, scrollerId, `At x=${x}`);
+ }
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_iframe1.html b/gfx/layers/apz/test/mochitest/helper_iframe1.html
new file mode 100644
index 0000000000..047da96bd4
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_iframe1.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<!-- The purpose of the 'id' on the HTML element is to get something
+ identifiable to show up in the root scroll frame's content description,
+ so we can check it for layerization. -->
+<html id="outer3">
+ <head>
+ <link rel="stylesheet" type="text/css" href="helper_subframe_style.css"/>
+ </head>
+ <body>
+ <div id="inner3" class="inner-frame">
+ <div class="inner-content"></div>
+ </div>
+ </body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_iframe2.html b/gfx/layers/apz/test/mochitest/helper_iframe2.html
new file mode 100644
index 0000000000..fee3883e95
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_iframe2.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<!-- The purpose of the 'id' on the HTML element is to get something
+ identifiable to show up in the root scroll frame's content description,
+ so we can check it for layerization. -->
+<html id="outer4">
+ <head>
+ <link rel="stylesheet" type="text/css" href="helper_subframe_style.css"/>
+ </head>
+ <body>
+ <div id="inner4" class="inner-frame">
+ <div class="inner-content"></div>
+ </div>
+ </body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_iframe_pan.html b/gfx/layers/apz/test/mochitest/helper_iframe_pan.html
new file mode 100644
index 0000000000..032133c255
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_iframe_pan.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity panning test for scrollable div</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var outer = document.getElementById("outer");
+ var transformEndPromise = promiseTransformEnd();
+
+ await synthesizeNativeTouchDrag(outer.contentDocument.body, 10, 100, 0, -50);
+ dump("Finished native drag, waiting for transform-end observer...\n");
+
+ await transformEndPromise;
+
+ dump("Transform complete; flushing repaints...\n");
+ await promiseOnlyApzControllerFlushed(outer.contentWindow);
+
+ var outerScroll = outer.contentWindow.scrollY;
+ if (getPlatform() == "windows") {
+ // On windows, because we run this test with native event synthesization,
+ // Windows can end up eating the first touchmove which can result in the
+ // scroll amount being slightly smaller than 50px. See bug 1388955.
+ dump("iframe scrolled " + outerScroll + "px");
+ ok(outerScroll > 45, "iframe scrolled at least 45 px");
+ ok(outerScroll <= 50, "iframe scrolled at most 50 px");
+ } else {
+ is(outerScroll, 50, "check that the iframe scrolled");
+ }
+}
+
+waitUntilApzStable()
+ .then(test)
+ .then(subtestDone);
+
+ </script>
+</head>
+<body>
+ <iframe id="outer" style="height: 250px; border: solid 1px black" srcdoc="<body style='height:5000px'>"></iframe>
+ <div style="height: 5000px; background-color: lightgreen;">
+ This div makes the top-level page scrollable.
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_iframe_textarea.html b/gfx/layers/apz/test/mochitest/helper_iframe_textarea.html
new file mode 100644
index 0000000000..73cf635b5b
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_iframe_textarea.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <link rel="stylesheet" type="text/css" href="helper_subframe_style.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="apz_test_utils.js"></script>
+ </head>
+ <body>
+ <div style="height: 8000px;">ABC</div>
+ <textarea rows="20"></textarea>
+ <!-- Leave additional room below the element so it can be scrolled to the center -->
+ <div style="height: 1000px;">ABC</div>
+ </body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_interrupted_reflow.html b/gfx/layers/apz/test/mochitest/helper_interrupted_reflow.html
new file mode 100644
index 0000000000..0dcaf1d67d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_interrupted_reflow.html
@@ -0,0 +1,712 @@
+<!DOCTYPE html>
+<html>
+ <!--
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1292781
+ -->
+ <head>
+ <title>Test for bug 1292781</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ .outer {
+ height: 400px;
+ width: 415px;
+ overflow: hidden;
+ position: relative;
+ }
+ .inner {
+ height: 100%;
+ outline: none;
+ overflow-x: hidden;
+ overflow-y: scroll;
+ position: relative;
+ }
+ .inner div:nth-child(even) {
+ background-color: lightblue;
+ }
+ .inner div:nth-child(odd) {
+ background-color: lightgreen;
+ }
+ .outer.contentBefore::before {
+ top: 0;
+ content: '';
+ display: block;
+ height: 2px;
+ position: absolute;
+ width: 100%;
+ z-index: 99;
+ }
+ </style>
+ </head>
+ <body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1292781">Mozilla Bug 1292781</a>
+<p id="display"></p>
+<div id="content">
+ <p>The frame reconstruction should not leave this scrollframe in a bad state</p>
+ <div class="outer">
+ <div class="inner">
+ this is the top of the scrollframe.
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ this is near the top of the scrollframe.
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ this is near the bottom of the scrollframe.
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ this is the bottom of the scrollframe.
+ </div>
+ </div>
+</div>
+
+<pre id="test">
+<script type="text/javascript">
+
+const is = window.opener.is;
+const ok = window.opener.ok;
+const SimpleTest = window.opener.SimpleTest;
+
+// Returns a list of async scroll offsets that the |inner| element had, one for
+// each paint.
+function getAsyncScrollOffsets(aPaintsToIgnore) {
+ var offsets = [];
+ var compositorTestData = SpecialPowers.getDOMWindowUtils(window).getCompositorAPZTestData();
+ var buckets = compositorTestData.paints.slice(aPaintsToIgnore);
+ ok(buckets.length >= 3, "Expected at least three paints in the compositor test data");
+ var childIsLayerized = false;
+ for (var i = 0; i < buckets.length; ++i) {
+ var apzcTree = buildApzcTree(convertScrollFrameData(buckets[i].scrollFrames));
+ var rcd = findRcdNode(apzcTree);
+ if (rcd == null) {
+ continue;
+ }
+ if (rcd.children.length) {
+ // The child may not be layerized in the first few paints, but once it is
+ // layerized, it should stay layerized.
+ childIsLayerized = true;
+ }
+ if (!childIsLayerized) {
+ continue;
+ }
+
+ ok(rcd.children.length == 1, "Root content APZC has exactly one child");
+ offsets.push(parsePoint(rcd.children[0].asyncScrollOffset));
+ }
+ return offsets;
+}
+
+async function test() {
+ var utils = SpecialPowers.DOMWindowUtils;
+
+ // The APZ test data accumulates whenever a test turns it on. We just want
+ // the data for this test, so we check how many frames are already recorded
+ // and discard those later.
+ var framesToSkip = SpecialPowers.getDOMWindowUtils(window).getCompositorAPZTestData().paints.length;
+
+ var elm = document.getElementsByClassName("inner")[0];
+ // Set a zero-margin displayport to ensure that the element is async-scrollable
+ // otherwise on Fennec it is not
+ utils.setDisplayPortMarginsForElement(0, 0, 0, 0, elm, 0);
+
+ var maxScroll = elm.scrollTopMax;
+ elm.scrollTop = maxScroll;
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+
+ // Take control of the refresh driver
+ utils.advanceTimeAndRefresh(0);
+
+ // Force the next reflow to get interrupted
+ utils.forceReflowInterrupt();
+
+ // Make a change that triggers frame reconstruction, and then tick the refresh
+ // driver so that layout processes the pending restyles and then runs an
+ // interruptible reflow. That reflow *will* be interrupted (because of the flag
+ // we set above), and we should end up with a transient 0,0 scroll offset
+ // being sent to the compositor.
+ elm.parentNode.classList.add("contentBefore");
+ utils.advanceTimeAndRefresh(0);
+ // On android, and maybe non-e10s platforms generally, we need to manually
+ // kick the paint to send the layer transaction to the compositor.
+ await promiseAllPaintsDone();
+
+ // Read the main-thread scroll offset; although this is temporarily 0,0 that
+ // temporary value is never exposed to content - instead reading this value
+ // will finish doing the interrupted reflow from above and then report the
+ // correct scroll offset.
+ is(elm.scrollTop, maxScroll, "Main-thread scroll position was restored");
+
+ // .. and now flush everything to make sure the state gets pushed over to the
+ // compositor and APZ as well.
+ utils.restoreNormalRefresh();
+ await promiseApzFlushedRepaints();
+
+ // Now we pull the compositor data and check it. What we expect to see is that
+ // the scroll position goes to maxScroll, then drops to 0 and then goes back
+ // to maxScroll. This test is specifically testing that last bit - that it
+ // properly gets restored from 0 to maxScroll.
+ // The one hitch is that on Android this page is loaded with some amount of
+ // zoom, and the async scroll is in ParentLayerPixel coordinates, so it will
+ // not match maxScroll exactly. Since we can't reliably compute what that
+ // ParentLayer scroll will be, we just make sure the async scroll is nonzero
+ // and use the first value we encounter to verify that it got restored properly.
+ // The other alternative is to spawn this test into a new window with 1.0 zoom
+ // but I'm tired of doing that for pretty much every test.
+ var state = 0;
+ var asyncScrollOffsets = getAsyncScrollOffsets(framesToSkip);
+ dump("Got scroll offsets: " + JSON.stringify(asyncScrollOffsets) + "\n");
+ var maxScrollParentLayerPixels = maxScroll;
+ while (asyncScrollOffsets.length) {
+ let offset = asyncScrollOffsets.shift();
+ switch (state) {
+ // 0 is the initial state, the scroll offset might be zero but should
+ // become non-zero from when we set scrollTop to scrollTopMax
+ case 0:
+ if (offset.y == 0) {
+ break;
+ }
+ if (getPlatform() == "android") {
+ ok(offset.y > 0, "Async scroll y of scrollframe is " + offset.y);
+ maxScrollParentLayerPixels = offset.y;
+ } else {
+ is(offset.y, maxScrollParentLayerPixels, "Async scroll y of scrollframe is " + offset.y);
+ }
+ state = 1;
+ break;
+
+ // state 1 starts out at maxScrollParentLayerPixels, should drop to 0
+ // because of the interrupted reflow putting the scroll into a transient
+ // zero state
+ case 1:
+ if (offset.y == maxScrollParentLayerPixels) {
+ break;
+ }
+ is(offset.y, 0, "Async scroll position was temporarily 0");
+ state = 2;
+ break;
+
+ // state 2 starts out the transient 0 scroll offset, and we expect the
+ // scroll position to get restored back to maxScrollParentLayerPixels
+ case 2:
+ if (offset.y == 0) {
+ break;
+ }
+ is(offset.y, maxScrollParentLayerPixels, "Async scroll y of scrollframe restored to " + offset.y);
+ state = 3;
+ break;
+
+ // Terminal state. The scroll position should stay at maxScrollParentLayerPixels
+ case 3:
+ is(offset.y, maxScrollParentLayerPixels, "Scroll position maintained");
+ break;
+ }
+ }
+ is(state, 3, "The scroll position did drop to 0 and then get restored properly");
+
+ window.opener.finishTest();
+}
+
+waitUntilApzStable()
+.then(async () => test());
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_key_scroll.html b/gfx/layers/apz/test/mochitest/helper_key_scroll.html
new file mode 100644
index 0000000000..021e2803b7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_key_scroll.html
@@ -0,0 +1,109 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1383365
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Async key scrolling test, helper page</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript">
+ // --------------------------------------------------------------------
+ // Async key scrolling test
+ //
+ // This test checks that a key scroll occurs asynchronously.
+ //
+ // The page contains a <div> that is large enough to make the page
+ // scrollable. We first synthesize a page down to scroll to the bottom
+ // of the page. Once we have reached the bottom of the page, we synthesize
+ // a page up to get us back to the top of the page.
+ //
+ // Once at the top, we request test data from APZ, rebuild the APZC tree
+ // structure, and use it to check that async key scrolling happened.
+ // --------------------------------------------------------------------
+
+ async function test() {
+ // Sanity check
+ is(checkHasAsyncKeyScrolled(), false, "expected no async key scrolling before test");
+
+ // Send a key to initiate a page scroll to take us to the bottom of the
+ // page. This scroll is done synchronously because APZ doesn't have
+ // current focus state at page load.
+ let scrollBottomPromise = new Promise(resolve => {
+ let checkBottom = function(e) {
+ if (window.scrollY < window.scrollMaxY) {
+ return;
+ }
+ info("Reached final scroll position of sync KEY_End scroll");
+ window.removeEventListener("scroll", checkBottom);
+ resolve();
+ };
+ window.addEventListener("scroll", checkBottom);
+ });
+
+ window.synthesizeKey("KEY_End");
+ await scrollBottomPromise;
+
+ // Spin the refresh driver a few times, so that the AsyncScroll instance
+ // that was running the main-thread scroll animation finishes up and
+ // triggers any repaints that it needs to.
+ var utils = SpecialPowers.DOMWindowUtils;
+ for (var i = 0; i < 10; i++) {
+ utils.advanceTimeAndRefresh(50);
+ }
+ utils.restoreNormalRefresh();
+
+ // Wait for the APZ to reach a stable state as well, before dispatching
+ // the next key input or the default action won't occur.
+ await promiseApzFlushedRepaints();
+
+ is(checkHasAsyncKeyScrolled(), false, "expected no async key scrolling before KEY_Home dispatch");
+
+ // This scroll should be asynchronous now that the focus state is up to date.
+ let scrollTopPromise = new Promise(resolve => {
+ let checkTop = function(e) {
+ if (window.scrollY > 0) {
+ return;
+ }
+ info("Reached final scroll position of async KEY_Home scroll");
+ window.removeEventListener("scroll", checkTop);
+ resolve();
+ };
+ window.addEventListener("scroll", checkTop);
+ });
+
+ window.synthesizeKey("KEY_Home");
+ await scrollTopPromise;
+
+ // Wait for APZ to settle and then check that async scrolling happened.
+ await promiseApzFlushedRepaints();
+ is(checkHasAsyncKeyScrolled(), true, "expected async key scrolling after test");
+ }
+
+ function checkHasAsyncKeyScrolled() {
+ // Reconstruct the APZC tree structure in the last paint.
+ var apzcTree = getLastApzcTree();
+ var rcd = findRcdNode(apzcTree);
+
+ if (rcd) {
+ return rcd.hasAsyncKeyScrolled === "1";
+ }
+
+ info("Last paint rcd is null");
+ return false;
+ }
+
+ waitUntilApzStable()
+ .then(forceLayerTreeToCompositor)
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</head>
+<body style="height: 500px; overflow: scroll">
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1383365">Async key scrolling test</a>
+ <!-- Put enough content into the page to make it have a nonzero scroll range -->
+ <div style="height: 5000px"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_keyboard_scrollend.html b/gfx/layers/apz/test/mochitest/helper_keyboard_scrollend.html
new file mode 100644
index 0000000000..05ae2dda92
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_keyboard_scrollend.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="initial-scale=1,width=device-width">
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/NativeKeyCodes.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+#scroller {
+ height: 50vh;
+ width: 50vw;
+ overflow: scroll;
+ overscroll-behavior: none;
+}
+
+#spacer {
+ height: 200vh;
+ width: 200vw;
+}
+ </style>
+</head>
+<body>
+ <div id="scroller">
+ <div id="spacer">
+ </div>
+ </div>
+</body>
+<script>
+const searchParams = new URLSearchParams(location.search);
+
+async function test() {
+ var scrollendCount = 0;
+
+ // Add a scrollend event listener that counts the number of scrollend
+ // events fired to the scrollable element.
+ scroller.addEventListener("scrollend", () => {
+ scrollendCount += 1;
+ });
+
+ // Move the mouse over the scroller and perform a mouse click.
+ await promiseNativeMouseEventWithAPZ({
+ target: scroller,
+ offsetX: 10,
+ offsetY: 10,
+ type: "mousemove",
+ });
+
+ await promiseNativeMouseEventWithAPZ({
+ target: scroller,
+ offsetX: 10,
+ offsetY: 10,
+ type: "mousedown",
+ });
+
+ await promiseNativeMouseEventWithAPZ({
+ target: scroller,
+ offsetX: 10,
+ offsetY: 10,
+ type: "mouseup",
+ });
+
+ // Determine the arrow key value based on the value of "direction".
+ let key = null;
+ let direction = searchParams.get("direction");
+ switch (direction) {
+ case "up":
+ key = nativeArrowUpKey();
+ break;
+ case "down":
+ key = nativeArrowDownKey();
+ break;
+ default:
+ ok(false, "Unsupported direction value: " + direction);
+ break;
+ }
+
+ is(scrollendCount, 0, "A scrollend event should not be triggered yet");
+ is(scroller.scrollTop, 0, "No scroll should have occured yet");
+
+ // In order to exercise handling of keyboard events in APZ, we may
+ // want to flush repaints before the key input.
+ if (searchParams.has("flush-before-key")) {
+ await promiseApzFlushedRepaints();
+ }
+ await promiseFrame();
+
+ let transformEndPromise = promiseTransformEnd();
+
+ let scrollPromise = new Promise(resolve => scroller.addEventListener("scroll", resolve));
+ await new Promise(resolve => {
+ synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, key, {},
+ "", "", resolve);
+ });
+
+ await promiseApzFlushedRepaints();
+
+ if (direction == "up") {
+ if (SpecialPowers.getBoolPref("general.smoothScroll")) {
+ // The smooth scroll animation with no distance should not be longer than
+ // half a second.
+ await new Promise(resolve => setTimeout(resolve, 500));
+ } else {
+ await promiseFrame();
+ }
+ is(scroller.scrollTop, 0, "No scroll should have occured");
+ is(scrollendCount, 0, "A user gesture with no scroll should have no scrollend");
+ } else {
+ await scrollPromise;
+ if (SpecialPowers.getBoolPref("general.smoothScroll")) {
+ // If smooth scrolling is enabled and there is room to scroll, wait for the
+ // transform end notification.
+ await transformEndPromise;
+ }
+ await promiseFrame();
+ isnot(scroller.scrollTop, 0, "A scroll should have occured");
+ is(scrollendCount, 1, "A user gesture with a scroll should trigger a scrollend");
+ }
+}
+
+function isOnChaosMode() {
+ return SpecialPowers.Services.env.get("MOZ_CHAOSMODE");
+}
+
+if ((getPlatform() == "mac" || getPlatform() == "windows") &&
+ !isOnChaosMode()) {
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+} else {
+ ok(true, "Skipping test, native key events are not supported on " +
+ getPlatform());
+ subtestDone();
+}
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_long_tap.html b/gfx/layers/apz/test/mochitest/helper_long_tap.html
new file mode 100644
index 0000000000..8baa943f2e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_long_tap.html
@@ -0,0 +1,197 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Ensure we get a touch-cancel after a contextmenu comes up</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function addMouseEventListeners(aTarget) {
+ aTarget.addEventListener("mousemove", recordEvent, true);
+ aTarget.addEventListener("mouseover", recordEvent, true);
+ aTarget.addEventListener("mouseenter", recordEvent, true);
+ aTarget.addEventListener("mouseout", recordEvent, true);
+ aTarget.addEventListener("mouseleave", recordEvent, true);
+}
+
+function removeMouseEventListeners(aTarget) {
+ aTarget.removeEventListener("mousemove", recordEvent, true);
+ aTarget.removeEventListener("mouseover", recordEvent, true);
+ aTarget.removeEventListener("mouseenter", recordEvent, true);
+ aTarget.removeEventListener("mouseout", recordEvent, true);
+ aTarget.removeEventListener("mouseleave", recordEvent, true);
+}
+
+async function longPressLink() {
+ let target = document.getElementById("b");
+ addMouseEventListeners(target);
+ await synthesizeNativeTouch(target, 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, function() {
+ dump("Finished synthesizing touch-start, waiting for events...\n");
+ });
+}
+
+var eventsFired = 0;
+function recordEvent(e) {
+ let target = document.getElementById("b");
+ const platform = getPlatform();
+ if (platform == "windows") {
+ // On Windows we get a mouselongtap event once the long-tap has been detected
+ // by APZ, and that's what we use as the trigger to lift the finger. That then
+ // triggers the contextmenu. This matches the platform convention.
+ switch (eventsFired) {
+ case 0: is(e.type, "touchstart", "Got a touchstart"); break;
+ case 1:
+ is(e.type, "mouselongtap", "Got a mouselongtap");
+ setTimeout(async () => {
+ await synthesizeNativeTouch(document.getElementById("b"), 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE);
+ }, 0);
+ break;
+ case 2: is(e.type, "touchend", "Got a touchend"); break;
+ case 3: is(e.type, "mouseover", "Got a mouseover"); break;
+ case 4: is(e.type, "mouseenter", "Got a mouseenter"); break;
+ case 5: is(e.type, "mousemove", "Got a mousemove"); break;
+ case 6: is(e.type, "contextmenu", "Got a contextmenu"); e.preventDefault(); break;
+ default: ok(false, "Got an unexpected event of type " + e.type); break;
+ }
+ eventsFired++;
+
+ if (eventsFired == 7) {
+ removeMouseEventListeners(target);
+ dump("Finished waiting for events, doing an APZ flush to see if any more unexpected events come through...\n");
+ promiseOnlyApzControllerFlushed().then(function() {
+ dump("Done APZ flush, ending test...\n");
+ subtestDone();
+ });
+ }
+ } else if (platform != "android") {
+ // On non-Windows desktop platforms we get a contextmenu event once the
+ // long-tap has been detected. Since we prevent-default that, we don't get
+ // a mouselongtap event at all, and instead get a touchcancel.
+ switch (eventsFired) {
+ case 0: is(e.type, "touchstart", "Got a touchstart"); break;
+ case 1: is(e.type, "mouseover", "Got a mouseover"); break;
+ case 2: is(e.type, "mouseenter", "Got a mouseenter"); break;
+ case 3: is(e.type, "mousemove", "Got a mousemove"); break;
+ case 4: is(e.type, "contextmenu", "Got a contextmenu");
+ // Do preventDefault() in this content, thus we will not get any
+ // touchcancel event.
+ e.preventDefault();
+ setTimeout(async () => {
+ await synthesizeNativeTouch(target, 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, function() {
+ dump("Finished synthesizing touch-end, waiting for a touchend event...\n");
+ });
+ }, 0);
+ break;
+ case 5: is(e.type, "touchend", "Got a touchend");
+ // Send another long press.
+ setTimeout(async () => {
+ await synthesizeNativeTouch(target, 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, function() {
+ dump("Finished synthesizing touch-start, waiting for events...\n");
+ });
+ }, 0);
+ break;
+ case 6: is(e.type, "touchstart", "Got another touchstart"); break;
+ // NOTE: In this another event case, we don't get mouseover or mouseenter
+ // event either since the target element hasn't been changed.
+ case 7: is(e.type, "mousemove", "Got another mousemove"); break;
+ case 8: is(e.type, "contextmenu", "Got another contextmenu");
+ // DON'T DO preventDefault() this time, thus we should get a touchcancel
+ // event.
+ break;
+ case 9: is(e.type, "mouselongtap", "Got a mouselongtap"); break;
+ case 10: is(e.type, "touchcancel", "Got a touchcancel"); break;
+ default: ok(false, "Got an unexpected event of type " + e.type); break;
+ }
+ eventsFired++;
+
+ if (eventsFired == 11) {
+ removeMouseEventListeners(target);
+
+ setTimeout(async () => {
+ // Ensure the context menu got closed, otherwise in the next test case
+ // events will be consumed by the context menu unfortunately.
+ await closeContextMenu();
+
+ await synthesizeNativeTouch(target, 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, function() {
+ dump("Finished synthesizing touch-end, doing an APZ flush to see if any more unexpected events come through...\n");
+ promiseOnlyApzControllerFlushed().then(function() {
+ dump("Done APZ flush, ending test...\n");
+ subtestDone();
+ });
+ });
+ }, 0);
+ }
+ } else {
+ // On Android we get a contextmenu event once the long-tap has been
+ // detected. If contextmenu opens we get a touchcancel event, and if
+ // contextmenu didn't open because of preventDefault() in the content,
+ // we will not get the touchcancel event.
+ switch (eventsFired) {
+ case 0: is(e.type, "touchstart", "Got a touchstart"); break;
+ case 1: is(e.type, "mouseover", "Got a mouseover"); break;
+ case 2: is(e.type, "mouseenter", "Got a mouseenter"); break;
+ case 3: is(e.type, "mousemove", "Got a mousemove"); break;
+ case 4: is(e.type, "contextmenu", "Got a contextmenu");
+ // Do preventDefault() in this content, thus we will not get any
+ // touchcancel event.
+ e.preventDefault();
+ setTimeout(async () => {
+ await synthesizeNativeTouch(target, 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, function() {
+ dump("Finished synthesizing touch-end, waiting for a touchend event...\n");
+ });
+ }, 0);
+ break;
+ case 5: is(e.type, "touchend", "Got a touchend");
+ // Send another long press.
+ setTimeout(async () => {
+ await synthesizeNativeTouch(target, 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, function() {
+ dump("Finished synthesizing touch-start, waiting for events...\n");
+ });
+ }, 0);
+ break;
+ case 6: is(e.type, "touchstart", "Got another touchstart"); break;
+ // NOTE: In this another event case, we don't get mouseover or mouseenter
+ // event either since the target element hasn't been changed.
+ case 7: is(e.type, "mousemove", "Got another mousemove"); break;
+ case 8: is(e.type, "contextmenu", "Got another contextmenu");
+ // DON'T DO preventDefault() this time, thus we should get a touchcancel
+ // event.
+ break;
+ case 9: is(e.type, "touchcancel", "Got a touchcancel"); break;
+ default: ok(false, "Got an unexpected event of type " + e.type); break;
+ }
+ eventsFired++;
+
+ if (eventsFired == 10) {
+ removeMouseEventListeners(target);
+ setTimeout(async () => {
+ await synthesizeNativeTouch(target, 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, function() {
+ dump("Finished synthesizing touch-end, doing an APZ flush to see if any more unexpected events come through...\n");
+ promiseOnlyApzControllerFlushed().then(function() {
+ dump("Done APZ flush, ending test...\n");
+ subtestDone();
+ });
+ });
+ }, 0);
+ }
+ }
+}
+
+window.addEventListener("touchstart", recordEvent, { passive: true, capture: true });
+window.addEventListener("touchend", recordEvent, { passive: true, capture: true });
+window.addEventListener("touchcancel", recordEvent, true);
+window.addEventListener("contextmenu", recordEvent, true);
+SpecialPowers.addChromeEventListener("mouselongtap", recordEvent, true);
+
+waitUntilApzStable()
+.then(longPressLink);
+
+ </script>
+</head>
+<body>
+ <a id="b" href="#">Link to nowhere</a>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_main_thread_smooth_scroll_scrollend.html b/gfx/layers/apz/test/mochitest/helper_main_thread_smooth_scroll_scrollend.html
new file mode 100644
index 0000000000..4f07db516e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_main_thread_smooth_scroll_scrollend.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html {
+ overflow: hidden;
+ }
+ #spacer {
+ height: 200vh;
+ width: 200vw;
+ position: absolute;
+ background: linear-gradient(green, blue);
+ }
+ </style>
+</head>
+<body>
+<div id="spacer"></div>
+</body>
+<script>
+async function test() {
+ let scrollendCount = 0;
+
+ window.addEventListener("scrollend", e => {
+ scrollendCount += 1;
+ });
+
+ window.scrollTo({ top: window.scrollY, behavior: 'smooth' });
+
+ await promiseFrame();
+
+ is(scrollendCount, 0, "Scrollend is not fired for a main thread no-op smooth scroll");
+
+ window.scrollTo({ top: window.scrollY + 200, behavior: 'smooth' });
+
+ await promiseOneEvent(window, "scrollend");
+
+ is(scrollendCount, 1, "Scrollend is fired for a main thread smooth scroll");
+}
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_mainthread_scroll_bug1662379.html b/gfx/layers/apz/test/mochitest/helper_mainthread_scroll_bug1662379.html
new file mode 100644
index 0000000000..f784d4219f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_mainthread_scroll_bug1662379.html
@@ -0,0 +1,174 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, minimum-scale=1.0">
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<div id="content">
+ <div id="lhs">
+ <ul>
+ <li>Test item 1</li>
+ <li>Test item 2</li>
+ <li>Test item 3</li>
+ <li>Test item 4</li>
+ <li>Test item 5</li>
+ <li>Test item 6</li>
+ <li>Test item 7</li>
+ <li>Test item 8</li>
+ <li>Test item 9</li>
+ <li>Test item 10</li>
+ <li>Test item 11</li>
+ <li>Test item 13</li>
+ <li>Test item 14</li>
+ <li>Test item 15</li>
+ <li>Test item 16</li>
+ <li>Test item 17</li>
+ <li>Test item 18</li>
+ <li>Test item 19</li>
+ </ul>
+ </div>
+ <div id="center">
+ <div>
+ Steps to reproduce:
+ <ol>
+ <li>Scroll the list of "test items" all the way to the bottom
+ <li>Click on the reparent button below
+ <li>Click on one of the test items
+ <li>The `clickTarget` JS variable should match the thing you clicked on
+ </ol>
+ </div>
+ <button onclick="reparent()"> Click here to reparent </button>
+ </div>
+</div>
+<style>
+#content {
+ display: flex;
+ height: 300px;
+ background-color: pink;
+ border: 3px solid green;
+}
+
+#lhs, #rhs {
+ width: 250px;
+ overflow: scroll;
+ flex: 0 0 250px
+}
+
+#center {
+ padding: 20px;
+}
+
+ul {
+ margin: 16px 0px;
+}
+
+/* Each element has a border-height of 20 + (2 * 5) + (2 * 1) = 32px */
+ul li {
+ background-color: aqua;
+ border: 1px solid blue;
+ padding: 5px;
+ cursor: pointer;
+ height: 20px;
+}
+</style>
+<script>
+var clickTarget = null;
+
+for (var el of document.querySelectorAll("ul li")) {
+ el.addEventListener("click", function(e) {
+ clickTarget = e.target;
+ });
+}
+
+function reparent() {
+ var content = document.getElementById("content");
+ var lhs = document.getElementById("lhs");
+ content.appendChild(lhs);
+ lhs.id = "rhs";
+}
+
+function getAsyncScrollOffsetForUniqueRcdChild() {
+ let apzcTree = getLastApzcTree();
+ let rcd = findRcdNode(apzcTree);
+ // We assume a unique child of the RCD. If this is not the case, bail out.
+ if (rcd == null || rcd.children.length != 1) {
+ info("Did not find unique child on the RCD: rcd=" + JSON.stringify(rcd));
+ return {x: -1, y: -1};
+ }
+ let child = rcd.children[0];
+ return parsePoint(child.asyncScrollOffset);
+}
+
+async function test() {
+ if (getPlatform() == "android") {
+ ok(true, "Mousewheel is not supported on android, skipping test...");
+ return;
+ }
+
+ // Simulate user mouse-wheel scrolling the lhs pane down to the bottom.
+ let lhs = document.getElementById("lhs");
+ let scrollendPromise = promiseScrollend(lhs);
+ while (lhs.scrollTop < lhs.scrollTopMax) {
+ await promiseNativeWheelAndWaitForScrollEvent(
+ lhs,
+ 50, 50,
+ 0, -50);
+ info("Did scroll, pos is now " + lhs.scrollTop + "/" + lhs.scrollTopMax);
+ }
+ // Wait for the animation to be completely done. (scrollTop is rounded to
+ // the nearest integer value, so at the time scrollTop reaches scrollTopMax,
+ // the compositor animation may still be a sub-pixel amount away from the
+ // destination).
+ await scrollendPromise;
+ await promiseApzFlushedRepaints();
+
+ // Click at 50,50 from the top-left corner of the lhs pane. If lhs were
+ // not scrolled, this would hit "Test item 2" but since lhs is scrolled
+ // it should hit something else. So let's check that it doesn't hit
+ // "Test item 2".
+ await promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "click",
+ target: lhs,
+ offsetX: 50,
+ offsetY: 50,
+ });
+ isnot(clickTarget, null, "Click target got set");
+ info("Hit " + clickTarget.textContent);
+ isnot(clickTarget.textContent, "Test item 2", "Item two didn't get hit");
+ clickTarget = null;
+
+ // Do the reparenting
+ reparent();
+ await promiseApzFlushedRepaints();
+ info("Done reparent + wait, about to fire click...");
+
+ // Now fire the click on the reparented element (which is now called "rhs")
+ // at the same 50,50 offset from the top-left. This time it *should* hit
+ // "Test item 2" because the reparenting should reset the scroll offset
+ // back to zero.
+ await promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "click",
+ target: document.getElementById("rhs"),
+ offsetX: 50,
+ offsetY: 50,
+ });
+
+ // Check that the visual scroll position (as determined by the compositor
+ // scroll offset) is consistent with the main-thread scroll position (as
+ // determined by the click target). In both cases the scroll position
+ // should have gotten reset back to zero with the reparenting step. In
+ // bug 1662379 the visual scroll position was *not* getting reset, and
+ // so even though the main-thread click target was "Test item 2", the
+ // compositor scroll offset was non-zero, so to the user the click
+ // appeared to trigger something different from what they clicked on.
+ isnot(clickTarget, null, "Click target got set");
+ is(clickTarget.textContent, "Test item 2", "Item two got hit correctly");
+ let rhsCompositorScrollOffset = getAsyncScrollOffsetForUniqueRcdChild();
+ is(rhsCompositorScrollOffset.x, 0, "rhs compositor x-offset is zero");
+ is(rhsCompositorScrollOffset.y, 0, "rhs compositor y-offset is zero");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_minimum_scale_1_0.html b/gfx/layers/apz/test/mochitest/helper_minimum_scale_1_0.html
new file mode 100644
index 0000000000..17ccb3a54d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_minimum_scale_1_0.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=200, minimum-scale=1.0, initial-scale=2.0">
+ <title>Tests that the layout viewport is expanted to the minimum scale size (minimim-scale >= 1.0)</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html,body {
+ overflow-x: hidden;
+ margin: 0;
+ }
+ div {
+ position: absolute;
+ }
+ </style>
+</head>
+<body>
+ <div style="width: 200%; height: 200%; background-color: green"></div>
+ <div style="width: 100%; height: 100%; background-color: blue"></div>
+ <script type="application/javascript">
+ const utils = SpecialPowers.getDOMWindowUtils(window);
+
+ async function test(testDriver) {
+ utils.scrollToVisual(100, 0, utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_INSTANT);
+
+ const promiseForVisualViewportScroll = new Promise(resolve => {
+ window.visualViewport.addEventListener("scroll", () => {
+ resolve();
+ }, { once: true });
+ });
+
+ await waitUntilApzStable();
+
+ await promiseForVisualViewportScroll;
+
+ is(visualViewport.offsetLeft, 100,
+ "The visual viewport offset should be moved");
+ }
+
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_no_scalable_with_initial_scale.html b/gfx/layers/apz/test/mochitest/helper_no_scalable_with_initial_scale.html
new file mode 100644
index 0000000000..7280a26006
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_no_scalable_with_initial_scale.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, minimum-scale=0.25, initial-scale=0.5, user-scalable=no">
+ <title>Tests that the layout viewport is not expanted to the minimum scale size if user-scalable=no is specified</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html,body {
+ overflow: hidden;
+ margin: 0;
+ }
+ div {
+ position: absolute;
+ }
+ </style>
+</head>
+<body>
+ <div style="width: 400%; height: 400%; background: red;"></div>
+ <div style="width: 100%; height: 100%; background-color: blue"></div>
+ <script type="application/javascript">
+ const utils = SpecialPowers.getDOMWindowUtils(window);
+
+ async function test(testDriver) {
+ utils.scrollToVisual(100, 0, utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_INSTANT);
+
+ let receivedScrollEvent = false;
+ window.visualViewport.addEventListener("scroll", () => {
+ receivedScrollEvent = true;
+ }, { once: true });
+
+ await waitUntilApzStable();
+
+ // Waits two frames to get a chance to deliver scroll events.
+ await promiseFrame();
+ await promiseFrame();
+
+ ok(!receivedScrollEvent, "Scroll should never happen");
+ is(visualViewport.offsetLeft, 0,
+ "The visual viewport offset should not be moved");
+ }
+
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_onetouchpinch_nested.html b/gfx/layers/apz/test/mochitest/helper_onetouchpinch_nested.html
new file mode 100644
index 0000000000..54b578b496
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_onetouchpinch_nested.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>One-touch pinch zooming while on a non-root scroller</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test_onetouchpinch() {
+ // layerize the scroller so it gets an APZC and GestureEventListener
+ var scroller = document.getElementById("scroller");
+ SpecialPowers.getDOMWindowUtils(window).setDisplayPortForElement(0, 0, 400, 1000, scroller, 1);
+ await promiseApzFlushedRepaints();
+
+ ok(isLayerized("scroller"), "scroller has been successfully layerized");
+
+ var initial_resolution = await getResolution();
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+
+ let transformEndPromise = promiseTransformEnd();
+
+ function translateY(point, dy) {
+ return {x: point.x, y: point.y + dy};
+ }
+
+ var zoom_point = centerOf(scroller);
+ var zoom_in = [
+ [ zoom_point ],
+ [ null ],
+ [ zoom_point ],
+ [ translateY(zoom_point, 5) ],
+ [ translateY(zoom_point, 10) ],
+ [ translateY(zoom_point, 15) ],
+ [ translateY(zoom_point, 20) ],
+ [ translateY(zoom_point, 25) ],
+ ];
+
+ var touchIds = [0];
+ await synthesizeNativeTouchSequences(scroller, zoom_in, null, touchIds);
+
+ // Wait for the APZ:TransformEnd to be fired after touch events are processed.
+ await transformEndPromise;
+
+ // Flush state and get the resolution we're at now
+ await promiseApzFlushedRepaints();
+ let final_resolution = await getResolution();
+ ok(final_resolution > initial_resolution, "The final resolution (" + final_resolution + ") is greater after zooming in");
+
+ // Also check that the scroller didn't get scrolled.
+ is(scroller.scrollTop, 0, "scroller didn't y-scroll");
+ is(scroller.scrollLeft, 0, "scroller didn't x-scroll");
+}
+
+async function test() {
+ // Run the test with the scrollable div
+ await test_onetouchpinch();
+ dump("Wrapping scroller in fixed-pos div...\n");
+ // Now wrap the scrollable div inside a fixed-pos div
+ var fixedElement = document.createElement("div");
+ fixedElement.id = "fixed";
+ document.body.appendChild(fixedElement);
+ fixedElement.appendChild(document.getElementById("scroller"));
+ dump("Done wrapping scroller in fixed-pos div.\n");
+ // Now run the test again, with the scrollable div inside a fixed-pos div
+ await test_onetouchpinch();
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ #scroller {
+ width: 300px;
+ height: 300px;
+ overflow: scroll;
+ }
+
+ #fixed {
+ background-color: green;
+ position: fixed;
+ width: 300px;
+ height: 300px;
+ left: 100px;
+ top: 100px;
+ }
+ </style>
+</head>
+<body>
+ Here is some text outside the scrollable div.
+ <div id="scroller">
+ Here is some text inside the scrollable div.
+ <div style="height: 2000px">This div actually makes it overflow.</div>
+ </div>
+ <div style="height: 2000px">This div makes the body scrollable.</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_overflowhidden_zoom.html b/gfx/layers/apz/test/mochitest/helper_overflowhidden_zoom.html
new file mode 100644
index 0000000000..6c12008e4b
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_overflowhidden_zoom.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, minimum-scale=1.0">
+ <title>Tests that zooming in and out doesn't change the scroll position on an overflow hidden document</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html,body {
+ overflow: hidden;
+ }
+ </style>
+</head>
+<body>
+ <div style="height: 20000px; background-color: green"></div>
+ <script>
+ const utils = SpecialPowers.getDOMWindowUtils(window);
+
+ async function test() {
+ is(await getResolution(), 1.0, "should not be zoomed (1)");
+
+ is(window.scrollX, 0, "shouldn't have scrolled (2)");
+ is(window.scrollY, 0, "shouldn't have scrolled (3)");
+ is(visualViewport.pageTop, 0, "shouldn't have scrolled (4)");
+ is(visualViewport.pageLeft, 0, "shouldn't have scrolled (5)");
+
+ // Force reconstruction of the root scroll frame to trigger bug 1665332.
+ document.documentElement.style.display = "flex";
+ document.documentElement.offsetLeft;
+ document.documentElement.style.display = "";
+ document.documentElement.offsetLeft;
+
+ is(await getResolution(), 1.0, "should not be zoomed (6)");
+
+ is(window.scrollX, 0, "shouldn't have scrolled (7)");
+ is(window.scrollY, 0, "shouldn't have scrolled (8)");
+ is(visualViewport.pageTop, 0, "shouldn't have scrolled (9)");
+ is(visualViewport.pageLeft, 0, "shouldn't have scrolled (10)");
+
+ // Zoom in
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(4.0);
+ await promiseApzFlushedRepaints();
+
+ is(await getResolution(), 4.0, "should be zoomed (11)");
+
+ is(window.scrollX, 0, "shouldn't have scrolled (12)");
+ is(window.scrollY, 0, "shouldn't have scrolled (13)");
+ is(visualViewport.pageTop, 0, "shouldn't have scrolled (14)");
+ is(visualViewport.pageLeft, 0, "shouldn't have scrolled (15)");
+
+ // Scroll so the visual viewport offset is non-zero
+ utils.scrollToVisual(20000, 20000, utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_INSTANT);
+
+ await promiseApzFlushedRepaints();
+
+ is(await getResolution(), 4.0, "should be zoomed (16)");
+
+ is(window.scrollX, 0, "shouldn't have scrolled (17)");
+ is(window.scrollY, 0, "shouldn't have scrolled (18)");
+ isnot(visualViewport.pageTop, 0, "should have scrolled (19)");
+ isnot(visualViewport.pageLeft, 0, "should have scrolled (20)");
+
+ // Zoom back out
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(1.0);
+ await promiseApzFlushedRepaints();
+
+ is(await getResolution(), 1.0, "should not be zoomed (21)");
+
+ is(window.scrollX, 0, "shouldn't have scrolled (22)");
+ is(window.scrollY, 0, "shouldn't have scrolled (23)");
+ is(visualViewport.pageTop, 0, "shouldn't have scrolled (24)");
+ is(visualViewport.pageLeft, 0, "shouldn't have scrolled (25)");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_override_root.html b/gfx/layers/apz/test/mochitest/helper_override_root.html
new file mode 100644
index 0000000000..81c1b34938
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_override_root.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Simple wheel scroll cancellation</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let wheelEventPromise = new Promise(resolve => {
+ // Add a non-passive listener on the document, so that we have a document-level
+ // APZ-aware listener, and the entire document is put in the dispatch-to-content
+ // region
+ document.addEventListener("wheel", function(e) {
+ // spin for 2 seconds to give APZ time to scroll, if the event region override
+ // is broken and it decides not to wait for the main thread. Note that it's
+ // possible the APZ controller thread is busy for whatever reason so APZ
+ // may not scroll. That might cause this test to only fail intermittently
+ // instead of consistently if the behaviour being tested regresses.
+ var now = Date.now();
+ while (Date.now() - now < 2000);
+
+ // Cancel the scroll. If this works then we know APZ waited for this listener
+ // to run.
+ e.preventDefault();
+ resolve()
+ }, { passive: false });
+ });
+
+ // Ensure APZ gets a paint with the d-t-c region
+ await promiseApzFlushedRepaints();
+
+ await synthesizeNativeWheel(document.body, 100, 100, 0, -50);
+ dump("Finished native wheel, waiting for listener to run...\n");
+
+ await wheelEventPromise;
+ await promiseOnlyApzControllerFlushed();
+
+ is(window.scrollY, 0, "check that the window didn't scroll");
+}
+
+if (window.top != window) {
+ dump("Running inside an iframe! stealing functions from window.top...\n");
+ window.subtestDone = window.top.subtestDone;
+ window.SimpleTest = window.top.SimpleTest;
+ window.is = window.top.is;
+ window.ok = window.top.ok;
+}
+
+waitUntilApzStable()
+ .then(test)
+ .then(subtestDone);
+
+ </script>
+</head>
+<body style="height: 5000px; background-image: linear-gradient(green,red);">
+ This page should not be wheel-scrollable.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_override_subdoc.html b/gfx/layers/apz/test/mochitest/helper_override_subdoc.html
new file mode 100644
index 0000000000..910f36ddc3
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_override_subdoc.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Wheel scroll cancellation inside iframe</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+</head>
+<body>
+ This just loads helper_override_root in an iframe, so that we test event
+ regions overriding on in-process subdocuments.
+ <iframe id="ifr" src="helper_override_root.html" onload="document.getElementById('ifr').focus()"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425573.html b/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425573.html
new file mode 100644
index 0000000000..817522f9c3
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425573.html
@@ -0,0 +1,44 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Wheel-scrolling over inactive subframe with overscroll-behavior</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var subframe = document.getElementById("scroll");
+
+ // scroll over the middle of the subframe, and make sure that the page
+ // does not scroll.
+ var waitForScroll = false; // don't wait for a scroll event, it will never come
+ await promiseMoveMouseAndScrollWheelOver(subframe, 100, 100, waitForScroll);
+ ok(window.scrollY == 0, "overscroll-behavior was respected");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ #scroll {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+ overscroll-behavior: contain;
+ }
+ #scrolled {
+ width: 200px;
+ height: 1000px; /* so the subframe has room to scroll */
+ background: linear-gradient(red, blue); /* so you can see it scroll */
+ }
+ </style>
+</head>
+<body>
+ <div id="scroll">
+ <div id="scrolled"></div>
+ </div>
+ <div style="height: 5000px;"></div><!-- So the page is scrollable as well -->
+</body>
+</head>
diff --git a/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425603.html b/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425603.html
new file mode 100644
index 0000000000..8324530c95
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425603.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Scrolling over checkerboarded area respects overscroll-behavior</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ #subframe {
+ width: 100px;
+ height: 100px;
+ overflow: scroll;
+ margin-top: 10px;
+ margin-left: 10px;
+ overscroll-behavior: contain;
+ }
+ #contents {
+ width: 100%;
+ height: 1000px;
+ background-image: linear-gradient(red, blue);
+ }
+ </style>
+</head>
+<body>
+ <div id="subframe">
+ <div id="contents"></div>
+ </div>
+ <div id="make_root_scrollable" style="height: 5000px"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ var subframe = document.getElementById("subframe");
+
+ // Activate the scrollframe but keep the main-thread scroll position at 0.
+ // Also apply an async scroll offset in the y-direction large enough
+ // to make the scrollframe checkerboard.
+ // Note: We have to be careful with the numbers here.
+ // promiseMoveMouseAndScrollWheelOver() relies on the main thread receiving
+ // the synthesized mouse-move and wheel events. However, the async
+ // transform created by setAsyncScrollOffset() will cause an untransform
+ // to be applied to the synthesized events' coordinates before they're
+ // passed to the main thread. We have to make sure the transform is
+ // large enough to cause the scroll frame to checkerboard, but not so
+ // large that the untransformed coordinates hit-test out of bounds for
+ // the browser's content area. This is why we make the scroll frame
+ // small (100x100), and give it a display port that's also just 100x100,
+ // so we can keep the async scroll offset small enough (300 in this case)
+ // that the untransformed coordinates are still in-bounds for the window.
+ utils.setDisplayPortForElement(0, 0, 100, 100, subframe, 1);
+ await promiseAllPaintsDone();
+ var scrollY = 300;
+ utils.setAsyncScrollOffset(subframe, 0, scrollY);
+ // Tick the refresh driver once to make sure the compositor has applied the
+ // async scroll offset (for WebRender hit-testing we need to make sure WR has
+ // the latest info).
+ utils.advanceTimeAndRefresh(16);
+ utils.restoreNormalRefresh();
+
+ // Scroll over the subframe, and make sure that the page does not scroll,
+ // i.e. overscroll-behavior is respected.
+ var waitForScroll = false; // don't wait for a scroll event, it will never come
+ await promiseMoveMouseAndScrollWheelOver(subframe, 50, 50, waitForScroll);
+ is(window.scrollY, 0, "overscroll-behavior was respected");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1494440.html b/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1494440.html
new file mode 100644
index 0000000000..3f12e36102
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1494440.html
@@ -0,0 +1,50 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Inactive iframe with overscroll-behavior</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+</head>
+<body>
+ <iframe id="scroll" srcdoc="<!doctype html><html style='overscroll-behavior:none; overflow: auto;'><div style='width:100px;height:2000px;'>">
+ </iframe>
+ <div style="height: 5000px;"></div><!-- So the page is scrollable as well -->
+
+ <script type="application/javascript">
+
+async function test() {
+ var iframe = document.getElementById("scroll");
+ var iframeWindow = iframe.contentWindow;
+
+ // scroll the iframe to the bottom, such that a subsequent scroll on it
+ // _would_ hand off to the page if overscroll-behavior allowed it
+ iframeWindow.scrollTo(0, iframeWindow.scrollMaxY);
+ await promiseApzFlushedRepaints();
+ is(iframeWindow.scrollY, iframeWindow.scrollMaxY, "iframe has scrolled to the bottom");
+
+ // Scroll over the iframe, and make sure that the page
+ // does not scroll.
+ // We can't wait for a "scroll" event unconditionally, since if the platform
+ // behaviour we are testing is correct (overscroll-behavior is respected),
+ // one will never arrive.
+ var waitForScroll = false;
+ await promiseMoveMouseAndScrollWheelOver(iframeWindow, 100, 100, waitForScroll);
+ // However, we need to give a potential "scroll" event a chance to be dispatched,
+ // so that if the platform behaviour we are testing is incorrect (overscroll-behavior)
+ // is not respected, we catch it.
+ await promiseApzFlushedRepaints();
+ is(window.scrollY, 0, "overscroll-behavior was respected");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ #scroll {
+ width: 200px;
+ height: 500px;
+ }
+ </style>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_overscroll_in_apz_test_data.html b/gfx/layers/apz/test/mochitest/helper_overscroll_in_apz_test_data.html
new file mode 100644
index 0000000000..ed05f25819
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_overscroll_in_apz_test_data.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, minimum-scale=1.0">
+<title>A simple test checks "overscrolled" info in APZTestData</title>
+<title>Tests scroll anchoring updates in-progress wheel scrolling __relatively__</title>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<div style="height: 300vh; background-color: blue;"></div><!-- Make the root scrollable -->
+<script>
+ async function test() {
+ // Try to overscroll by using setAsyncScrollOffset to avoid race conditions
+ // that APZTestData haven't been arrived on the main-thread.
+ SpecialPowers.DOMWindowUtils.setAsyncScrollOffset(document.scrollingElement, 0, -100);
+
+ const scrollId =
+ SpecialPowers.DOMWindowUtils.getViewId(document.scrollingElement);
+ const data = SpecialPowers.DOMWindowUtils.getCompositorAPZTestData();
+ for (apzcData of data.additionalData) {
+ if (apzcData.key == scrollId) {
+ ok(apzcData.value.split(",").includes("overscrolled"));
+ }
+ }
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_overscroll_in_subscroller.html b/gfx/layers/apz/test/mochitest/helper_overscroll_in_subscroller.html
new file mode 100644
index 0000000000..5936de97f7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_overscroll_in_subscroller.html
@@ -0,0 +1,165 @@
+<!DOCTYPE HTML>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, minimum-scale=1.0">
+<title>
+ Tests that the overscroll gutter in a sub scroll container is restored if it's
+ no longer target scroll container
+</title>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+ html {
+ overflow: scroll;
+ }
+
+ .content {
+ height: 500px;
+ width: 300px;
+ overflow-y: scroll;
+ background-color: red;
+ }
+</style>
+<!-- a sub scroll container -->
+<div class="content">
+ <div style="height:100vh; background-color: white;"></div>
+</div>
+<div style="height:200vh"></div>
+<script>
+ document.documentElement.addEventListener(
+ "wheel",
+ e => {
+ if (!e.target.closest(`div[class="content"]`)) {
+ e.preventDefault();
+ }
+ },
+ {
+ passive: false,
+ }
+ );
+
+ const subScroller = document.querySelector(`div[class="content"]`);
+ // Make the sub scroll container overscrollable at the top edge.
+ // A `waitUntilApzStable()` call below ensures that this scroll position
+ // has been informed into APZ before starting this test.
+ subScroller.scrollTop = 1;
+
+ // A utility function to collect overscrolled scroll container information.
+ function collectOverscrolledData() {
+ const apzData = SpecialPowers.DOMWindowUtils.getCompositorAPZTestData().additionalData;
+ return apzData.filter(data => {
+ return SpecialPowers.wrap(data).value.split(",").includes("overscrolled");
+ });
+ }
+
+ async function test() {
+ const oneScrollPromise = new Promise(resolve => {
+ subScroller.addEventListener("scroll", () => {
+ resolve();
+ }, { once: true });
+ });
+
+ // Start a pan upward gesture to try oversrolling on the sub scroll
+ // container.
+ await NativePanHandler.promiseNativePanEvent(
+ subScroller,
+ 100,
+ 100,
+ 0,
+ -NativePanHandler.delta,
+ NativePanHandler.beginPhase
+ );
+
+ const rootScrollId =
+ SpecialPowers.DOMWindowUtils.getViewId(document.scrollingElement);
+ const subScrollId =
+ SpecialPowers.DOMWindowUtils.getViewId(subScroller);
+
+ await promiseApzFlushedRepaints();
+ await oneScrollPromise;
+
+ let overscrolledData = collectOverscrolledData();
+ ok(overscrolledData.length >= 1,
+ "There should be at least one overscrolled scroll container");
+ ok(overscrolledData.every(data => SpecialPowers.wrap(data).key == subScrollId),
+ "The overscrolled scroll container should be the sub scroll container");
+
+ let twoScrollEndPromise = new Promise(resolve => {
+ let count = 0;
+ subScroller.addEventListener("scrollend", () => {
+ count++;
+ ok(count <= 2, "There should never be more than two scrollend events");
+ if (count == 2) {
+ resolve();
+ }
+ });
+ });
+
+ // Finish the pan upward gesture.
+ await NativePanHandler.promiseNativePanEvent(
+ subScroller,
+ 100,
+ 100,
+ 0,
+ 0,
+ NativePanHandler.endPhase
+ );
+
+ await promiseApzFlushedRepaints();
+
+ // Now do another pan upward gesture again.
+ await NativePanHandler.promiseNativePanEvent(
+ subScroller,
+ 100,
+ 100,
+ 0,
+ -NativePanHandler.delta,
+ NativePanHandler.beginPhase
+ );
+
+ // Wait two `apz-repaints-flushed`s to give a chance to overscroll the root
+ // scroll container.
+ await promiseApzFlushedRepaints();
+ await promiseApzFlushedRepaints();
+
+ overscrolledData = collectOverscrolledData();
+ ok(overscrolledData.length >= 2,
+ "There should be at least two overscrolled scroll containers");
+ ok(overscrolledData.some(data => SpecialPowers.wrap(data).key == rootScrollId),
+ "The root scroll container should be overscrolled");
+ ok(overscrolledData.some(data => SpecialPowers.wrap(data).key == subScrollId),
+ "The sub scroll container should also be overscrolled");
+
+ // While the root scroll container is still being overscrolled because the
+ // new pan gesture is still on-going, the sub scroll container should be
+ // restored.
+ // Note that this test relies on the fact that two scrollend events get
+ // fired when overscrolling happens, one gets fired when the scroll position
+ // reached to the edge of the scrollport (i.e. just about to start
+ // overscrolling), the other one gets fired when overscrolling ends.
+ await twoScrollEndPromise;
+ info("Got two scroll end events on the sub scroll container");
+
+ await promiseApzFlushedRepaints();
+
+ overscrolledData = collectOverscrolledData();
+ ok(overscrolledData.length >= 1,
+ "There should be at least one overscrolled scroll container");
+ ok(overscrolledData.every(data => SpecialPowers.wrap(data).key == rootScrollId),
+ "The root scroll container should still be overscrolled");
+
+ // Finish the pan upward gesture.
+ await NativePanHandler.promiseNativePanEvent(
+ subScroller,
+ 100,
+ 100,
+ 0,
+ 0,
+ NativePanHandler.endPhase
+ );
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-1.html b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-1.html
new file mode 100644
index 0000000000..6c986b0ca1
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-1.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ overscroll handoff for fixed elements</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+html, body {
+ height: 100%;
+ overflow: hidden;
+}
+
+#main {
+ height: 100%;
+ overflow: auto;
+}
+
+#spacer {
+ height: 5000px;
+}
+
+#fixed {
+ position: fixed;
+ top: 50%;
+ left: 0;
+ width: 100%;
+ height: 100px;
+ background: red;
+ overflow: auto;
+}
+
+#long {
+ height: 250px;
+ width: 50%;
+ position: absolute;
+ background: green;
+ top: 0;
+ left: 25%;
+}
+ </style>
+</head>
+<body>
+ <div id="main">
+ <div id="spacer">
+ </div>
+ <div id="fixed">
+ <div id="long">
+ </div>
+ </div>
+ </div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ // Scroll to the bottom of the fixed position element that should not
+ // allow overscroll handoff.
+ fixed.scrollTop = fixed.scrollHeight;
+
+ // After scrolling to bottom tick the refresh driver.
+ await promiseFrame();
+
+ info("Start: fixed=" + fixed.scrollTop + " main=" + main.scrollTop);
+
+ // Async scroll the fixed element by 200 pixels using the mouse-wheel.
+ // This should not handoff the overscroll.
+ await promiseMoveMouseAndScrollWheelOver(fixed, 50, 50, false, 200);
+
+ // Make sure scrolling that has happened is propagated to the main thread.
+ await promiseApzFlushedRepaints();
+
+ // Try another gesture to ensure the overscroll handoff runs.
+ await promiseMoveMouseAndScrollWheelOver(fixed, 50, 50, false, 200);
+ await promiseApzFlushedRepaints();
+
+ info("After scroll: fixed=" + fixed.scrollTop + " main=" + main.scrollTop);
+
+ // Ensure that the main element has not scrolled.
+ is(main.scrollTop, 0, "The overscroll should not handoff");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-2.html b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-2.html
new file mode 100644
index 0000000000..29b11072ca
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-2.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<head>
+ <title>APZ overscroll handoff for fixed elements</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+<style>
+html, body {
+ margin: 0;
+}
+html {
+ overflow: auto;
+ background: blue;
+}
+.spacer {
+ height: 2000px;
+}
+#fixed {
+ position: fixed;
+ overflow: auto;
+ background: red;
+ width: 200px;
+ height: 200px;
+ top: 0;
+ left: 0;
+}
+</style>
+</head>
+<div id="fixed">
+ <div class="spacer"></div>
+</div>
+<div class="spacer"></div>
+<script type="application/javascript">
+
+async function test() {
+ // Scroll to the bottom of the fixed position element that should
+ // allow overscroll handoff.
+ fixed.scrollTop = fixed.scrollHeight;
+
+ // After scrolling to bottom tick the refresh driver.
+ await promiseFrame();
+
+ info("Start: fixed=" + fixed.scrollTop + " window=" + window.scrollY);
+
+ // Async scroll the fixed element by 200 pixels using the mouse-wheel.
+ // This should handoff the overscroll to the window.
+ await promiseMoveMouseAndScrollWheelOver(fixed, 50, 50, false, 200);
+
+ // Make sure scrolling that has happened is propagated to the main thread.
+ await promiseApzFlushedRepaints();
+
+ // Try another gesture to ensure the overscroll handoff runs.
+ await promiseMoveMouseAndScrollWheelOver(fixed, 50, 50, false, 200);
+ await promiseApzFlushedRepaints();
+
+ // Ensure that the window has scrolled.
+ isnot(window.scrollY, 0, "The overscroll should not handoff");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-3.html b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-3.html
new file mode 100644
index 0000000000..4a0687ba20
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-3.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<head>
+ <title>APZ overscroll handoff for fixed elements</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+<style>
+html, body {
+ margin: 0;
+}
+#scrolled {
+ overflow: auto;
+ background: blue;
+ width: 400px;
+ height: 400px;
+}
+.spacer {
+ height: 2000px;
+}
+#fixed {
+ position: fixed;
+ background: red;
+ top: 0;
+ left: 0;
+}
+#subframe {
+ overflow: auto;
+ width: 200px;
+ height: 200px;
+}
+</style>
+</head>
+<div id="scrolled">
+ <div id="fixed">
+ <div id="subframe">
+ <div id="firstspacer" class="spacer"></div>
+ </div>
+ </div>
+ <div id="secondspacer" class="spacer"></div>
+</div>
+<script type="application/javascript">
+
+async function test() {
+ // Scroll to the bottom of the fixed position element that should not
+ // allow overscroll handoff.
+ subframe.scrollTop = subframe.scrollHeight;
+
+ // After scrolling to bottom tick the refresh driver.
+ await promiseFrame();
+
+ info("Before scroll: subframe=" + subframe.scrollTop + " scrolled=" +
+ scrolled.scrollTop);
+
+ // Async scroll the fixed element by 200 pixels using the mouse-wheel.
+ // This should not handoff the overscroll.
+ await promiseMoveMouseAndScrollWheelOver(subframe, 50, 50, false, 200);
+
+ // Make sure scrolling that has happened is propagated to the main thread.
+ await promiseApzFlushedRepaints();
+
+ // Try another gesture to ensure the overscroll handoff runs.
+ await promiseMoveMouseAndScrollWheelOver(subframe, 50, 50, false, 200);
+ await promiseApzFlushedRepaints();
+
+ info("After scroll: subframe=" + subframe.scrollTop + " scrolled=" +
+ scrolled.scrollTop);
+
+ // Ensure that the scrolled element has not scrolled.
+ is(scrolled.scrollTop, 0, "scrolled: The overscroll should not handoff");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-4.html b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-4.html
new file mode 100644
index 0000000000..7394984ce3
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-4.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<head>
+ <title>APZ overscroll handoff for fixed elements</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+<style>
+html, body {
+ margin: 0;
+}
+#scrolled {
+ overflow: auto;
+ background: blue;
+ width: 400px;
+ height: 400px;
+}
+.spacer {
+ height: 2000px;
+}
+#fixed {
+ position: fixed;
+ background: red;
+ top: 0;
+ left: 0;
+}
+#subframe {
+ overflow: auto;
+ width: 200px;
+ height: 200px;
+}
+</style>
+</head>
+<div id="scrolled">
+ <div id="fixed">
+ <div>
+ <div id="subframe">
+ <div id="firstspacer" class="spacer"></div>
+ </div>
+ </div>
+ </div>
+ <div id="secondspacer" class="spacer"></div>
+</div>
+<script type="application/javascript">
+
+async function test() {
+ // Scroll to the bottom of the fixed position element that should not
+ // allow overscroll handoff.
+ subframe.scrollTop = subframe.scrollHeight;
+
+ // After scrolling to bottom tick the refresh driver.
+ await promiseFrame();
+
+ info("Before scroll: subframe=" + subframe.scrollTop + " scrolled=" +
+ scrolled.scrollTop);
+
+ // Async scroll the fixed element by 200 pixels using the mouse-wheel.
+ // This should not handoff the overscroll.
+ await promiseMoveMouseAndScrollWheelOver(subframe, 50, 50, false, 200);
+
+ // Make sure scrolling that has happened is propagated to the main thread.
+ await promiseApzFlushedRepaints();
+
+ // Try another gesture to ensure the overscroll handoff runs.
+ await promiseMoveMouseAndScrollWheelOver(subframe, 50, 50, false, 200);
+ await promiseApzFlushedRepaints();
+
+ info("After scroll: subframe=" + subframe.scrollTop + " scrolled=" +
+ scrolled.scrollTop);
+
+ // Ensure that the scrolled element has not scrolled.
+ is(scrolled.scrollTop, 0, "scrolled: The overscroll should not handoff");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-5.html b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-5.html
new file mode 100644
index 0000000000..3d62287c7c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-5.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>APZ overscroll handoff for fixed elements in a subdoc</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ iframe {
+ width: 400px;
+ height: 400px;
+ border: solid 2px black;
+ }
+ #rootcontent {
+ height: 200vh;
+ background: yellow;
+ }
+ </style>
+ </head>
+ <body>
+ <iframe id="subdoc" srcdoc="
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <style>
+ #fixed {
+ position: fixed;
+ top: 0;
+ height: 100px;
+ width: 80%;
+ overflow: scroll;
+ }
+ #fixed-content {
+ background: red;
+ }
+ #rootcontent {
+ background: green;
+ }
+ .spacer {
+ height: 200vh;
+ width: 100%;
+ }
+ </style>
+ </head>
+ <body>
+ <div id='fixed'>
+ <div id='fixed-content' class='spacer'></div>
+ </div>
+ <div id='rootcontent' class='spacer'></div>
+ </body>
+ </html>
+ "></iframe>
+ <div id="rootcontent"></div>
+ </body>
+ <script>
+async function test() {
+ // Scroll to the bottom of the fixed position element to ensure that the following
+ // scroll does trigger overscroll handoff to the subdoc root scrollable element.
+ subdoc.contentWindow.fixed.scrollTop = subdoc.contentWindow.fixed.scrollHeight;
+
+ // After scrolling to bottom tick the refresh driver.
+ await promiseFrame();
+
+ let firstTransformEnd = promiseTransformEnd();
+
+ info("start scroll #1");
+
+ // Async scroll the fixed element by 200 pixels using the mouse-wheel. This should
+ // handoff the overscroll to the subdoc's root scrollable element.
+ await promiseMoveMouseAndScrollWheelOver(subdoc.contentWindow.fixed, 50, 50, false, 200);
+
+ info("After scroll #1: fixed=" + subdoc.contentWindow.fixed.scrollTop +
+ " subdoc window=" + subdoc.contentWindow.scrollY + " window=" + window.scrollY);
+
+ info("wait scroll #1");
+ await firstTransformEnd;
+
+ // Do not attempt the second scroll if we did scroll the root document.
+ // A scroll in this case would likely cause the test to timeout. The assertions at the
+ // end of the test will catch this.
+
+ // If we triggered a scroll handoff to the _root_ document from the subframe, do not
+ // make another attempt at a second scroll. The test has already failed.
+ if (window.scrollY == 0) {
+ let secondTransformEnd = promiseTransformEnd();
+
+ info("start scroll #2");
+
+ await promiseMoveMouseAndScrollWheelOver(subdoc.contentWindow.fixed, 50, 50, false, 200);
+
+ info("After scroll #2: fixed=" + subdoc.contentWindow.fixed.scrollTop +
+ " subdoc window=" + subdoc.contentWindow.scrollY + " window=" + window.scrollY);
+
+ info("wait scroll #2");
+ await secondTransformEnd;
+ }
+
+ // Ensure that the main element has not scrolled and overscroll was handed off to
+ // the subdocument root scrollable element.
+ is(window.scrollY, 0, "The overscroll should not handoff to the root window");
+ isnot(subdoc.contentWindow.scrollY, 0,
+ "The overscroll should handoff to the subdocument's root scrollable element");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+ </script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_position_sticky_flicker.html b/gfx/layers/apz/test/mochitest/helper_position_sticky_flicker.html
new file mode 100644
index 0000000000..28495a7122
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_position_sticky_flicker.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<style>
+body {
+ margin: 0;
+}
+#sticky {
+ position: sticky;
+ top: 0;
+ height: 50vh;
+ background-color: green;
+}
+</style>
+<div id="sticky"></div>
+<!-- Content to make the page scrollable will be added dynamically -->
+<script>
+ // Silence SimpleTest warning about missing assertions by having it wait
+ // indefinitely. We don't need to give it an explicit finish because the
+ // entire window this test runs in will be closed after the main browser test
+ // finished.
+ SimpleTest.waitForExplicitFinish();
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_position_sticky_scroll_handoff.html b/gfx/layers/apz/test/mochitest/helper_position_sticky_scroll_handoff.html
new file mode 100644
index 0000000000..ae7815a2fc
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_position_sticky_scroll_handoff.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ overscroll handoff for sticky elements</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+html, body {
+ height: 100%;
+ overflow: hidden;
+ margin: 0;
+}
+
+#main {
+ height: 100%;
+ overflow: auto;
+}
+
+#spacer {
+ height: 5000px;
+}
+
+#sticky {
+ position: sticky;
+ top: 50%;
+ left: 0;
+ width: 100%;
+ height: 100px;
+ background: red;
+ overflow: auto;
+}
+
+#long {
+ height: 250px;
+ width: 50%;
+ position: absolute;
+ background: green;
+ top: 0;
+ left: 25%;
+}
+ </style>
+</head>
+<body>
+ <div id="main">
+ <div id="sticky">
+ <div id="long">
+ </div>
+ </div>
+ <div id="spacer">
+ </div>
+ </div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ // Scroll to the bottom of the sticky position element that should not
+ // allow overscroll handoff.
+ sticky.scrollTop = sticky.scrollHeight;
+
+ // After scrolling to bottom tick the refresh driver.
+ await promiseFrame();
+
+ info("Start: sticky=" + sticky.scrollTop + " main=" + main.scrollTop);
+
+ let transformEnd = promiseTransformEnd();
+
+ // Async scroll the sticky element by 200 pixels using the mouse-wheel.
+ // This should handoff the overscroll to the parent element.
+ await promiseMoveMouseAndScrollWheelOver(sticky, 25, 25, false, 200);
+
+ // Wait for the trasform triggered by the gesture to complete.
+ await transformEnd;
+ await promiseApzFlushedRepaints();
+
+ info("After scroll: sticky=" + sticky.scrollTop + " main=" + main.scrollTop);
+
+ // Ensure that the main element has scrolled.
+ isnot(main.scrollTop, 0, "The overscroll should handoff");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_programmatic_scroll_behavior.html b/gfx/layers/apz/test/mochitest/helper_programmatic_scroll_behavior.html
new file mode 100644
index 0000000000..721ce7e538
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_programmatic_scroll_behavior.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html, body { margin: 0; }
+
+ #big {
+ height: 250vh;
+ width: 100%;
+ }
+
+ #target {
+ height: 500px;
+ width: 100%;
+ background: red;
+ }
+ </style>
+</head>
+<body>
+ <div id="big">
+ </div>
+ <div id="target">
+ </div>
+</body>
+<script>
+const searchParams = new URLSearchParams(location.search);
+
+async function test() {
+ // Count the number of scroll events that occur. Instant scrolls should only
+ // trigger one scroll event, so a scroll event count of 1 indicates that a
+ // instant scroll was conducted.
+ let scrollCount = 0;
+ window.addEventListener("scroll", (e) => {
+ scrollCount += 1;
+ });
+
+ let scrollendPromise = promiseScrollend();
+
+ // Call the given programmatic scroll with behavior: smooth.
+ switch (searchParams.get("action")) {
+ case "scrollIntoView":
+ target.scrollIntoView({behavior: "smooth"});
+ break;
+ case "scrollBy":
+ document.scrollingElement.scrollBy({top: 500, behavior: "smooth"});
+ break;
+ case "scrollTo":
+ document.scrollingElement.scrollTo({top: 500, behavior: "smooth"});
+ break;
+ case "scroll":
+ document.scrollingElement.scroll({top: 500, behavior: "smooth"});
+ break;
+ default:
+ ok(false, "Unsupported action: " + searchParams.get("action"));
+ break;
+ }
+
+ await scrollendPromise;
+
+ // If general.smoothScroll is set, the behavior of the scroll should be
+ // "smooth". If general.smoothScroll is disabled, we should respect it and
+ // the scrolls should instant regardless of the specified behavior.
+ if (SpecialPowers.getBoolPref("general.smoothScroll")) {
+ info("final enabled scroll count: " + scrollCount);
+ ok(scrollCount > 1, "The programmatic scroll should create more than one scroll event");
+ } else {
+ info("final disabled scroll count: " + scrollCount);
+ ok(scrollCount == 1, "The programmatic scroll should be instant with one scroll event");
+ }
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_relative_scroll_smoothness.html b/gfx/layers/apz/test/mochitest/helper_relative_scroll_smoothness.html
new file mode 100644
index 0000000000..3f895c37d8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_relative_scroll_smoothness.html
@@ -0,0 +1,156 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/NativeKeyCodes.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<title>What happens if main thread scrolls?</title>
+<style>
+html, body { margin: 0; }
+
+html {
+ background:
+ repeating-linear-gradient(45deg, transparent 0, transparent 100px, rgba(0,0,0,0.1) 0, rgba(0,0,0,0.1) 200px),
+ repeating-linear-gradient(-45deg, transparent 0, transparent 100px, rgba(0,0,0,0.1) 0, rgba(0,0,0,0.1) 200px),
+ repeating-linear-gradient(to bottom, transparent 0, transparent 500px, rgba(0,0,0,0.4) 0, rgba(0,0,0,0.4) 1000px),
+ repeating-linear-gradient(to bottom, hsl(0, 60%, 80%), hsl(0, 60%, 80%) 200px, hsl(70, 60%, 80%) 0, hsl(70, 60%, 80%) 400px, hsl(140, 60%, 80%) 0, hsl(140, 60%, 80%) 600px, hsl(210, 60%, 80%) 0, hsl(210, 60%, 80%) 800px),
+ white;
+ background-size:
+ 283px 283px,
+ 283px 283px,
+ 100px 1000px,
+ 100px 800px;
+}
+
+body {
+ height: 10000px;
+}
+</style>
+
+<script>
+const searchParams = new URLSearchParams(location.search);
+let strict = searchParams.get("strict") == "true";
+
+var intervalId;
+// Start periodic content expansions after we get a scroll event triggered by
+// a key press in test() function below, otherwise we may have same scroll
+// offsets caused by this script before we start scrolling.
+window.addEventListener("scroll", () => {
+ var offset = 0;
+ var initialBodyHeight = 10000;
+ intervalId = setInterval(() => {
+ // "Add content" at the top. We do this by making the body longer and adjusting the background position.
+ offset += 10;
+ document.documentElement.style.backgroundPosition = `0px ${offset}px`;
+ document.body.style.height = `${initialBodyHeight + offset}px`;
+
+ switch (searchParams.get("scroll-method")) {
+ case "scrollBy":
+ window.scrollBy(0, 10);
+ break;
+ case "scrollTop":
+ document.scrollingElement.scrollTop += 10;
+ break;
+ case "scrollTo":
+ window.scrollTo(0, window.scrollY + 10);
+ break;
+ default:
+ ok(false, "Unsupported scroll method: " + searchParams.get("scroll-method"));
+ break;
+ }
+
+ // Simulate some jank.
+ var freezeDurationInMilliseconds = 100;
+ var startTime = Date.now();
+ while (Date.now() - startTime < freezeDurationInMilliseconds) {} // eslint-disable-line no-empty
+ }, 300);
+}, { once: true });
+
+
+async function test() {
+ // Once this content starts scrolling, it triggers a 100ms jank every 300ms so
+ // sending arrow down keys for 1500ms will cause some jank.
+ const TEST_DURATION_MS = 1500;
+ const timeAtStart = performance.now();
+ while (performance.now() - timeAtStart < TEST_DURATION_MS) {
+ switch (searchParams.get("input-type")) {
+ case "key":
+ synthesizeKey("KEY_ArrowDown");
+ break;
+ case "native-key":
+ const DownArrowKeyCode = nativeArrowDownKey();
+ ok(synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US,
+ DownArrowKeyCode, {} /* no modifier */,
+ "", ""),
+ "Dispatched an down arrow key event");
+ break;
+ case "wheel":
+ await synthesizeNativeWheel(window, 50, 50, 0, -50);
+ break;
+ default:
+ ok(false, "Unsupported input type: " + searchParams.get("input-type"));
+ break;
+ }
+ await promiseFrame(window);
+ }
+
+ // Stop the periodic expansions.
+ clearInterval(intervalId);
+
+ const records = collectSampledScrollOffsets(document.scrollingElement);
+
+ let previousRecord = { scrollOffsetY: 0, sampledTimeStamp: 0 };
+ let scrollStartTime = null;
+ for (const record of records) {
+ // Ignore offsets before scrolling.
+ if (record.scrollOffsetY == 0) {
+ continue;
+ }
+ // Ignore offsets after TEST_DURATION_MS has elapsed since the
+ // start of scrolling. Note that the sampled timestamps are
+ // in microseconds.
+ if (!scrollStartTime) {
+ scrollStartTime = record.sampledTimeStamp;
+ } else if (((record.sampledTimeStamp - scrollStartTime) / 1000) > TEST_DURATION_MS) {
+ break;
+ }
+ ok(
+ strict
+ ? (record.scrollOffsetY > previousRecord.scrollOffsetY)
+ : (record.scrollOffsetY >= previousRecord.scrollOffsetY),
+ "scroll offset should be " +
+ (strict ? "strictly monotonically increasing " : "nondecreasing ") +
+ "previous offset: " + previousRecord.scrollOffsetY +
+ ", offset: " + record.scrollOffsetY
+ );
+ ok(
+ record.sampledTimeStamp > previousRecord.sampledTimeStamp,
+ "sampled time stamp should be strictly monotonically increasing " +
+ "previous timestamp: " + previousRecord.sampledTimeStamp +
+ ", timestamp: " + record.sampledTimeStamp
+ );
+ previousRecord = record;
+ }
+}
+
+function isOnChaosMode() {
+ return SpecialPowers.Services.env.get("MOZ_CHAOSMODE");
+}
+
+if (searchParams.get("input-type") == "native-key" &&
+ getPlatform() != "mac" && getPlatform() != "windows") {
+ ok(true, "Skipping test because native key events are not supported on " +
+ getPlatform());
+ subtestDone();
+} else if (searchParams.get("input-type") == "native-key" &&
+ getPlatform() == "mac" && isOnChaosMode()) {
+ ok(true, "Skipping native-key tests on verify runs on Mac");
+ subtestDone();
+} else {
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+}
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_reset_zoom_bug1818967.html b/gfx/layers/apz/test/mochitest/helper_reset_zoom_bug1818967.html
new file mode 100644
index 0000000000..4a4d7e34ca
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_reset_zoom_bug1818967.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html lang="en"><head>
+<meta http-equiv="content-type" content="text/html; charset=UTF-8">
+<meta charset="utf-8">
+<title>Test that we do not checkerboard after resetting the pinch-zoom scale</title>
+<script type="application/javascript" src="apz_test_utils.js"></script>
+<script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+.scrolled {
+ width: 10000px;
+ height: 10000px;
+ background: linear-gradient(lime, cyan);
+}
+</style>
+</head><body>
+ <div class="scrolled"></div>
+</body>
+
+<script type="application/javascript">
+async function test() {
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ var scrollerId = utils.getViewId(document.documentElement);
+
+ // Zoom in to the maximum level
+ utils.setResolutionAndScaleTo(10.0);
+ await promiseApzFlushedRepaints();
+
+ // Scroll the layout viewport to around middle of the page
+ window.scrollTo(window.scrollMaxX / 2, window.scrollMaxY / 2);
+ await promiseApzFlushedRepaints();
+
+ // Scroll the visual viewport to the bottom of the layout viewport.
+ // This creates an offset between the visual and layout viewport
+ // offsets which is needed to trigger the bug.
+ utils.scrollToVisual(window.scrollX,
+ window.scrollY + (0.9 * document.documentElement.clientHeight),
+ utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_INSTANT);
+ await promiseApzFlushedRepaints();
+
+ // Reset the zoom level to 1.0x
+ utils.setResolutionAndScaleTo(1.0);
+ await promiseApzFlushedRepaints();
+
+ // Assert that we're not checkerboarded
+ assertNotCheckerboarded(utils, scrollerId, "After resetting zoom level");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_on_wheel.html b/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_on_wheel.html
new file mode 100644
index 0000000000..9f439a8ce3
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_on_wheel.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, minimum-scale=1.0">
+<title>Tests scroll anchoring updates in-progress wheel scrolling __relatively__</title>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+ body { margin: 0 }
+ #target > div {
+ height: 500px;
+ }
+</style>
+<div id="target"></div>
+<div class="spacer" style="height: 1000vh"></div>
+<script>
+ const targetElement = document.getElementById("target");
+
+ async function test() {
+ // Fistly send a wheel event to measure the scroll amount of one event.
+ let transformEndPromise = promiseTransformEnd();
+ await synthesizeNativeWheel(window, 50, 50, 0, -50);
+ await transformEndPromise;
+
+ ok(window.scrollY > 0, "Should be scrolled to some amount");
+
+ const firstScrollAmount = window.scrollY;
+
+ // Now send a wheel event again.
+ transformEndPromise = promiseTransformEnd();
+ await synthesizeNativeWheel(window, 50, 50, 0, -50);
+ await promiseApzFlushedRepaints();
+
+ // And insert an element during the wheel scrolling is still in progress.
+ targetElement.appendChild(document.createElement("div"));
+
+ await transformEndPromise;
+
+ // Give scroll offsets a chance to sync.
+ await promiseApzFlushedRepaints();
+
+ // Though in an ideal environment, the expected total scroll amount should
+ // be `firstScrollAmount * 2 + 500`, we don't expect it on our CIs, so we
+ // assume here;
+ // 1) it's greater than double of the first scroll amount since it should
+ // include the height of the inserted element.
+ // 2) it's greater than the first scroll amount + the height of the inserted
+ // element.
+ ok(window.scrollY > firstScrollAmount * 2,
+ `the scroll amount ${window.scrollY} should be greater than double of the first scroll amount ${firstScrollAmount*2}`);
+ ok(window.scrollY > firstScrollAmount + 500,
+ `the scroll amount ${window.scrollY} should also be greater than the first scroll amount + the inserted element height ${firstScrollAmount+500}`);
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll.html b/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll.html
new file mode 100644
index 0000000000..7bdfa83f24
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, minimum-scale=1.0">
+<title>Tests scroll anchoring interaction with smooth visual scrolling.</title>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+ body { margin: 0 }
+ #target > div {
+ height: 500px;
+ }
+</style>
+<div id="target"></div>
+<div class="spacer" style="height: 200vh"></div>
+<script>
+ const utils = SpecialPowers.DOMWindowUtils;
+ const targetElement = document.getElementById("target");
+
+ async function test() {
+ const destY = window.scrollMaxY;
+ ok(destY > 0, "Should have some scroll range");
+
+ // Scroll to the bottom of the page.
+ window.scrollTo(0, destY);
+
+ is(window.scrollY, window.scrollMaxY, "Should be at the bottom");
+
+ // Register a TransformEnd observer so we can tell when the smooth scroll
+ // animation stops.
+ let transformEndPromise = promiseTransformEnd();
+
+ // Trigger smooth scrolling, and quickly insert an element which takes
+ // space into the DOM.
+ //
+ // It is important that it actually takes space so as to trigger scroll
+ // anchoring.
+ targetElement.scrollIntoView({ behavior: "smooth" });
+ targetElement.appendChild(document.createElement("div"));
+
+ // Wait for the TransformEnd.
+ await transformEndPromise;
+
+ // Give scroll offsets a chance to sync.
+ await promiseApzFlushedRepaints();
+
+ // Check that the async smooth scroll finished.
+ is(window.scrollY, 0, "Should've completed the smooth scroll without getting interrupted by scroll anchoring");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll_with_set_timeout.html b/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll_with_set_timeout.html
new file mode 100644
index 0000000000..07ba816c36
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll_with_set_timeout.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, minimum-scale=1.0">
+<title>Tests scroll anchoring interaction with smooth visual scrolling with set timeout.</title>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+ body { margin: 0 }
+ #target > div {
+ height: 500px;
+ }
+</style>
+<div id="target"></div>
+<div class="spacer" style="height: 200vh"></div>
+<script>
+ const utils = SpecialPowers.DOMWindowUtils;
+ const targetElement = document.getElementById("target");
+
+ async function test() {
+ const destY = window.scrollMaxY;
+ ok(destY > 0, "Should have some scroll range");
+
+ // Scroll to the bottom of the page.
+ window.scrollTo(0, destY);
+
+ is(window.scrollY, window.scrollMaxY, "Should be at the bottom");
+
+ // Register a TransformEnd observer so we can tell when the smooth scroll
+ // animation stops.
+ let transformEndPromise = promiseTransformEnd();
+
+ // Trigger smooth scrolling, and insert an element which takes space into
+ // the DOM in a 20ms setTimeout callback.
+ //
+ // It is important that it actually takes space so as to trigger scroll
+ // anchoring.
+ targetElement.scrollIntoView({ behavior: "smooth" });
+ setTimeout(() => {
+ targetElement.appendChild(document.createElement("div"));
+ }, 20);
+
+ // Wait for the TransformEnd.
+ await transformEndPromise;
+
+ // Give scroll offsets a chance to sync.
+ await promiseApzFlushedRepaints();
+
+ // Check that the async smooth scroll finished.
+ is(window.scrollY, 0, "Should've completed the smooth scroll without getting interrupted by scroll anchoring");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_inactive_perspective.html b/gfx/layers/apz/test/mochitest/helper_scroll_inactive_perspective.html
new file mode 100644
index 0000000000..727d0e4fd1
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_inactive_perspective.html
@@ -0,0 +1,45 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Wheel-scrolling over inactive subframe with perspective</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var subframe = document.getElementById("scroll");
+
+ // scroll over the middle of the subframe, to make sure it scrolls,
+ // not the page
+ var scrollPos = subframe.scrollTop;
+ await promiseMoveMouseAndScrollWheelOver(subframe, 100, 100);
+ dump("after scroll, subframe.scrollTop = " + subframe.scrollTop + "\n");
+ ok(subframe.scrollTop > scrollPos, "subframe scrolled after wheeling over it");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ #scroll {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+ perspective: 400px;
+ }
+ #scrolled {
+ width: 200px;
+ height: 1000px; /* so the subframe has room to scroll */
+ background: linear-gradient(red, blue); /* so you can see it scroll */
+ transform: translateZ(0px); /* so the perspective makes it to the display list */
+ }
+ </style>
+</head>
+<body>
+ <div id="scroll">
+ <div id="scrolled"></div>
+ </div>
+ <div style="height: 5000px;"></div><!-- So the page is scrollable as well -->
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_inactive_zindex.html b/gfx/layers/apz/test/mochitest/helper_scroll_inactive_zindex.html
new file mode 100644
index 0000000000..44c3cf3217
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_inactive_zindex.html
@@ -0,0 +1,46 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Wheel-scrolling over inactive subframe with z-index</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var subframe = document.getElementById("scroll");
+
+ // scroll over the middle of the subframe, and make sure that it scrolls,
+ // not the page
+ var scrollPos = subframe.scrollTop;
+ await promiseMoveMouseAndScrollWheelOver(subframe, 100, 100);
+ dump("after scroll, subframe.scrollTop = " + subframe.scrollTop + "\n");
+ ok(subframe.scrollTop > scrollPos, "subframe scrolled after wheeling over it");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ #scroll {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+ }
+ #scrolled {
+ width: 200px;
+ height: 1000px; /* so the subframe has room to scroll */
+ z-index: 2;
+ background: linear-gradient(red, blue); /* so you can see it scroll */
+ transform: translateZ(0px); /* to force active layers */
+ will-change: transform; /* to force active layers */
+ }
+ </style>
+</head>
+<body>
+ <div id="scroll">
+ <div id="scrolled"></div>
+ </div>
+ <div style="height: 5000px;"></div><!-- So the page is scrollable as well -->
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1516056.html b/gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1516056.html
new file mode 100644
index 0000000000..99952dddd4
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1516056.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Test for bug 1516056: "scroll into view" respects bounds on layout scroll position</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ #target {
+ width: 100px;
+ height: 100px;
+ margin-left: 50%;
+ margin-right: 50%;
+ background: cyan;
+ }
+ </style>
+</head>
+<body>
+ <div id="target"></div>
+ <script>
+ let vv = window.visualViewport;
+ function getVisualScrollRange() {
+ let rootScroller = document.scrollingElement;
+ return {
+ width: rootScroller.scrollWidth - vv.width,
+ height: rootScroller.scrollHeight - vv.height,
+ };
+ }
+ async function test() {
+ is(window.scrollMaxX, 0, "page should have a zero horizontal layout scroll range");
+ is(window.scrollMaxY, 0, "page should have a zero vertical layout scroll range");
+ let visualScrollRange = getVisualScrollRange();
+ ok(visualScrollRange.width > 0, "page should have a nonzero horizontal visual scroll range");
+ ok(visualScrollRange.height > 0, "page should have a nonzero vertical visual scroll range");
+ let target = document.getElementById("target");
+
+ // Scroll target element into view. Wait until any visual scrolling is done before doing checks.
+ let scrollPromise = new Promise(resolve => {
+ vv.addEventListener("scroll", resolve, { once: true });
+ });
+ target.scrollIntoView();
+ await scrollPromise; // wait for visual viewport "scroll" event
+ await promiseApzFlushedRepaints();
+
+ // Test that scrollIntoView() respected the layout scroll range.
+ is(window.scrollX, 0, "page should not layout-scroll with a zero layout scroll range");
+ is(window.scrollY, 0, "page should not layout-scroll with a zero layout scroll range");
+
+ // Test that scrollIntoView() did perform visual scrolling.
+ let vvRect = getVisualViewportRect(vv);
+ let targetBounds = target.getBoundingClientRect();
+ assertRectContainment(vvRect, "visual viewport", targetBounds, "target element bounding rect");
+ }
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1562757.html b/gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1562757.html
new file mode 100644
index 0000000000..4aff2901d7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1562757.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Test for bug 1562757: "scroll into view" in iframe respects bounds on layout scroll position</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ #iframe {
+ width: 100px;
+ height: 100px;
+ margin-left: 50%;
+ margin-right: 50%;
+ background: cyan;
+ display: block;
+ }
+ </style>
+</head>
+<body>
+ <iframe id="iframe" scrolling="no" frameborder="no" srcdoc="<div id='target' style='width:100px;height:100px;'></div>"></iframe>
+
+ <script>
+ let vv = window.visualViewport;
+ function getVisualScrollRange() {
+ let rootScroller = document.scrollingElement;
+ return {
+ width: rootScroller.scrollWidth - vv.width,
+ height: rootScroller.scrollHeight - vv.height,
+ };
+ }
+ async function test() {
+ is(window.scrollMaxX, 0, "page should have a zero horizontal layout scroll range");
+ is(window.scrollMaxY, 0, "page should have a zero vertical layout scroll range");
+ let visualScrollRange = getVisualScrollRange();
+ ok(visualScrollRange.width > 0, "page should have a nonzero horizontal visual scroll range");
+ ok(visualScrollRange.height > 0, "page should have a nonzero vertical visual scroll range");
+ let target = iframe.contentDocument.getElementById("target");
+
+ // Scroll target element into view. Wait until any visual scrolling is done before doing checks.
+ let scrollPromise = new Promise(resolve => {
+ vv.addEventListener("scroll", resolve, { once: true });
+ });
+ target.scrollIntoView();
+ await scrollPromise; // wait for visual viewport "scroll" event
+ await promiseApzFlushedRepaints();
+
+ // Test that scrollIntoView() respected the layout scroll range.
+ is(window.scrollX, 0, "page should not layout-scroll with a zero layout scroll range");
+ is(window.scrollY, 0, "page should not layout-scroll with a zero layout scroll range");
+
+ // Test that scrollIntoView() did perform visual scrolling.
+ let vvRect = getVisualViewportRect(vv);
+ let targetBounds = iframe.getBoundingClientRect();
+ assertRectContainment(vvRect, "visual viewport", targetBounds, "iframe having the target element bounding rect");
+ }
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_by_wheel.html b/gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_by_wheel.html
new file mode 100644
index 0000000000..f9303253b8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_by_wheel.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<title>A scroll linked effect scrolled by wheel events</title>
+<style>
+html, body { margin: 0; }
+body {
+ height: 1000vh;
+}
+#target {
+ position: absolute;
+ height: 800px;
+ background-color: #cc00cc;
+ top: 0;
+ left: 0;
+ right: 0;
+}
+</style>
+<div id="target"></div>
+<script>
+// Set up a scroll linked effect element.
+window.addEventListener("scroll", () => {
+ target.style.top = window.scrollY + "px";
+});
+
+async function test() {
+ let rect = rectRelativeToScreen(target);
+ // We only care the top 10px here since the scroll linked effect on the
+ // main-thread can't keep up with the async scrolling by wheel events so that
+ // the position absolute element is often partially scrolled out.
+ rect.height = 10.0;
+ const initialSnapshot = await getSnapshot(rect);
+
+ let snapshots = [];
+ for (let i = 0; i < 10; i++) {
+ await synthesizeNativeWheel(window, 50, 50, 0, -10);
+ snapshots.push(await getSnapshot(rect));
+ }
+
+ const sampledData = collectSampledScrollOffsets(document.scrollingElement);
+ const hasPoint5FractionalOffset = sampledData.some(data => {
+ return SpecialPowers.wrap(data).scrollOffsetY.toString().split(".")?.[1] === "5"
+ });
+
+ if (hasPoint5FractionalOffset) {
+ todo(false, "Bug 1752789: There's at least one sampled scroll offset " +
+ "having .5 fractional part in such cases scroll linked effects can " +
+ "not be rendered at the same position of the async scroll offset");
+ return;
+ }
+
+ snapshots.forEach(snapshot => {
+ is(initialSnapshot, snapshot);
+ });
+}
+
+pushPrefs([["apz.test.logging_enabled", true]])
+.then(waitUntilApzStable)
+.then(test)
+.then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_detector.html b/gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_detector.html
new file mode 100644
index 0000000000..aaa4b43829
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_detector.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<title>ScrollLinkedEffectDetector tests</title>
+<style>
+html, body { margin: 0; }
+body {
+ height: 1000vh;
+}
+#target {
+ position: absolute;
+ height: 800px;
+ background-color: #cc00cc;
+ top: 0;
+ left: 0;
+ right: 0;
+}
+</style>
+<div id="target"></div>
+<script>
+async function test() {
+ let eventTimeStamp;
+ // Utility function to synthesize a scroll and call the given function
+ // in the event listener.
+ async function promiseScrollAndEvent(fn) {
+ let scrollEventPromise = new Promise(resolve => {
+ window.addEventListener("scroll", () => {
+ fn();
+ resolve();
+ }, { once: true });
+ });
+ await scrollEventPromise;
+ // Wait a rAF to make sure we are outside of the micro tasks for the scroll
+ // event callback so that we can ensure our stack based
+ // ScrollLinkedEffectDetector has been scoped out from the function firing
+ // scroll events.
+ await promiseFrame();
+ }
+
+ let intervalId = setInterval(async () => {
+ await synthesizeNativeWheel(window, 50, 50, 0, -10);
+ }, 0);
+ await promiseScrollAndEvent(() => {
+ eventTimeStamp = document.timeline.currentTime;
+ });
+ is(eventTimeStamp, document.timeline.currentTime,
+ `We are in same time frame where we got a scroll event at ${eventTimeStamp}`);
+ ok(!SpecialPowers.DOMWindowUtils.hasScrollLinkedEffect,
+ "No scroll-linked effect found yet");
+
+ // Setup a scroll-linked effect callback.
+ await promiseScrollAndEvent(() => {
+ isnot(window.scrollY, 0, "we've already scrolled some amount");
+ target.style.top = window.scrollY + "px";
+ eventTimeStamp = document.timeline.currentTime;
+ });
+ is(eventTimeStamp, document.timeline.currentTime,
+ `We are in same time frame where we got a scroll event at ${eventTimeStamp}`);
+ ok(SpecialPowers.DOMWindowUtils.hasScrollLinkedEffect,
+ "Scroll-linked effect found");
+
+ // A no-op again.
+ await promiseScrollAndEvent(() => {
+ eventTimeStamp = document.timeline.currentTime;
+ });
+ is(eventTimeStamp, document.timeline.currentTime,
+ `We are in same time frame where we got a scroll event at ${eventTimeStamp}`);
+ ok(!SpecialPowers.DOMWindowUtils.hasScrollLinkedEffect,
+ "No scroll-linked effect found");
+
+ // Setup a non-effective scroll-linked effect callback.
+ await promiseScrollAndEvent(() => {
+ target.style.top = getComputedStyle(target).top;
+ eventTimeStamp = document.timeline.currentTime;
+ });
+ is(eventTimeStamp, document.timeline.currentTime,
+ `We are in same time frame where we got a scroll event at ${eventTimeStamp}`);
+ ok(!SpecialPowers.DOMWindowUtils.hasScrollLinkedEffect,
+ "No scroll-linked effect found");
+
+ // Setup a callback to remove the style.
+ await promiseScrollAndEvent(() => {
+ target.style.top = "";
+ eventTimeStamp = document.timeline.currentTime;
+ });
+ is(eventTimeStamp, document.timeline.currentTime,
+ `We are in same time frame where we got a scroll event at ${eventTimeStamp}`);
+ ok(SpecialPowers.DOMWindowUtils.hasScrollLinkedEffect,
+ "Scroll-linked effect found");
+
+ // Setup a no-op callback.
+ await promiseScrollAndEvent(() => {
+ eventTimeStamp = document.timeline.currentTime;
+ });
+ is(eventTimeStamp, document.timeline.currentTime,
+ `We are in same time frame where we got a scroll event at ${eventTimeStamp}`);
+ ok(!SpecialPowers.DOMWindowUtils.hasScrollLinkedEffect,
+ "No scroll-linked effect found this time");
+ clearInterval(intervalId);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_on_position_fixed.html b/gfx/layers/apz/test/mochitest/helper_scroll_on_position_fixed.html
new file mode 100644
index 0000000000..5fbbc1437f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_on_position_fixed.html
@@ -0,0 +1,60 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Wheel-scrolling over position:fixed and position:sticky elements, in the top-level document as well as iframes</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var iframeWin = document.getElementById("iframe").contentWindow;
+
+ // scroll over the middle of the iframe's position:sticky element, check
+ // that it scrolls the iframe
+ var scrollPos = iframeWin.scrollY;
+ await promiseMoveMouseAndScrollWheelOver(iframeWin.document.body, 50, 150);
+ ok(iframeWin.scrollY > scrollPos, "iframe scrolled after wheeling over the position:sticky element");
+
+ // same, but using the iframe's position:fixed element
+ scrollPos = iframeWin.scrollY;
+ await promiseMoveMouseAndScrollWheelOver(iframeWin.document.body, 250, 150);
+ ok(iframeWin.scrollY > scrollPos, "iframe scrolled after wheeling over the position:fixed element");
+
+ // same, but scrolling the scrollable frame *inside* the position:fixed item
+ var fpos = document.getElementById("fpos_scrollable");
+ scrollPos = fpos.scrollTop;
+ await promiseMoveMouseAndScrollWheelOver(fpos, 50, 150);
+ ok(fpos.scrollTop > scrollPos, "scrollable item inside fixed-pos element scrolled");
+ // wait for it to layerize fully and then try again
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ scrollPos = fpos.scrollTop;
+ await promiseMoveMouseAndScrollWheelOver(fpos, 50, 150);
+ ok(fpos.scrollTop > scrollPos, "scrollable item inside fixed-pos element scrolled after layerization");
+
+ // same, but using the top-level window's position:sticky element
+ scrollPos = window.scrollY;
+ await promiseMoveMouseAndScrollWheelOver(document.body, 50, 150);
+ ok(window.scrollY > scrollPos, "top-level document scrolled after wheeling over the position:sticky element");
+
+ // same, but using the top-level window's position:fixed element
+ scrollPos = window.scrollY;
+ await promiseMoveMouseAndScrollWheelOver(document.body, 250, 150);
+ ok(window.scrollY > scrollPos, "top-level document scrolled after wheeling over the position:fixed element");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body style="height:5000px; margin:0">
+ <div style="position:sticky; width: 100px; height: 300px; top: 0; background-color:red">sticky</div>
+ <div style="position:fixed; width: 100px; height: 300px; top: 0; left: 200px; background-color: green">fixed</div>
+ <iframe id='iframe' width="300" height="400" srcdoc="<body style='height:5000px; margin:0'><div style='position:sticky; width:100px; height:300px; top: 0; background-color:red'>sticky</div><div style='position:fixed; right:0; top: 0; width:100px; height:300px; background-color:green'>fixed</div>"></iframe>
+
+ <div id="fpos_scrollable" style="position:fixed; width: 100px; height: 300px; top: 0; left: 400px; background-color: red; overflow:scroll">
+ <div style="background-color: blue; height: 1000px; margin: 3px">scrollable content inside a fixed-pos item</div>
+ </div>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_over_scrollbar.html b/gfx/layers/apz/test/mochitest/helper_scroll_over_scrollbar.html
new file mode 100644
index 0000000000..b7a8698cf8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_over_scrollbar.html
@@ -0,0 +1,48 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Wheel-scrolling over scrollbar</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var subframe = document.getElementById("scroll");
+
+ // scroll over the scrollbar, and make sure the subframe scrolls
+ var scrollPos = subframe.scrollTop;
+ if (subframe.clientWidth == 200) {
+ // No scrollbar, abort the test. This can happen e.g. on local macOS runs
+ // with OS settings to only show scrollbars on trackpad/mouse activity.
+ ok(false, "No scrollbars found, cannot run this test!");
+ return;
+ }
+ var scrollbarX = (200 + subframe.clientWidth) / 2;
+ await promiseNativeWheelAndWaitForScrollEvent(subframe, scrollbarX, 100,
+ 0, -10);
+ ok(subframe.scrollTop > scrollPos, "subframe scrolled after wheeling over scrollbar");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ #scroll {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+ }
+ #scrolled {
+ width: 200px;
+ height: 1000px; /* so the subframe has room to scroll */
+ will-change: transform; /* to force active layers */
+ }
+ </style>
+</head>
+<body>
+ <div id="scroll">
+ <div id="scrolled"></div>
+ </div>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_snap_no_valid_snap_position.html b/gfx/layers/apz/test/mochitest/helper_scroll_snap_no_valid_snap_position.html
new file mode 100644
index 0000000000..00f5e7d344
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_snap_no_valid_snap_position.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>No snapping occurs if there is no valid snap position</title>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ div {
+ position: absolute;
+ }
+ #scroller {
+ width: 100%;
+ height: 500px;
+ overflow-y: scroll;
+ scroll-snap-type: y mandatory;
+ }
+ .child {
+ width: 100%;
+ height: 100px;
+ background-color: blue;
+ }
+ </style>
+</head>
+<body>
+ <div id="scroller">
+ <div class="child" style="top: 0px;"></div>
+ <div style="width: 100%; height: 2000px;"></div>
+ <div class="child" style="top: 1000px;"></div>
+ </div>
+ <script type="application/javascript">
+ async function test() {
+ await promiseMoveMouseAndScrollWheelOver(scroller, 100, 100);
+
+ ok(scroller.scrollTop > 0, "Scroll should happen some amount");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_panning.html b/gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_panning.html
new file mode 100644
index 0000000000..f28a2f9396
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_panning.html
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Skip re-snapping during pan gesture</title>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ margin: 0;
+ }
+ div {
+ position: absolute;
+ }
+ #scroller {
+ width: 100%;
+ height: 500px;
+ overflow-y: scroll;
+ scroll-snap-type: y mandatory;
+ }
+ .child {
+ width: 100%;
+ height: 100px;
+ background-color: blue;
+ scroll-snap-align: start;
+ }
+ </style>
+</head>
+<body>
+ <div id="scroller">
+ <div class="child" style="top: 0px;"></div>
+ <div id="spacer" style="width: 100%; height: 2000px;"></div>
+ </div>
+ <script type="application/javascript">
+ async function test() {
+ is(scroller.scrollTop, 0, "The initial snap point is at 0px");
+
+ let scrollEventPromise = waitForScrollEvent(scroller);
+
+ // Start a pan gesture downward.
+ await promiseNativePanGestureEventAndWaitForObserver(
+ scroller,
+ 100,
+ 100,
+ 0,
+ -10,
+ 1 /* kCGScrollPhaseBegan */);
+
+ await scrollEventPromise;
+
+ ok(scroller.scrollTop > 0, "The pan-start should scroll");
+
+ // Expand the scrollable region during the panning.
+ spacer.style.height = "2200px";
+
+ isnot(scroller.scrollTop, 0, "Do not re-snap to the original 0px");
+ let previousScrollPosition = scroller.scrollTop;
+
+ scrollEventPromise = waitForScrollEvent(scroller);
+ // Finish the pan gesture now.
+ await promiseNativePanGestureEventAndWaitForObserver(
+ scroller,
+ 100,
+ 100,
+ 0,
+ 0 /* 0 velocity to avoid further scrolling by this event */,
+ 4 /* kCGScrollPhaseEnd */);
+
+ await scrollEventPromise;
+
+ // Make sure the new scroll positions have reached to the main-thread.
+ await promiseApzFlushedRepaints();
+
+ // There's no good way to tell whether the snapping has finished, has
+ // reached to the snap destination, so we just check whether the current
+ // scroll position is a bit scrolled back toward the snap destination.
+ ok(scroller.scrollTop < previousScrollPosition,
+ "The pan-end should trigger snapping toward 0px");
+ }
+
+ if (getPlatform() == "mac") {
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ } else {
+ ok(true, "Skipping test because this test works only on mac");
+ subtestDone();
+ }
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_scrollbar_dragging.html b/gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_scrollbar_dragging.html
new file mode 100644
index 0000000000..ca2be3916f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_scrollbar_dragging.html
@@ -0,0 +1,105 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Skip re-snapping during scrollbar dragging</title>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ margin: 0;
+ }
+ div {
+ position: absolute;
+ }
+ #scroller {
+ width: 100%;
+ height: 500px;
+ overflow-y: scroll;
+ scroll-snap-type: y mandatory;
+ }
+ .child {
+ width: 100%;
+ height: 100px;
+ background-color: blue;
+ scroll-snap-align: start;
+ }
+ </style>
+</head>
+<body>
+ <div id="scroller">
+ <div class="child" style="top: 0px;"></div>
+ <div id="spacer" style="width: 100%; height: 2000px;"></div>
+ </div>
+ <script type="application/javascript">
+ async function test() {
+ is(scroller.scrollTop, 0, "The initial snap point is at 0px");
+
+ // Move onto the scroll thumb for the scroller element.
+ let w = {}, h = {};
+ SpecialPowers.DOMWindowUtils.getScrollbarSizes(scroller, w, h);
+ if (w.value == 0) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ const x = scroller.clientWidth + w.value / 2;
+ await promiseNativeMouseEventWithAPZ({
+ target: scroller,
+ offsetX: x,
+ offsetY: w.value + 5,
+ type: "mousemove"});
+
+ // Start dragging the thumb.
+ await promiseNativeMouseEventWithAPZ({
+ target: scroller,
+ offsetX: x,
+ offsetY: w.value + 5,
+ type: "mousedown",
+ });
+
+ // Move down the thumb to scroll.
+ let scrollEventPromise = waitForScrollEvent(scroller);
+ await promiseNativeMouseEventWithAPZ({
+ target: scroller,
+ offsetX: x,
+ offsetY: w.value + 10,
+ type: "mousemove"});
+ await scrollEventPromise;
+
+ ok(scroller.scrollTop > 0, "Dragging the scroll thumb should scroll");
+
+ // Expand the scrollable region during the dragging.
+ spacer.style.height = "2200px";
+
+ isnot(scroller.scrollTop, 0, "Do not re-snap to the original 0px");
+ let previousScrollPosition = scroller.scrollTop;
+
+ scrollEventPromise = waitForScrollEvent(scroller);
+ // Release the mouse button, it will trigger snapping.
+ await promiseNativeMouseEventWithAPZ({
+ target: scroller,
+ offsetX: x,
+ offsetY: 10,
+ type: "mouseup"});
+
+ await scrollEventPromise;
+
+ // Make sure the new scroll positions have reached to the main-thread.
+ await promiseApzFlushedRepaints();
+
+ // There's no good way to tell whether the snapping has finished, has
+ // reached to the snap destination, so we just check whether the current
+ // scroll position is a bit scrolled back toward the snap destination.
+ ok(scroller.scrollTop < previousScrollPosition,
+ "Releasing the mouse button should trigger snapping toward 0px");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_snap_on_page_down_scroll.html b/gfx/layers/apz/test/mochitest/helper_scroll_snap_on_page_down_scroll.html
new file mode 100644
index 0000000000..bca89aac42
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_snap_on_page_down_scroll.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Page scroll snaps a snap point in the same page rather than the one in the next page</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ div {
+ position: absolute;
+ margin: 0px;
+ }
+ body {
+ margin: 0px;
+ }
+ html {
+ overflow-y: scroll;
+ scroll-snap-type: y mandatory;
+ }
+ .snap {
+ scroll-snap-align: start;
+ background: green;
+ }
+ </style>
+</head>
+<body>
+ <div class="snap" style="top: 0vh; width: 100%; height: 20px;">1</div>
+ <div class="snap" style="top: 50vh; width: 100%; height: 20px;">2</div>
+ <div class="snap" style="top: 102vh; width: 100%; height: 20px;">3</div>
+ <div class="snap" style="top: 300vh; width: 100%; height: 20px;">4</div>
+ <div class="snap" style="top: 400vh; width: 100%; height: 20px;">5</div>
+
+ <script type="application/javascript">
+ async function test() {
+ const expectedPosition =
+ document.querySelectorAll(".snap")[1].getBoundingClientRect().top;
+
+ const keyPromise = promiseOneEvent(window, "keydown", null);
+ window.synthesizeKey("KEY_PageDown");
+ await keyPromise;
+
+ let startedScroll = false;
+ let sameScrollPosition = false;
+ let prevScrollPos = window.scrollY;
+ while (true) {
+ // Flush APZ repaints to ensure that scroll offset changes from
+ // a compositor sample reach the content process.
+ await promiseApzFlushedRepaints();
+
+ let scrollPos = window.scrollY;
+ if (scrollPos == prevScrollPos) {
+ if (startedScroll) {
+ // If we had the same scroll position in two frames consecutively,
+ // we consider scroll has finished.
+ if (sameScrollPosition) {
+ break;
+ }
+ sameScrollPosition = true;
+ }
+ } else {
+ sameScrollPosition = false;
+ }
+
+ if (!startedScroll && scrollPos > 0) {
+ startedScroll = true;
+ }
+ prevScrollPos = scrollPos;
+ }
+
+ // Use a fuzzy comparison with epsilon=1, because expectedPosition
+ // can be fractional (e.g. if viewport height is an odd number of pixels,
+ // then 50vh will be fractional), and ClampAndAlignWithPixels can round
+ // the final scroll offset requested by APZ at the end of an animation,
+ // to an integer value. We plan to remove ClampAndAlignWithPixels in
+ // bug 1774315.
+ isfuzzy(window.scrollY, expectedPosition, 1,
+ "Snaps to the second snap point rather than the third snap point " +
+ "which was initially out of the scrollport even if it's closer to " +
+ "the top of the next page than the second snap point");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scroll.html b/gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scroll.html
new file mode 100644
index 0000000000..ddf65d2209
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scroll.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Re-snapping to the last snapped element on APZ</title>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ margin: 0;
+ }
+ div {
+ position: absolute;
+ }
+ #scroller {
+ width: 100%;
+ height: 500px;
+ overflow-y: scroll;
+ scroll-snap-type: y proximity; /* to escape from the last snap point */
+ }
+ .child {
+ width: 100%;
+ height: 100px;
+ background-color: blue;
+ scroll-snap-align: start;
+ }
+ </style>
+</head>
+<body>
+ <div id="scroller">
+ <div class="child" style="top: 0px;"></div>
+ <div class="child" style="top: 200px;"></div>
+ <div id="spacer" style="width: 100%; height: 2000px;"></div>
+ </div>
+ <script type="application/javascript">
+ async function test() {
+ let transformEndPromise = promiseTransformEnd();
+
+ // The scroll delta value for promiseMoveMouseAndScrollWheelOver doesn't
+ // exactly mean promiseMoveMouseAndScrollWheelOver scrolls the delta
+ // pixels. It will be convered to OS internal values depending on
+ // platforms. Below values are empirical values to work this test on all
+ // platforms.
+ const scrollDelta = getPlatform() == "mac" ? 10 : 100;
+ // Scroll to a position where the distance from the next snap target
+ // position is less than the scroll snap proximity value.
+ await promiseMoveMouseAndScrollWheelOver(scroller, 100, 100,
+ true /* waitForScroll */, scrollDelta);
+
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+
+ is(scroller.scrollTop, 200, "snap to 200px");
+
+ document.querySelectorAll(".child")[1].style.top = "400px";
+ is(scroller.scrollTop, 400, "re-snap to 400px");
+
+ // Make sure the new snap position has been reflected in APZ side.
+ await promiseApzFlushedRepaints();
+
+ // Now trigger another wheel scroll to escape from the last snap point.
+ transformEndPromise = promiseTransformEnd();
+ // With a small `layout.css.scroll-behavior.spring-constant` preference
+ // value (it's set in test_group_scroll_snap.html), an async scroll
+ // operation, e.g. a wheel scroll operation, fires multiple scroll events
+ // so that here we wait the first scroll event here by specifying
+ // `waitForScroll=true`, there should be other scroll events after the
+ // first one.
+ await promiseMoveMouseAndScrollWheelOver(scroller, 100, 100,
+ true /* waitForScroll */, scrollDelta + 1000);
+
+ // Expand the scrollable region during the wheel scroll.
+ spacer.style.height = "2200px";
+
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+
+ isnot(scroller.scrollTop, 400, "Do not re-snap to the original 400px point");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scrollBy.html b/gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scrollBy.html
new file mode 100644
index 0000000000..0dc398e41d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scrollBy.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Re-snapping to the last snapped element on APZ</title>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ margin: 0;
+ }
+ div {
+ position: absolute;
+ }
+ #scroller {
+ width: 100%;
+ height: 500px;
+ overflow-y: scroll;
+ scroll-snap-type: y proximity; /* to escape from the last snap point */
+ }
+ .child {
+ width: 100%;
+ height: 100px;
+ background-color: blue;
+ scroll-snap-align: start;
+ }
+ </style>
+</head>
+<body>
+ <div id="scroller">
+ <div class="child" style="top: 0px;"></div>
+ <div class="child" style="top: 200px;"></div>
+ <div id="spacer" style="width: 100%; height: 2000px;"></div>
+ </div>
+ <script type="application/javascript">
+ async function test() {
+ const proximityThreshold = scroller.getBoundingClientRect().height * 0.3;
+
+ let transformEndPromise = promiseTransformEnd();
+ scroller.scrollBy({left: 0, top: 100, behavior: "smooth"});
+
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+
+ is(scroller.scrollTop, 200, "snap to 200px");
+
+ document.querySelectorAll(".child")[1].style.top = "400px";
+ is(scroller.scrollTop, 400, "re-snap to 400px");
+
+ // Now trigger another async scroll to escape from the last snap point.
+ transformEndPromise = promiseTransformEnd();
+ scroller.scrollBy({left: 0, top: proximityThreshold + 100,
+ behavior: "smooth"});
+
+ // Expand the scrollable region during the async scroll.
+ spacer.style.height = "2200px";
+
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+
+ isnot(scroller.scrollTop, 400, "Do not re-snap to the original 400px point");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_tables_perspective.html b/gfx/layers/apz/test/mochitest/helper_scroll_tables_perspective.html
new file mode 100644
index 0000000000..404274d3f4
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_tables_perspective.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<head>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+async function test() {
+ var subframe = document.getElementById("content-wrapper");
+
+ // scroll over the middle of the subframe, to make sure it scrolls,
+ // not the page
+ var scrollPos = subframe.scrollTop;
+ await promiseMoveMouseAndScrollWheelOver(subframe, 100, 100);
+ ok(subframe.scrollTop > scrollPos, "subframe scrolled after wheeling over it");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ html {
+ perspective:1000px;
+ overflow: hidden;
+ }
+ #fullscreen-wrapper {
+ display:table;
+ visibility:hidden;
+ width:100%;
+ height:100%;
+ position:fixed;
+ top:0;
+ left:0;
+ overflow:hidden;
+ z-index:9999;
+ perspective:1000px;
+ }
+ #content-wrapper {
+ overflow-y:auto;
+ height: 100vh;
+ }
+ #content-content {
+ min-height: 10000px;
+ }
+ </style>
+</head>
+<body>
+ <div id="fullscreen-wrapper">
+ <div></div>
+ </div>
+ <div id="content-wrapper">
+ <div id="content-content">
+ A<br>
+ B<br>
+ C<br>
+ D<br>
+ E<br>
+ f<br>
+ g<br>
+ h<br>
+ i<br>
+ j<br>
+ </div>
+ </div>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_thumb_dragging.html b/gfx/layers/apz/test/mochitest/helper_scroll_thumb_dragging.html
new file mode 100644
index 0000000000..821cf00be7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_thumb_dragging.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<style>
+iframe {
+ width: 500px;
+ height: 50px; /* lower height relative to the root scrollable height */
+}
+</style>
+<div style="height: 200vh;"></div>
+<iframe src="http://example.org/"></iframe>
+<div style="height: 200vh;"></div>
+<script>
+ // Silence SimpleTest warning about missing assertions by having it wait
+ // indefinitely. We don't need to give it an explicit finish because the
+ // entire window this test runs in will be closed after subtestDone is called.
+ SimpleTest.waitForExplicitFinish();
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_scrollbar_snap_bug1501062.html b/gfx/layers/apz/test/mochitest/helper_scrollbar_snap_bug1501062.html
new file mode 100644
index 0000000000..ec18fd856d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scrollbar_snap_bug1501062.html
@@ -0,0 +1,135 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Exercising the slider.snapMultiplier code</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+</head>
+<body>
+ <div id="scrollable" style="width: 300px; height: 300px; overflow: auto">
+ <div id="filler" style="height: 2000px; background-image: linear-gradient(red,blue)"></div>
+ </div>
+</body>
+<script type="text/javascript">
+async function test() {
+ // Note that this pref is a read-once-on-startup pref so we can't change it
+ // and have the change take effect. Instead we just use the value to determine
+ // what the expected behaviour is.
+ var snapMultiplier = SpecialPowers.getIntPref("slider.snapMultiplier");
+
+ // Much of the code below is "inlined" from promiseVerticalScrollbarDrag. Reusing
+ // that code was nontrivial given the modifications we needed to make, and
+ // would have increased the complexity of that helper function more than I'd
+ // like. However if any bugfixes are made to that function this code might
+ // need to be updated as well.
+
+ var scrollableDiv = document.getElementById("scrollable");
+ var boundingClientRect = scrollableDiv.getBoundingClientRect();
+ var verticalScrollbarWidth = boundingClientRect.width - scrollableDiv.clientWidth;
+ if (verticalScrollbarWidth == 0) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ // register a scroll listener for the initial drag
+ let scrollPromise = new Promise(resolve => {
+ scrollableDiv.addEventListener("scroll", resolve, {once: true});
+ });
+
+ var upArrowHeight = verticalScrollbarWidth; // assume square scrollbar buttons
+ var mouseX = scrollableDiv.clientWidth + (verticalScrollbarWidth / 2);
+ var mouseY = upArrowHeight + 5; // start dragging somewhere in the thumb
+
+ dump("Starting drag at " + mouseX + ", " + mouseY + " from top-left of #" + scrollableDiv.id + "\n");
+
+ // Move the mouse to the scrollbar thumb and drag it down
+ await promiseNativeMouseEventWithAPZ({
+ target: scrollableDiv,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ type: "mousemove",
+ });
+ await promiseNativeMouseEventWithAPZ({
+ target: scrollableDiv,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ type: "mousedown",
+ });
+ // drag down by 100 pixels
+ mouseY += 100;
+ await promiseNativeMouseEventWithAPZ({
+ target: scrollableDiv,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ type: "mousemove",
+ });
+
+ // wait here until the scroll event listener is triggered.
+ await scrollPromise;
+ var savedScrollPos = scrollableDiv.scrollTop;
+ ok(savedScrollPos > 0, "Scrolled to " + savedScrollPos);
+
+ // register a new scroll event listener. The next mousemove below will either
+ // trigger the snapback behaviour (if snapMultiplier > 0) or trigger a vertical
+ // scroll (if snapMultiplier == 0) because of the x- and y-coordinates we move
+ // the mouse to. This allows us to wait for a scroll event in either case.
+ // If we only triggered the snapback case then waiting for the scroll to
+ // "not happen" in the other case would be more error-prone.
+ scrollPromise = new Promise(resolve => {
+ scrollableDiv.addEventListener("scroll", resolve, {once: true});
+ });
+ // Add 2 to snapMultipler just to make sure we get far enough away from the scrollbar
+ var snapBackDistance = (snapMultiplier + 2) * verticalScrollbarWidth;
+ await promiseNativeMouseEventWithAPZ({
+ target: scrollableDiv,
+ offsetX: mouseX + snapBackDistance,
+ offsetY: mouseY + 10,
+ type: "mousemove",
+ });
+
+ // wait here until the scroll happens
+ await scrollPromise;
+ if (snapMultiplier > 0) {
+ ok(scrollableDiv.scrollTop == 0, "Scroll position snapped back to " + scrollableDiv.scrollTop);
+ } else {
+ ok(scrollableDiv.scrollTop > savedScrollPos, "Scroll position increased to " + scrollableDiv.scrollTop);
+ }
+
+ // Now we move the mouse back to the old position to ensure the scroll position
+ // gets restored properly
+ scrollPromise = new Promise(resolve => {
+ scrollableDiv.addEventListener("scroll", resolve, {once: true});
+ });
+ await promiseNativeMouseEventWithAPZ({
+ target: scrollableDiv,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ type: "mousemove",
+ });
+
+ // wait here until the scroll happens
+ await scrollPromise;
+ ok(scrollableDiv.scrollTop == savedScrollPos, "Scroll position was restored to " + scrollableDiv.scrollTop);
+
+ // Release mouse and ensure the scroll position stuck
+ await promiseNativeMouseEventWithAPZ({
+ target: scrollableDiv,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ type: "mouseup",
+ });
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ ok(scrollableDiv.scrollTop == savedScrollPos, "Final scroll position was " + scrollableDiv.scrollTop);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scrollbarbutton_repeat.html b/gfx/layers/apz/test/mochitest/helper_scrollbarbutton_repeat.html
new file mode 100644
index 0000000000..723b250bd8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scrollbarbutton_repeat.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Basic test that click and hold on a scrollbar button works as expected</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="text/javascript">
+
+
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ let scroller = document.getElementById('scroller');
+ let w = {}, h = {};
+ utils.getScrollbarSizes(scroller, w, h);
+ let verticalScrollbarWidth = w.value;
+ if (verticalScrollbarWidth == 0) {
+ ok(false, "No scrollbar, test will fail");
+ }
+ let downArrowHeight = verticalScrollbarWidth; // assume square scrollbar buttons
+ var mouseX = scroller.clientWidth + verticalScrollbarWidth / 2;
+ let mouseY = scroller.clientHeight - (downArrowHeight / 2);
+
+ let waitForScroll = waitForScrollEvent(scroller);
+
+ info("Synthesizing click at (" + mouseX + ", " + mouseY);
+ await promiseNativeMouseEventWithAPZ({
+ type: "click",
+ target: scroller,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ });
+
+ await waitForScroll;
+
+ // Sanity-check: we should have scrolled.
+ ok(scroller.scrollTop > 0, "Should have scrolled by clicking the scrollbar button");
+
+ let startPos = scroller.scrollTop;
+
+ info("scroller.scrollTop " + scroller.scrollTop);
+
+ // mouse move over the scrollbar button
+ await promiseNativeMouseEventWithAPZ({
+ type: "mousemove",
+ target: scroller,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ });
+ // mouse down
+ await promiseNativeMouseEventWithAPZ({
+ type: "mousedown",
+ target: scroller,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ });
+
+ info("sent mouse down");
+
+ info("scroller.scrollTop " + scroller.scrollTop);
+
+ // mouse down on the scrollbar button and then wait until
+ // we scroll more 2x the distance of one click.
+ while ((scroller.scrollTop - startPos) < 2*startPos) {
+ let waitForScroll2 = waitForScrollEvent(scroller);
+ // Wait a bit
+ await SpecialPowers.promiseTimeout(50);
+ await waitForScroll2;
+ info("loop scroller.scrollTop " + scroller.scrollTop);
+ }
+
+ await promiseNativeMouseEventWithAPZ({
+ type: "mouseup",
+ target: scroller,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ });
+
+ ok(true, "got enough scroll");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ .spacer {
+ background-color: #212121;
+ height: 9000vh;
+ }
+ </style>
+</head>
+<body>
+ <div id="scroller" style="overflow: auto; width: 200px; height: 200px;">
+ <div class="spacer"></div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scrollbarbuttonclick_checkerboard.html b/gfx/layers/apz/test/mochitest/helper_scrollbarbuttonclick_checkerboard.html
new file mode 100644
index 0000000000..e7b7895966
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scrollbarbuttonclick_checkerboard.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test that repeated scrollbar button clicks do not cause checkerboarding</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="text/javascript">
+
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ let scrollId = utils.getViewId(document.documentElement);
+ let w = {}, h = {};
+ utils.getScrollbarSizes(document.documentElement, w, h);
+ let verticalScrollbarWidth = w.value;
+ if (verticalScrollbarWidth == 0) {
+ ok(true, "No scrollbar, can't do this test");
+ }
+ let downArrowHeight = verticalScrollbarWidth; // assume square scrollbar buttons
+ var mouseX = document.documentElement.clientWidth + verticalScrollbarWidth / 2;
+ let mouseY = document.documentElement.clientHeight - (downArrowHeight / 2);
+
+ // Hold the mouse down on the scrollbar button and
+ // keep it down to trigger the "button repeat" codepath.
+ await promiseNativeMouseEventWithAPZ({
+ type: "mousedown",
+ target: window,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ });
+
+ const repetitions = 20;
+ const repeat_delay = 50; // milliseconds
+ for (i = 0; i < repetitions; i++) {
+ // Wait for the results of the click (or, on subsequent iterations
+ // the repeat) to be painted.
+ await promiseFrame();
+
+ assertNotCheckerboarded(utils, scrollId, "after scrollbar button click-hold", true);
+
+ // Wait enough time to trigger the repeat timer.
+ await SpecialPowers.promiseTimeout(repeat_delay);
+ }
+
+ // Release mouse button to clean up.
+ await promiseNativeMouseEventWithAPZ({
+ type: "mouseup",
+ target: window,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ });
+
+ // Sanity-check: we should have scrolled.
+ ok(window.scrollY > 0, "Should have scrolled by clicking the scrollbar button");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ .page-footer {
+ background-color: #212121;
+ color: #fff;
+ height: 3000px;
+ }
+ </style>
+</head>
+<body>
+ <footer class="page-footer"></footer>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scrollbartrack_click_overshoot.html b/gfx/layers/apz/test/mochitest/helper_scrollbartrack_click_overshoot.html
new file mode 100644
index 0000000000..7ee9f3f052
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scrollbartrack_click_overshoot.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Scrolling with mouse down on the scrollbar</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ .content {
+ width: 1000px;
+ height: 20000px;
+ }
+ </style>
+ <script type="text/javascript">
+
+async function test() {
+ var targetElement = elementForTarget(window);
+ var w = {},
+ h = {};
+ utilsForTarget(window).getScrollbarSizes(targetElement, w, h);
+ var verticalScrollbarWidth = w.value;
+ var mouseX = targetElement.clientWidth + verticalScrollbarWidth / 2;
+ var mouseY = targetElement.clientHeight - 100; // 100 pixels above the bottom of the scrollbar track
+
+ let scrollEndPromise = promiseScrollend();
+
+ // Click and hold the mouse. Thumb should start scrolling towards the click location.
+ await promiseNativeMouseEventWithAPZ({
+ target: window,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ type: "mousemove",
+ });
+ // mouse down
+ await promiseNativeMouseEventWithAPZ({
+ target: window,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ type: "mousedown",
+ });
+
+ // Wait for scrolling to complete
+ await scrollEndPromise;
+
+ // Work around bug 1825879: we may get an extra scrollend
+ // event too early.
+ if (window.scrollY < (window.scrollMaxY / 2)) {
+ scrollEndPromise = promiseScrollend();
+ await scrollEndPromise;
+ }
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ var result = hitTest({x: mouseX, y: mouseY});
+
+ // Check that the scroll thumb is under the cursor.
+ // If the bug occurs, the thumb will scroll too far and
+ // will not be under the cursor.
+ ok((result.hitInfo & APZHitResultFlags.SCROLLBAR_THUMB) != 0, "Scrollbar thumb hit");
+
+ await promiseNativeMouseEventWithAPZ({
+ target: window,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ type: "mouseup",
+ });
+}
+
+if (getPlatform() == "mac") {
+ ok(true, "Skipping test on Mac (bug 1851423)");
+ subtestDone();
+} else {
+ // Note: on Linux, if the gtk-primary-button-warps-slider setting
+ // is enabled, this test will not exercise the codepath it intends
+ // to test (the thumb will jump immediately under the cursor, causing
+ // the test to pass trivially). However, the test should still not fail.
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+}
+
+ </script>
+</head>
+<body>
+ <div class="content">Some content to ensure the root scrollframe is scrollable</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scrollby_bug1531796.html b/gfx/layers/apz/test/mochitest/helper_scrollby_bug1531796.html
new file mode 100644
index 0000000000..5e6f0e1833
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scrollby_bug1531796.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test that scrollBy() doesn't scroll more than it should</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ const maxSteps = 20;
+ let scrollPerStep = 40;
+ for (let step = 0; step < maxSteps; step++) {
+ window.scrollBy(0, scrollPerStep);
+ await promiseFrame();
+ }
+ is(window.scrollY, maxSteps * scrollPerStep, "Scrolled by the expected amount");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ body {
+ height: 5000px;
+ background: linear-gradient(red, black);
+ }
+ </style>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scrollend_bubbles.html b/gfx/layers/apz/test/mochitest/helper_scrollend_bubbles.html
new file mode 100644
index 0000000000..d0d763b474
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scrollend_bubbles.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html, body { margin: 0; }
+
+ body {
+ height: 10000px;
+ }
+
+ #container {
+ height: 500px;
+ width: 500px;
+ overflow: scroll;
+ }
+
+ .spacer {
+ height: 5000px;
+ width: 100%;
+ }
+ </style>
+ <script>
+const searchParams = new URLSearchParams(location.search);
+
+async function test() {
+ let scrollendCount = 0;
+
+ // When scrollend is fired at the document, the document and
+ // window event listeners should be fired.
+ let expectedScrollendCount = 2;
+ let scrollTarget = document.scrollingElement;
+
+ // The scrollend event should not bubble if the target is not Document.
+ function onElementScrollend(e) {
+ scrollendCount += 1;
+ is(e.bubbles, false, "Event bubbles should be false for Element");
+ }
+
+ // The scrollend event should bubble if the target is Document.
+ function onDocumentScrollend(e) {
+ scrollendCount += 1;
+ is(e.bubbles, true, "Event bubbles should be true for Document");
+ }
+
+ function failOnScrollend(e) {
+ ok(false, e.target + ": should not receive a scrollend event");
+ }
+
+ switch (searchParams.get("scroll-target")) {
+ case "document":
+ // The window and the document event listeners should be triggered.
+ document.addEventListener("scrollend", onDocumentScrollend);
+ window.addEventListener("scrollend", onDocumentScrollend);
+
+ // Fail if the element receives a scrollend event.
+ container.addEventListener("scrollend", failOnScrollend);
+ break;
+ case "element":
+ scrollTarget = document.getElementById("container");
+ expectedScrollendCount = 1;
+
+ // Only the the element event listener should be triggered.
+ container.addEventListener("scrollend", onElementScrollend);
+
+ // Fail if the document or window receive a scrollend event.
+ document.addEventListener("scrollend", failOnScrollend);
+ window.addEventListener("scrollend", failOnScrollend);
+ break;
+ default:
+ ok(false, "Unsupported scroll-target: " + searchParams.get("scroll-target"));
+ break;
+ }
+
+ // Call the scrollTo function on the target to trigger the scrollend.
+ scrollTarget.scrollBy({ top: 500, left: 0 });
+
+ // Ensure the refresh driver has ticked.
+ await promiseFrame();
+
+ // A scrollend event should be posted after the refresh driver has ticked.
+ is(scrollendCount, expectedScrollendCount,
+ "Trigger the expected number of scrollend events");
+}
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+ </script>
+</head>
+<body>
+ <div id="container">
+ <div class="spacer">
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scrollframe_activation_on_load.html b/gfx/layers/apz/test/mochitest/helper_scrollframe_activation_on_load.html
new file mode 100644
index 0000000000..1947a89a8f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scrollframe_activation_on_load.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html id="root-element">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1151663
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1151663, helper page</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+ // In this test we have a simple page which is scrollable, with a
+ // scrollable <div> which is also scrollable. We test that the
+ // <div> does not get an initial APZC, since primary scrollable
+ // frame is the page's root scroll frame.
+
+ function test() {
+ // Reconstruct the APZC tree structure in the last paint.
+ var apzcTree = getLastApzcTree();
+
+ // The apzc tree for this page should consist of a single root APZC,
+ // which either is the RCD with no child APZCs (e10s/B2G case) or has a
+ // single child APZC which is for the RCD (fennec case). If we activate
+ // all scrollframes then we expect there to be a child.
+ var rcd = findRcdNode(apzcTree);
+ ok(rcd != null, "found the RCD node");
+
+ // For reference when I run this test locally when I modified it to
+ // handle activateAllScrollFrames I got the following values:
+ // rootElement.clientWidth 1280
+ // rootElement.clientHeight 863
+ // rootDisplayPort: {"x":0,"y":0,"w":1280,"h":3200} non wr
+ // rootDisplayPort: {"x":0,"y":0,"w":1280,"h":1792} wr
+ // At this time activateAllScrollFrames is only active with wr fission:
+ // div displayPort: {"x":0,"y":0,"w":50,"h":50} wr fission
+ // theDiv.clientWidth: 50 wr fission
+ // theDiv.clientHeight: 50 wr fission
+
+ let config = getHitTestConfig();
+
+ let heightMultiplier = SpecialPowers.getCharPref("apz.y_stationary_size_multiplier");
+ // With WebRender, the effective height multiplier can be reduced
+ // for alignment reasons. The reduction should be no more than a
+ // factor of two.
+ heightMultiplier /= 2;
+ info("effective displayport height multipler is " + heightMultiplier);
+
+ let rootDisplayPort = getLastContentDisplayportFor('root-element');
+ let rootElement = document.getElementById('root-element');
+ info("rootDisplayPort is " + JSON.stringify(rootDisplayPort));
+ info("rootElement.clientWidth " + rootElement.clientWidth +
+ " rootElement.clientHeight " + rootElement.clientHeight);
+ ok(rootDisplayPort.width >= rootElement.clientWidth, "rootDisplayPort should be at least as wide as page");
+ ok(rootDisplayPort.height >= heightMultiplier * rootElement.clientHeight,
+ "rootDisplayPort should be at least as tall as page times heightMultiplier");
+
+ let displayPort = getLastContentDisplayportFor('thediv');
+ if (config.activateAllScrollFrames) {
+ is(rcd.children.length, 1, "expected one child on the RCD");
+ let theDiv = document.getElementById("thediv");
+ ok(displayPort != null, "should have displayPort");
+ info("div displayPort: " + JSON.stringify(displayPort));
+ info("div width: " + theDiv.clientWidth);
+ info("div height: " + theDiv.clientHeight);
+ ok(displayPort.width <= theDiv.clientWidth + 1, "displayPort w should have empty margins");
+ ok(displayPort.height <= theDiv.clientHeight + 1, "displayPort h should have empty margins");
+ } else {
+ is(rcd.children.length, 0, "expected no children on the RCD");
+ ok(displayPort == null, "should not have displayPort");
+ }
+ }
+ waitUntilApzStable()
+ .then(forceLayerTreeToCompositor)
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</head>
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1151663">Mozilla Bug 1151663</a>
+ <div id="thediv" style="height: 50px; width: 50px; overflow: scroll">
+ <!-- Put enough content into the subframe to make it have a nonzero scroll range -->
+ <div style="height: 100px; width: 50px"></div>
+ </div>
+ <!-- Put enough content into the page to make it have a nonzero scroll range -->
+ <div style="height: 5000px"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scrollto_tap.html b/gfx/layers/apz/test/mochitest/helper_scrollto_tap.html
new file mode 100644
index 0000000000..a1fb3f67a6
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scrollto_tap.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity touch-tapping test</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ while (window.scrollY == 0) {
+ // the scrollframe is not yet marked as APZ-scrollable. Mark it so
+ // before continuing.
+ window.scrollTo(0, 1);
+ await promiseApzFlushedRepaints();
+ }
+
+ // This is a scroll by 20px that should use paint-skipping if possible.
+ // If paint-skipping is enabled, this should not trigger a paint, but go
+ // directly to the compositor using an empty transaction. We check for this
+ // by ensuring the document element did not get painted.
+ var utils = window.opener.SpecialPowers.getDOMWindowUtils(window);
+ var elem = document.documentElement;
+ var skipping = location.search == "?true";
+ utils.checkAndClearPaintedState(elem);
+ window.scrollTo(0, 20);
+ await promiseAllPaintsDone();
+
+ if (skipping) {
+ is(utils.checkAndClearPaintedState(elem), false, "Document element didn't get painted");
+ }
+
+ // After that's done, we click on the button to make sure the
+ // skipped-paint codepath still has working APZ event transformations.
+ let clickPromise = new Promise(resolve => {
+ document.addEventListener("click", resolve);
+ });
+
+ await synthesizeNativeTap(document.getElementById("b"), 5, 5, function() {
+ dump("Finished synthesizing tap, waiting for button to be clicked...\n");
+ });
+
+ let clickEvent = await clickPromise;
+ is(clickEvent.target, document.getElementById("b"), "Clicked on button, yay! (at " + clickEvent.clientX + "," + clickEvent.clientY + ")");
+}
+
+waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body style="height: 5000px">
+ <div style="height: 50px">spacer</div>
+ <button id="b" style="width: 10px; height: 10px"></button>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_self_closer.html b/gfx/layers/apz/test/mochitest/helper_self_closer.html
new file mode 100644
index 0000000000..09a9286c06
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_self_closer.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<script>
+waitUntilApzStable().then(() => {
+ dump("Bye!\n");
+ window.close();
+});
+</script>
+
+See ya! (This window will close itself)
diff --git a/gfx/layers/apz/test/mochitest/helper_smoothscroll_spam.html b/gfx/layers/apz/test/mochitest/helper_smoothscroll_spam.html
new file mode 100644
index 0000000000..154ed3cafe
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_smoothscroll_spam.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for scenario in bug 1228407</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ utils.advanceTimeAndRefresh(0);
+
+ // Part of the problem in bug 1228407 was that the main-thread scroll
+ // generation counter was continually increasing (due to scrollBy calls in
+ // quick succession), and so repaint requests from APZ would get ignored (due
+ // to stale scroll generation), and so the main thread scroll position would
+ // never actually get updated. This loop exercises that case. The expected
+ // behaviour (pre-APZ) was that the scrollBy call would actually start the
+ // scroll animation and advance the scroll position a little bit, so the next
+ // scrollBy call would move the animation destination a little bit, and so
+ // the loop would continue advancing the scroll position. The bug resulted
+ // in the scroll position not advancing at all.
+ for (let i = 0; i < 100; i++) {
+ document.scrollingElement.scrollBy({top:60, behavior: "smooth"});
+ await promiseOnlyApzControllerFlushed();
+ utils.advanceTimeAndRefresh(16);
+ }
+
+ utils.restoreNormalRefresh();
+ await promiseOnlyApzControllerFlushed();
+
+ let scrollPos = document.scrollingElement.scrollTop;
+ ok(scrollPos > 60, `Scrolled ${scrollPos}px, should be more than 60`);
+}
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ body {
+ height: 5000px;
+ background: linear-gradient(red, black);
+ }
+ </style>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_smoothscroll_spam_interleaved.html b/gfx/layers/apz/test/mochitest/helper_smoothscroll_spam_interleaved.html
new file mode 100644
index 0000000000..003ae49ea5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_smoothscroll_spam_interleaved.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for scenario in bug 1228407 with two scrollframes</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ utils.advanceTimeAndRefresh(0);
+
+ // Basically the same setup as in helper_smoothscroll_spam.html, but
+ // with two scrollframes that get scrolled in an interleaved manner.
+ // The original fix for bug 1228407 left this scenario unhandled, with
+ // bug 1231177 tracking the problem. This test exercises the scenario.
+ let s1 = document.getElementById('s1');
+ let s2 = document.getElementById('s2');
+ for (let i = 0; i < 100; i++) {
+ s1.scrollBy({top:60, behavior: "smooth"});
+ s2.scrollBy({top:60, behavior: "smooth"});
+ await promiseOnlyApzControllerFlushed();
+ utils.advanceTimeAndRefresh(16);
+ }
+
+ utils.restoreNormalRefresh();
+ await promiseOnlyApzControllerFlushed();
+
+ let s1pos = s1.scrollTop;
+ let s2pos = s2.scrollTop;
+ ok(s1pos > 60, `s1 scrolled ${s1pos}px, should be more than 60`);
+ ok(s2pos > 60, `s2 scrolled ${s2pos}px, should be more than 60`);
+}
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ .scrollable {
+ overflow: scroll;
+ height: 300px;
+ }
+
+ .content {
+ height: 1000px;
+ background-image: linear-gradient(green, blue);
+ }
+ </style>
+</head>
+<body>
+ <div id="s1" class="scrollable"><div class="content"></div></div>
+ <div id="s2" class="scrollable"><div class="content"></div></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_subframe_style.css b/gfx/layers/apz/test/mochitest/helper_subframe_style.css
new file mode 100644
index 0000000000..5af9640802
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_subframe_style.css
@@ -0,0 +1,15 @@
+body {
+ height: 500px;
+}
+
+.inner-frame {
+ margin-top: 50px; /* this should be at least 30px */
+ height: 200%;
+ width: 75%;
+ overflow: scroll;
+}
+.inner-content {
+ height: 200%;
+ width: 200%;
+ background: repeating-linear-gradient(#EEE, #EEE 100px, #DDD 100px, #DDD 200px);
+}
diff --git a/gfx/layers/apz/test/mochitest/helper_tab_scroll_scrollIntoView.html b/gfx/layers/apz/test/mochitest/helper_tab_scroll_scrollIntoView.html
new file mode 100644
index 0000000000..044b08eb0c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_tab_scroll_scrollIntoView.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+.spacer {
+ height: 200vh;
+ width: 100%;
+ background: green;
+}
+#target {
+ height: 5vh;
+ width: 100%;
+ background: red;
+}
+ </style>
+</head>
+<body>
+ <div class="spacer"></div>
+ <button id="target"></button>
+ <div class="spacer"></div>
+</body>
+<script>
+async function test() {
+ let scrollendPromise = promiseScrollend();
+
+ window.synthesizeKey("KEY_Tab");
+
+ await scrollendPromise;
+
+ let tabScrollPosition = document.scrollingElement.scrollTop;
+
+ target.scrollIntoView({block: "center", inline: "center"});
+
+ let scrollIntoViewPosition = document.scrollingElement.scrollTop;
+
+ // The scroll position after an explicit scrollIntoView call on the target
+ // with block and inline set to "center" should be within +/- 1 of the
+ // scroll position from the tab scroll.
+ ok(tabScrollPosition >= scrollIntoViewPosition - 1 &&
+ tabScrollPosition <= scrollIntoViewPosition + 1,
+ "target element should be centered by tab scroll");
+}
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_tall.html b/gfx/layers/apz/test/mochitest/helper_tall.html
new file mode 100644
index 0000000000..7fde795fdc
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_tall.html
@@ -0,0 +1,504 @@
+<html id="tall_html">
+<body>
+This is a tall page<br/>
+1<br/>
+2<br/>
+3<br/>
+4<br/>
+5<br/>
+6<br/>
+7<br/>
+8<br/>
+9<br/>
+10<br/>
+11<br/>
+12<br/>
+13<br/>
+14<br/>
+15<br/>
+16<br/>
+17<br/>
+18<br/>
+19<br/>
+20<br/>
+21<br/>
+22<br/>
+23<br/>
+24<br/>
+25<br/>
+26<br/>
+27<br/>
+28<br/>
+29<br/>
+30<br/>
+31<br/>
+32<br/>
+33<br/>
+34<br/>
+35<br/>
+36<br/>
+37<br/>
+38<br/>
+39<br/>
+40<br/>
+41<br/>
+42<br/>
+43<br/>
+44<br/>
+45<br/>
+46<br/>
+47<br/>
+48<br/>
+49<br/>
+50<br/>
+51<br/>
+52<br/>
+53<br/>
+54<br/>
+55<br/>
+56<br/>
+57<br/>
+58<br/>
+59<br/>
+60<br/>
+61<br/>
+62<br/>
+63<br/>
+64<br/>
+65<br/>
+66<br/>
+67<br/>
+68<br/>
+69<br/>
+70<br/>
+71<br/>
+72<br/>
+73<br/>
+74<br/>
+75<br/>
+76<br/>
+77<br/>
+78<br/>
+79<br/>
+80<br/>
+81<br/>
+82<br/>
+83<br/>
+84<br/>
+85<br/>
+86<br/>
+87<br/>
+88<br/>
+89<br/>
+90<br/>
+91<br/>
+92<br/>
+93<br/>
+94<br/>
+95<br/>
+96<br/>
+97<br/>
+98<br/>
+99<br/>
+100<br/>
+101<br/>
+102<br/>
+103<br/>
+104<br/>
+105<br/>
+106<br/>
+107<br/>
+108<br/>
+109<br/>
+110<br/>
+111<br/>
+112<br/>
+113<br/>
+114<br/>
+115<br/>
+116<br/>
+117<br/>
+118<br/>
+119<br/>
+120<br/>
+121<br/>
+122<br/>
+123<br/>
+124<br/>
+125<br/>
+126<br/>
+127<br/>
+128<br/>
+129<br/>
+130<br/>
+131<br/>
+132<br/>
+133<br/>
+134<br/>
+135<br/>
+136<br/>
+137<br/>
+138<br/>
+139<br/>
+140<br/>
+141<br/>
+142<br/>
+143<br/>
+144<br/>
+145<br/>
+146<br/>
+147<br/>
+148<br/>
+149<br/>
+150<br/>
+151<br/>
+152<br/>
+153<br/>
+154<br/>
+155<br/>
+156<br/>
+157<br/>
+158<br/>
+159<br/>
+160<br/>
+161<br/>
+162<br/>
+163<br/>
+164<br/>
+165<br/>
+166<br/>
+167<br/>
+168<br/>
+169<br/>
+170<br/>
+171<br/>
+172<br/>
+173<br/>
+174<br/>
+175<br/>
+176<br/>
+177<br/>
+178<br/>
+179<br/>
+180<br/>
+181<br/>
+182<br/>
+183<br/>
+184<br/>
+185<br/>
+186<br/>
+187<br/>
+188<br/>
+189<br/>
+190<br/>
+191<br/>
+192<br/>
+193<br/>
+194<br/>
+195<br/>
+196<br/>
+197<br/>
+198<br/>
+199<br/>
+200<br/>
+201<br/>
+202<br/>
+203<br/>
+204<br/>
+205<br/>
+206<br/>
+207<br/>
+208<br/>
+209<br/>
+210<br/>
+211<br/>
+212<br/>
+213<br/>
+214<br/>
+215<br/>
+216<br/>
+217<br/>
+218<br/>
+219<br/>
+220<br/>
+221<br/>
+222<br/>
+223<br/>
+224<br/>
+225<br/>
+226<br/>
+227<br/>
+228<br/>
+229<br/>
+230<br/>
+231<br/>
+232<br/>
+233<br/>
+234<br/>
+235<br/>
+236<br/>
+237<br/>
+238<br/>
+239<br/>
+240<br/>
+241<br/>
+242<br/>
+243<br/>
+244<br/>
+245<br/>
+246<br/>
+247<br/>
+248<br/>
+249<br/>
+250<br/>
+251<br/>
+252<br/>
+253<br/>
+254<br/>
+255<br/>
+256<br/>
+257<br/>
+258<br/>
+259<br/>
+260<br/>
+261<br/>
+262<br/>
+263<br/>
+264<br/>
+265<br/>
+266<br/>
+267<br/>
+268<br/>
+269<br/>
+270<br/>
+271<br/>
+272<br/>
+273<br/>
+274<br/>
+275<br/>
+276<br/>
+277<br/>
+278<br/>
+279<br/>
+280<br/>
+281<br/>
+282<br/>
+283<br/>
+284<br/>
+285<br/>
+286<br/>
+287<br/>
+288<br/>
+289<br/>
+290<br/>
+291<br/>
+292<br/>
+293<br/>
+294<br/>
+295<br/>
+296<br/>
+297<br/>
+298<br/>
+299<br/>
+300<br/>
+301<br/>
+302<br/>
+303<br/>
+304<br/>
+305<br/>
+306<br/>
+307<br/>
+308<br/>
+309<br/>
+310<br/>
+311<br/>
+312<br/>
+313<br/>
+314<br/>
+315<br/>
+316<br/>
+317<br/>
+318<br/>
+319<br/>
+320<br/>
+321<br/>
+322<br/>
+323<br/>
+324<br/>
+325<br/>
+326<br/>
+327<br/>
+328<br/>
+329<br/>
+330<br/>
+331<br/>
+332<br/>
+333<br/>
+334<br/>
+335<br/>
+336<br/>
+337<br/>
+338<br/>
+339<br/>
+340<br/>
+341<br/>
+342<br/>
+343<br/>
+344<br/>
+345<br/>
+346<br/>
+347<br/>
+348<br/>
+349<br/>
+350<br/>
+351<br/>
+352<br/>
+353<br/>
+354<br/>
+355<br/>
+356<br/>
+357<br/>
+358<br/>
+359<br/>
+360<br/>
+361<br/>
+362<br/>
+363<br/>
+364<br/>
+365<br/>
+366<br/>
+367<br/>
+368<br/>
+369<br/>
+370<br/>
+371<br/>
+372<br/>
+373<br/>
+374<br/>
+375<br/>
+376<br/>
+377<br/>
+378<br/>
+379<br/>
+380<br/>
+381<br/>
+382<br/>
+383<br/>
+384<br/>
+385<br/>
+386<br/>
+387<br/>
+388<br/>
+389<br/>
+390<br/>
+391<br/>
+392<br/>
+393<br/>
+394<br/>
+395<br/>
+396<br/>
+397<br/>
+398<br/>
+399<br/>
+400<br/>
+401<br/>
+402<br/>
+403<br/>
+404<br/>
+405<br/>
+406<br/>
+407<br/>
+408<br/>
+409<br/>
+410<br/>
+411<br/>
+412<br/>
+413<br/>
+414<br/>
+415<br/>
+416<br/>
+417<br/>
+418<br/>
+419<br/>
+420<br/>
+421<br/>
+422<br/>
+423<br/>
+424<br/>
+425<br/>
+426<br/>
+427<br/>
+428<br/>
+429<br/>
+430<br/>
+431<br/>
+432<br/>
+433<br/>
+434<br/>
+435<br/>
+436<br/>
+437<br/>
+438<br/>
+439<br/>
+440<br/>
+441<br/>
+442<br/>
+443<br/>
+444<br/>
+445<br/>
+446<br/>
+447<br/>
+448<br/>
+449<br/>
+450<br/>
+451<br/>
+452<br/>
+453<br/>
+454<br/>
+455<br/>
+456<br/>
+457<br/>
+458<br/>
+459<br/>
+460<br/>
+461<br/>
+462<br/>
+463<br/>
+464<br/>
+465<br/>
+466<br/>
+467<br/>
+468<br/>
+469<br/>
+470<br/>
+471<br/>
+472<br/>
+473<br/>
+474<br/>
+475<br/>
+476<br/>
+477<br/>
+478<br/>
+479<br/>
+480<br/>
+481<br/>
+482<br/>
+483<br/>
+484<br/>
+485<br/>
+486<br/>
+487<br/>
+488<br/>
+489<br/>
+490<br/>
+491<br/>
+492<br/>
+493<br/>
+494<br/>
+495<br/>
+496<br/>
+497<br/>
+498<br/>
+499<br/>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_tap.html b/gfx/layers/apz/test/mochitest/helper_tap.html
new file mode 100644
index 0000000000..f987299447
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_tap.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity touch-tapping test</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function clickButton() {
+ document.addEventListener("click", clicked);
+
+ await synthesizeNativeTap(document.getElementById("b"), 5, 5, function() {
+ dump("Finished synthesizing tap, waiting for button to be clicked...\n");
+ });
+}
+
+function clicked(e) {
+ is(e.target, document.getElementById("b"), "Clicked on button, yay! (at " + e.clientX + "," + e.clientY + ")");
+ subtestDone();
+}
+
+waitUntilApzStable().then(clickButton);
+
+ </script>
+</head>
+<body>
+ <button id="b" style="width: 10px; height: 10px"></button>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_tap_default_passive.html b/gfx/layers/apz/test/mochitest/helper_tap_default_passive.html
new file mode 100644
index 0000000000..63e4d0dd46
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_tap_default_passive.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Ensure APZ doesn't wait for passive listeners</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+var touchdownTime;
+
+async function longPressLink() {
+ await synthesizeNativeTouch(document.getElementById("b"), 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, function() {
+ dump("Finished synthesizing touch-start, waiting for events...\n");
+ });
+}
+
+var touchstartReceived = false;
+function recordEvent(e) {
+ if (!touchstartReceived) {
+ touchstartReceived = true;
+ is(e.type, "touchstart", "Got a touchstart");
+ e.preventDefault(); // should be a no-op because it's a passive listener
+ return;
+ }
+
+ // If APZ decides to wait for the content response on a particular input block,
+ // it needs to wait until both the touchstart and touchmove event are handled
+ // by the main thread. In this case there is no touchmove at all, so APZ would
+ // end up waiting indefinitely and time out the test. The fact that we get this
+ // contextmenu event (mouselongtap on Windows) at all means that APZ decided
+ // not to wait for the content response, which is the desired behaviour, since
+ // the touchstart listener was registered as a passive listener.
+ if (getPlatform() == "windows") {
+ is(e.type, "mouselongtap", "Got a mouselongtap");
+ } else {
+ is(e.type, "contextmenu", "Got a contextmenu");
+ }
+ e.preventDefault();
+
+ setTimeout(async () => {
+ // On Windows below TOUCH_REMOVE event triggers opening a context menu, we
+ // need to prevent it.
+ const contextmenuPromise = promiseOneEvent(window, "contextmenu", event => {
+ event.preventDefault();
+ return true;
+ });
+ const touchendPromise = promiseOneEvent(window, "touchend", () => {
+ return true;
+ });
+
+ await synthesizeNativeTouch(document.getElementById("b"), 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, function() {
+ dump("Finished synthesizing touch-end to clear state; finishing test...\n");
+ });
+
+ await touchendPromise;
+ if (getPlatform() == "windows") {
+ await contextmenuPromise;
+ }
+ subtestDone();
+ }, 0);
+}
+
+function preventDefaultListener(e) {
+ e.preventDefault();
+}
+
+// Note, not passing 'passive'.
+window.addEventListener("touchstart", recordEvent, { capture: true });
+window.ontouchstart = preventDefaultListener;
+if (getPlatform() == "windows") {
+ SpecialPowers.addChromeEventListener("mouselongtap", recordEvent, true);
+} else {
+ window.addEventListener("contextmenu", recordEvent, true);
+}
+
+waitUntilApzStable()
+.then(longPressLink);
+
+ </script>
+</head>
+<body>
+ <a id="b" href="#">Link to nowhere</a>
+ <script>
+ document.addEventListener("touchstart", preventDefaultListener, { capture: true });
+ document.ontouchstart = preventDefaultListener;
+ document.documentElement.addEventListener("touchstart", preventDefaultListener, { capture: true });
+ document.documentElement.ontouchstart = preventDefaultListener;
+ document.body.addEventListener("touchstart", preventDefaultListener, { capture: true });
+ document.body.ontouchstart = preventDefaultListener;
+ document.body.setAttribute("ontouchstart", "event.preventDefault()");
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_tap_fullzoom.html b/gfx/layers/apz/test/mochitest/helper_tap_fullzoom.html
new file mode 100644
index 0000000000..7d739924f0
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_tap_fullzoom.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity touch-tapping test with fullzoom</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function clickButton() {
+ document.addEventListener("click", clicked);
+
+ await synthesizeNativeTap(document.getElementById("b"), 5, 5, function() {
+ dump("Finished synthesizing tap, waiting for button to be clicked...\n");
+ });
+}
+
+function clicked(e) {
+ is(e.target, document.getElementById("b"), "Clicked on button, yay! (at " + e.clientX + "," + e.clientY + ")");
+ subtestDone();
+}
+
+SpecialPowers.setFullZoom(window, 2.0);
+waitUntilApzStable().then(clickButton);
+
+ </script>
+</head>
+<body>
+ <button id="b" style="width: 10px; height: 10px; position: relative; top: 100px"></button>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_tap_passive.html b/gfx/layers/apz/test/mochitest/helper_tap_passive.html
new file mode 100644
index 0000000000..a8d2936e97
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_tap_passive.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Ensure APZ doesn't wait for passive listeners</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+var touchdownTime;
+
+async function longPressLink() {
+ await synthesizeNativeTouch(document.getElementById("b"), 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, function() {
+ dump("Finished synthesizing touch-start, waiting for events...\n");
+ });
+}
+
+var touchstartReceived = false;
+function recordEvent(e) {
+ if (!touchstartReceived) {
+ touchstartReceived = true;
+ is(e.type, "touchstart", "Got a touchstart");
+ e.preventDefault(); // should be a no-op because it's a passive listener
+ return;
+ }
+
+ // If APZ decides to wait for the content response on a particular input block,
+ // it needs to wait until both the touchstart and touchmove event are handled
+ // by the main thread. In this case there is no touchmove at all, so APZ would
+ // end up waiting indefinitely and time out the test. The fact that we get this
+ // contextmenu event (mouselongtap on Windows) at all means that APZ decided
+ // not to wait for the content response, which is the desired behaviour, since
+ // the touchstart listener was registered as a passive listener.
+ if (getPlatform() == "windows") {
+ is(e.type, "mouselongtap", "Got a mouselongtap");
+ } else {
+ is(e.type, "contextmenu", "Got a contextmenu");
+ }
+ e.preventDefault();
+
+ setTimeout(async () => {
+ // On Windows below TOUCH_REMOVE event triggers opening a context menu, we
+ // need to prevent it.
+ const contextmenuPromise = promiseOneEvent(window, "contextmenu", event => {
+ event.preventDefault();
+ return true;
+ });
+ const touchendPromise = promiseOneEvent(window, "touchend", () => {
+ return true;
+ });
+ await synthesizeNativeTouch(document.getElementById("b"), 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, function() {
+ dump("Finished synthesizing touch-end to clear state; finishing test...\n");
+ });
+
+ await touchendPromise;
+ if (getPlatform() == "windows") {
+ await contextmenuPromise;
+ }
+ subtestDone();
+ }, 0);
+}
+
+window.addEventListener("touchstart", recordEvent, { passive: true, capture: true });
+if (getPlatform() == "windows") {
+ SpecialPowers.addChromeEventListener("mouselongtap", recordEvent, true);
+} else {
+ window.addEventListener("contextmenu", recordEvent, true);
+}
+
+waitUntilApzStable()
+.then(longPressLink);
+
+ </script>
+</head>
+<body>
+ <a id="b" href="#">Link to nowhere</a>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_test_autoscrolling_in_oop_frame.html b/gfx/layers/apz/test/mochitest/helper_test_autoscrolling_in_oop_frame.html
new file mode 100644
index 0000000000..c1826f583f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_test_autoscrolling_in_oop_frame.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+</head>
+<body>
+ <iframe src="http://example.net" style="height: 90vh; width: 90vw;"></iframe>
+ <div style="height: 200px"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_test_reset_scaling_zoom.html b/gfx/layers/apz/test/mochitest/helper_test_reset_scaling_zoom.html
new file mode 100644
index 0000000000..31779410da
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_test_reset_scaling_zoom.html
@@ -0,0 +1,23 @@
+<html><head>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src='/tests/SimpleTest/paint_listener.js'></script>
+<script src='apz_test_utils.js'></script>
+<script src='apz_test_native_event_utils.js'></script>
+<script>
+async function doZoomIn() {
+ await waitUntilApzStable();
+ await pinchZoomInWithTouch(100, 100);
+ await promiseOnlyApzControllerFlushed();
+}
+
+// Silence SimpleTest warning about missing assertions by having it wait
+// indefinitely. We don't need to give it an explicit finish because the
+// entire window this test runs in will be closed after subtestDone is called.
+SimpleTest.waitForExplicitFinish();
+</script>
+</head>
+<body>
+Here is some text to stare at as the test runs. It serves no functional
+purpose, but gives you an idea of the zoom level. It's harder to tell what
+the zoom level is when the page is just solid white.
+</body></html>
diff --git a/gfx/layers/apz/test/mochitest/helper_test_select_popup_position.html b/gfx/layers/apz/test/mochitest/helper_test_select_popup_position.html
new file mode 100644
index 0000000000..c810751b2f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_test_select_popup_position.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src='/tests/SimpleTest/paint_listener.js'></script>
+<script src='apz_test_utils.js'></script>
+<style>
+html, body {
+ margin: 0;
+ padding: 0;
+}
+select {
+ position: absolute;
+ top: 150px;
+ left: 150px;
+ height: 30px;
+}
+</style>
+<select><option>he he he</option><option>boo boo</option><option>baz baz</option></select>
+<script>
+ // Silence SimpleTest warning about missing assertions by having it wait
+ // indefinitely. We don't need to give it an explicit finish because the
+ // entire window this test runs in will be closed after subtestDone is called.
+ SimpleTest.waitForExplicitFinish();
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_test_select_popup_position_transformed_in_parent.html b/gfx/layers/apz/test/mochitest/helper_test_select_popup_position_transformed_in_parent.html
new file mode 100644
index 0000000000..860ca079de
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_test_select_popup_position_transformed_in_parent.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src='/tests/SimpleTest/paint_listener.js'></script>
+<script src='apz_test_utils.js'></script>
+<style>
+html, body {
+ /* To calculate the popup window position easily. */
+ margin: 0;
+ padding: 0;
+}
+iframe {
+ width: 300px;
+ height: 300px;
+ /* To calculate the popup window position easily. */
+ border: none;
+}
+</style>
+<div style="transform: scale(2); transform-origin: top left;">
+ <iframe></iframe>
+</div>
+<script>
+ // Silence SimpleTest warning about missing assertions by having it wait
+ // indefinitely.
+ SimpleTest.waitForExplicitFinish();
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_test_select_popup_position_zoomed.html b/gfx/layers/apz/test/mochitest/helper_test_select_popup_position_zoomed.html
new file mode 100644
index 0000000000..2ec079369e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_test_select_popup_position_zoomed.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src='/tests/SimpleTest/paint_listener.js'></script>
+<script src='apz_test_utils.js'></script>
+<style>
+html, body {
+ /* To calculate the popup window position easily. */
+ margin: 0;
+ padding: 0;
+}
+iframe {
+ width: 300px;
+ height: 300px;
+ /* To calculate the popup window position easily. */
+ border: none;
+}
+</style>
+<iframe></iframe>
+<script>
+ // Call setResolutionAndScaleTo here to avoid bug 1691358.
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+ // Silence SimpleTest warning about missing assertions by having it wait
+ // indefinitely.
+ SimpleTest.waitForExplicitFinish();
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_test_select_zoom.html b/gfx/layers/apz/test/mochitest/helper_test_select_zoom.html
new file mode 100644
index 0000000000..d3fc5fcb67
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_test_select_zoom.html
@@ -0,0 +1,43 @@
+<html><head>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src='/tests/SimpleTest/paint_listener.js'></script>
+<script src='apz_test_utils.js'></script>
+<script src='apz_test_native_event_utils.js'></script>
+<script>
+function getSelectRect() {
+ const input = document.getElementById("select");
+ let rect = input.getBoundingClientRect();
+ let x = rect.left;
+ let y = rect.top;
+
+ const relativeOffset = getRelativeViewportOffset(window);
+
+ let resolution = SpecialPowers.getDOMWindowUtils(window).getResolution();
+ x = resolution * (x - relativeOffset.x);
+ y = resolution * (y - relativeOffset.y);
+
+ let fullZoom = SpecialPowers.getFullZoom(window);
+ rect = {
+ left: x * fullZoom,
+ top: y * fullZoom,
+ width: rect.width * fullZoom * resolution,
+ height: rect.height * fullZoom * resolution,
+ };
+
+ return rect;
+}
+</script>
+</head>
+<body>
+Here is some text to stare at as the test runs. It serves no functional
+purpose, but gives you an idea of the zoom level. It's harder to tell what
+the zoom level is when the page is just solid white.
+<select id='select' style="position: absolute; left:150px; top:300px;"><option>he he he</option><option>boo boo</option><option>baz baz</option></select>
+
+<script>
+ // Silence SimpleTest warning about missing assertions by having it wait
+ // indefinitely. We don't need to give it an explicit finish because the
+ // entire window this test runs in will be closed after subtestDone is called.
+ SimpleTest.waitForExplicitFinish();
+</script>
+</body></html>
diff --git a/gfx/layers/apz/test/mochitest/helper_test_tab_drag_event_counts.html b/gfx/layers/apz/test/mochitest/helper_test_tab_drag_event_counts.html
new file mode 100644
index 0000000000..7ad1093d3e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_test_tab_drag_event_counts.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src='/tests/SimpleTest/paint_listener.js'></script>
+ <script src='apz_test_utils.js'></script>
+ <script src='apz_test_native_event_utils.js'></script>
+</head>
+
+<body style="background-color:powderblue;">
+ <script>
+ document.addEventListener("keydown", function () {
+ document.body.style.backgroundColor = "red";
+ });
+ document.addEventListener("keyup", function () {
+ document.body.style.backgroundColor = "green";
+ });
+ SimpleTest.waitForExplicitFinish();
+ </script>
+</body>
+
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_test_tab_drag_zoom.html b/gfx/layers/apz/test/mochitest/helper_test_tab_drag_zoom.html
new file mode 100644
index 0000000000..9f0175468c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_test_tab_drag_zoom.html
@@ -0,0 +1,18 @@
+<html><head>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src='/tests/SimpleTest/paint_listener.js'></script>
+<script src='apz_test_utils.js'></script>
+<script src='apz_test_native_event_utils.js'></script>
+</head>
+<body>
+Here is some text to stare at as the test runs. It serves no functional
+purpose, but gives you an idea of the zoom level. It's harder to tell what
+the zoom level is when the page is just solid white.
+
+<script>
+ // Silence SimpleTest warning about missing assertions by having it wait
+ // indefinitely. We don't need to give it an explicit finish because the
+ // entire window this test runs in will be closed after subtestDone is called.
+ SimpleTest.waitForExplicitFinish();
+</script>
+</body></html>
diff --git a/gfx/layers/apz/test/mochitest/helper_touch_action.html b/gfx/layers/apz/test/mochitest/helper_touch_action.html
new file mode 100644
index 0000000000..10038de29f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_touch_action.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0,minimum-scale=1.0">
+ <title>Sanity touch-action test</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function checkScroll(x, y, desc) {
+ is(window.scrollX, x, desc + " - x axis");
+ is(window.scrollY, y, desc + " - y axis");
+}
+
+async function test() {
+ var target = document.getElementById("target");
+
+ // drag the page up to scroll down by 50px
+ let touchEndPromise = promiseTouchEnd(document.body);
+ ok(await synthesizeNativeTouchDrag(target, 10, 100, 0, -50),
+ "Synthesized native vertical drag (1), waiting for touch-end event...");
+ await touchEndPromise;
+ await promiseOnlyApzControllerFlushed();
+ checkScroll(0, 50, "After first vertical drag, with pan-y" );
+
+ // switch style to pan-x
+ document.body.style.touchAction = "pan-x";
+ ok(true, "Waiting for pan-x to propagate...");
+ await promiseAllPaintsDone(null, true);
+ await promiseOnlyApzControllerFlushed();
+
+ // drag the page up to scroll down by 50px, but it won't happen because pan-x
+ touchEndPromise = promiseTouchEnd(document.body);
+ ok(await synthesizeNativeTouchDrag(target, 10, 100, 0, -50),
+ "Synthesized native vertical drag (2), waiting for touch-end event...");
+ await touchEndPromise;
+ await promiseOnlyApzControllerFlushed();
+ checkScroll(0, 50, "After second vertical drag, with pan-x");
+
+ // drag the page left to scroll right by 50px
+ touchEndPromise = promiseTouchEnd(document.body);
+ ok(await synthesizeNativeTouchDrag(target, 100, 10, -50, 0),
+ "Synthesized horizontal drag (1), waiting for touch-end event...");
+ await touchEndPromise;
+ await promiseOnlyApzControllerFlushed();
+ checkScroll(50, 50, "After first horizontal drag, with pan-x");
+
+ // drag the page diagonally right/down to scroll up/left by 40px each axis;
+ // only the x-axis will actually scroll because pan-x
+ touchEndPromise = promiseTouchEnd(document.body);
+ ok(await synthesizeNativeTouchDrag(target, 10, 10, 40, 40),
+ "Synthesized diagonal drag (1), waiting for touch-end event...");
+ await touchEndPromise;
+ await promiseOnlyApzControllerFlushed();
+ checkScroll(10, 50, "After first diagonal drag, with pan-x");
+
+ // switch style back to pan-y
+ document.body.style.touchAction = "pan-y";
+ ok(true, "Waiting for pan-y to propagate...");
+ await promiseAllPaintsDone(null, true);
+ await promiseOnlyApzControllerFlushed();
+
+ // drag the page diagonally right/down to scroll up/left by 40px each axis;
+ // only the y-axis will actually scroll because pan-y
+ touchEndPromise = promiseTouchEnd(document.body);
+ ok(await synthesizeNativeTouchDrag(target, 10, 10, 40, 40),
+ "Synthesized diagonal drag (2), waiting for touch-end event...");
+ await touchEndPromise;
+ await promiseOnlyApzControllerFlushed();
+ checkScroll(10, 10, "After second diagonal drag, with pan-y");
+
+ // switch style to none
+ document.body.style.touchAction = "none";
+ ok(true, "Waiting for none to propagate...");
+ await promiseAllPaintsDone(null, true);
+ await promiseOnlyApzControllerFlushed();
+
+ // drag the page diagonally up/left to scroll down/right by 40px each axis;
+ // neither will scroll because of touch-action
+ touchEndPromise = promiseTouchEnd(document.body);
+ ok(await synthesizeNativeTouchDrag(target, 100, 100, -40, -40),
+ "Synthesized diagonal drag (3), waiting for touch-end event...");
+ await touchEndPromise;
+ await promiseOnlyApzControllerFlushed();
+ checkScroll(10, 10, "After third diagonal drag, with none");
+
+ document.body.style.touchAction = "manipulation";
+ ok(true, "Waiting for manipulation to propagate...");
+ await promiseAllPaintsDone(null, true);
+ await promiseOnlyApzControllerFlushed();
+
+ // drag the page diagonally up/left to scroll down/right by 40px each axis;
+ // both will scroll because of touch-action
+ touchEndPromise = promiseTouchEnd(document.body);
+ ok(await synthesizeNativeTouchDrag(target, 100, 100, -40, -40),
+ "Synthesized diagonal drag (4), waiting for touch-end event...");
+ await touchEndPromise;
+ await promiseOnlyApzControllerFlushed();
+ checkScroll(50, 50, "After fourth diagonal drag, with manipulation");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body style="touch-action: pan-y">
+ <div style="width: 5000px; height: 5000px; background-color: lightgreen;">
+ This div makes the page scrollable on both axes.<br>
+ This is the second line of text.<br>
+ This is the third line of text.<br>
+ This is the fourth line of text.
+ </div>
+ <!-- This fixed-position div remains in the same place relative to the browser chrome, so we
+ can use it as a targeting device for synthetic touch events. The body will move around
+ as we scroll, so we'd have to be constantly adjusting the synthetic drag coordinates
+ if we used that as the target element. -->
+ <div style="position:fixed; left: 10px; top: 10px; width: 1px; height: 1px" id="target"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_touch_action_complex.html b/gfx/layers/apz/test/mochitest/helper_touch_action_complex.html
new file mode 100644
index 0000000000..b8df34bfca
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_touch_action_complex.html
@@ -0,0 +1,137 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Complex touch-action test</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function checkScroll(target, x, y, desc) {
+ is(target.scrollLeft, x, desc + " - x axis");
+ is(target.scrollTop, y, desc + " - y axis");
+}
+
+async function resetConfiguration(config) {
+ // Cycle through all the configuration_X elements, setting them to display:none
+ // except for when X == config, in which case set it to display:block
+ var i = 0;
+ while (true) {
+ i++;
+ var element = document.getElementById("configuration_" + i);
+ if (element == null) {
+ if (i <= config) {
+ ok(false, "The configuration requested was not encountered!");
+ }
+ break;
+ }
+
+ if (i == config) {
+ element.style.display = "block";
+ } else {
+ element.style.display = "none";
+ }
+ }
+
+ // Also reset the scroll position on the scrollframe
+ var s = document.getElementById("scrollframe");
+ s.scrollLeft = 0;
+ s.scrollTop = 0;
+
+ await promiseAllPaintsDone();
+ await promiseApzFlushedRepaints();
+}
+
+async function test() {
+ var scrollframe = document.getElementById("scrollframe");
+
+ // Helper function for the tests below.
+ // Touch-pan configuration |configuration| towards scroll offset (dx, dy) with
+ // the pan touching down at (x, y). Check that the final scroll offset is
+ // (ex, ey). |desc| is some description string.
+ async function scrollAndCheck(configuration, x, y, dx, dy, ex, ey, desc) {
+ // Start with a clean slate
+ await resetConfiguration(configuration);
+ // Reverse the touch delta in order to scroll in the desired direction
+ dx = -dx;
+ dy = -dy;
+ // Do the pan
+ let touchEndPromise = promiseTouchEnd(document.body);
+ ok(await synthesizeNativeTouchDrag(scrollframe, x, y, dx, dy),
+ "Synthesized drag of (" + dx + ", " + dy + ") on configuration " + configuration);
+ await touchEndPromise;
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ // Check for expected scroll position
+ checkScroll(scrollframe, ex, ey, "configuration " + configuration + " " + desc);
+ }
+
+ // Test configuration_1, which contains two sibling elements that are
+ // overlapping. The touch-action from the second sibling (which is on top)
+ // should be used for the overlapping area.
+ await scrollAndCheck(1, 25, 75, 20, 0, 20, 0, "first element horizontal scroll");
+ await scrollAndCheck(1, 25, 75, 0, 50, 0, 0, "first element vertical scroll");
+ await scrollAndCheck(1, 75, 75, 50, 0, 0, 0, "overlap horizontal scroll");
+ await scrollAndCheck(1, 75, 75, 0, 50, 0, 50, "overlap vertical scroll");
+ await scrollAndCheck(1, 125, 75, 20, 0, 0, 0, "second element horizontal scroll");
+ await scrollAndCheck(1, 125, 75, 0, 50, 0, 50, "second element vertical scroll");
+
+ // Test configuration_2, which contains two overlapping elements with a
+ // parent/child relationship. The parent has pan-x and the child has pan-y,
+ // which means that panning on the parent should work horizontally only, and
+ // on the child no panning should occur at all.
+ await scrollAndCheck(2, 125, 125, 50, 50, 0, 0, "child scroll");
+ await scrollAndCheck(2, 75, 75, 50, 50, 0, 0, "overlap scroll");
+ await scrollAndCheck(2, 25, 75, 0, 50, 0, 0, "parent vertical scroll");
+ await scrollAndCheck(2, 75, 25, 50, 0, 50, 0, "parent horizontal scroll");
+
+ // Test configuration_3, which is the same as configuration_2, except the child
+ // has a rotation transform applied. This forces the event regions on the two
+ // elements to be built separately and then get merged.
+ await scrollAndCheck(3, 125, 125, 50, 50, 0, 0, "child scroll");
+ await scrollAndCheck(3, 75, 75, 50, 50, 0, 0, "overlap scroll");
+ await scrollAndCheck(3, 25, 75, 0, 50, 0, 0, "parent vertical scroll");
+ await scrollAndCheck(3, 75, 25, 50, 0, 50, 0, "parent horizontal scroll");
+
+ // Test configuration_4 has two elements, one above the other, not overlapping,
+ // and the second element is a child of the first. The parent has pan-x, the
+ // child has pan-y, but that means panning horizontally on the parent should
+ // work and panning in any direction on the child should not do anything.
+ await scrollAndCheck(4, 75, 75, 50, 50, 50, 0, "parent diagonal scroll");
+ await scrollAndCheck(4, 75, 150, 50, 50, 0, 0, "child diagonal scroll");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div id="scrollframe" style="width: 300px; height: 300px; overflow:scroll">
+ <div id="scrolled_content" style="width: 1000px; height: 1000px; background-color: green">
+ </div>
+ <div id="configuration_1" style="display:none; position: relative; top: -1000px">
+ <div style="touch-action: pan-x; width: 100px; height: 100px; background-color: blue"></div>
+ <div style="touch-action: pan-y; width: 100px; height: 100px; position: relative; top: -100px; left: 50px; background-color: yellow"></div>
+ </div>
+ <div id="configuration_2" style="display:none; position: relative; top: -1000px">
+ <div style="touch-action: pan-x; width: 100px; height: 100px; background-color: blue">
+ <div style="touch-action: pan-y; width: 100px; height: 100px; position: relative; top: 50px; left: 50px; background-color: yellow"></div>
+ </div>
+ </div>
+ <div id="configuration_3" style="display:none; position: relative; top: -1000px">
+ <div style="touch-action: pan-x; width: 100px; height: 100px; background-color: blue">
+ <div style="touch-action: pan-y; width: 100px; height: 100px; position: relative; top: 50px; left: 50px; background-color: yellow; transform: rotate(90deg)"></div>
+ </div>
+ </div>
+ <div id="configuration_4" style="display:none; position: relative; top: -1000px">
+ <div style="touch-action: pan-x; width: 100px; height: 100px; background-color: blue">
+ <div style="touch-action: pan-y; width: 100px; height: 100px; position: relative; top: 125px; background-color: yellow"></div>
+ </div>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_touch_action_ordering_block.html b/gfx/layers/apz/test/mochitest/helper_touch_action_ordering_block.html
new file mode 100644
index 0000000000..ca593c0db5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_touch_action_ordering_block.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Touch-action with sorted element</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+ async function test() {
+ var target = document.getElementById("target");
+ let touchEndPromise = promiseTouchEnd(document.body);
+
+ // drag the page up to scroll down by 50px
+ ok(await synthesizeNativeTouchDrag(target, 10, 100, 0, -50),
+ "Synthesized native vertical drag, waiting for touch-end event...");
+ await touchEndPromise;
+
+ await promiseOnlyApzControllerFlushed();
+
+ is(window.scrollX, 0, "X scroll offset didn't change");
+ is(window.scrollY, 50, "Y scroll offset changed");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone);
+ </script>
+</head>
+<body style="border: solid 1px green">
+ <div id="spacer" style="height: 2000px">
+ <div style="width:200px; height:200px; background-color:blue">
+ <span id="target" style="display:inline-block; width:200px; height:200px; background-color:red;"></span>
+ </div>
+ <div style="width:200px; height:200px; background-color:orange; touch-action:none; margin-top:-200px;"></div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_touch_action_ordering_zindex.html b/gfx/layers/apz/test/mochitest/helper_touch_action_ordering_zindex.html
new file mode 100644
index 0000000000..5b0d57a18d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_touch_action_ordering_zindex.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Touch-action with sorted element</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+ async function test() {
+ var target = document.getElementById("target");
+ let touchEndPromise = promiseTouchEnd(document.body);
+
+ // drag the page up to scroll down by 50px
+ ok(await synthesizeNativeTouchDrag(target, 10, 100, 0, -50),
+ "Synthesized native vertical drag, waiting for touch-end event...");
+ await touchEndPromise;
+
+ await promiseOnlyApzControllerFlushed();
+
+ is(window.scrollX, 0, "X scroll offset didn't change");
+ is(window.scrollY, 50, "Y scroll offset changed");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone);
+ </script>
+</head>
+<body style="border: solid 1px green">
+ <div id="spacer" style="height:2000px">
+ <div id="target" style="width:200px; height:200px; background-color:blue;"></div>
+ <div style="position: relative; width:200px; height:200px; background-color:red; touch-action:none; margin-top:-200px; z-index: -1;"></div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_touch_action_regions.html b/gfx/layers/apz/test/mochitest/helper_touch_action_regions.html
new file mode 100644
index 0000000000..6a8a09e55a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_touch_action_regions.html
@@ -0,0 +1,345 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test to ensure APZ doesn't always wait for touch-action</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function failure(e) {
+ ok(false, "This event listener should not have triggered: " + e.type);
+}
+
+function listener(callback) {
+ return function(e) {
+ ok(e.type == "touchstart", "The touchstart event handler was triggered after snapshotting completed");
+ setTimeout(callback, 0);
+ };
+}
+
+// This helper function provides a way for the child process to synchronously
+// check how many touch events the chrome process main-thread has processed. This
+// function can be called with three values: 'start', 'report', and 'end'.
+// The 'start' invocation sets up the listeners, and should be invoked before
+// the touch events of interest are generated. This should only be called once.
+// This returns true on success, and false on failure.
+// The 'report' invocation can be invoked multiple times, and returns an object
+// (in JSON string format) containing the counters.
+// The 'end' invocation tears down the listeners, and should be invoked once
+// at the end to clean up. Returns true on success, false on failure.
+function chromeTouchEventCounter(operation) {
+ function chromeProcessCounter() {
+ /* eslint-env mozilla/chrome-script */
+ const PREFIX = "apz:ctec:";
+
+ const LISTENERS = {
+ "start": function() {
+ var topWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!topWin) {
+ topWin = Services.wm.getMostRecentWindow("navigator:geckoview");
+ }
+ if (typeof topWin.eventCounts != "undefined") {
+ dump("Found pre-existing eventCounts object on the top window!\n");
+ return false;
+ }
+ topWin.eventCounts = { "touchstart": 0, "touchmove": 0, "touchend": 0 };
+ topWin.counter = function(e) {
+ topWin.eventCounts[e.type]++;
+ };
+
+ topWin.addEventListener("touchstart", topWin.counter, { passive: true });
+ topWin.addEventListener("touchmove", topWin.counter, { passive: true });
+ topWin.addEventListener("touchend", topWin.counter, { passive: true });
+
+ return true;
+ },
+
+ "report": function() {
+ var topWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!topWin) {
+ topWin = Services.wm.getMostRecentWindow("navigator:geckoview");
+ }
+ return JSON.stringify(topWin.eventCounts);
+ },
+
+ "end": function() {
+ for (let [msg, func] of Object.entries(LISTENERS)) {
+ Services.ppmm.removeMessageListener(PREFIX + msg, func);
+ }
+
+ var topWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!topWin) {
+ topWin = Services.wm.getMostRecentWindow("navigator:geckoview");
+ }
+ if (typeof topWin.eventCounts == "undefined") {
+ dump("The eventCounts object was not found on the top window!\n");
+ return false;
+ }
+ topWin.removeEventListener("touchstart", topWin.counter);
+ topWin.removeEventListener("touchmove", topWin.counter);
+ topWin.removeEventListener("touchend", topWin.counter);
+ delete topWin.counter;
+ delete topWin.eventCounts;
+ return true;
+ },
+ };
+
+ for (let [msg, func] of Object.entries(LISTENERS)) {
+ Services.ppmm.addMessageListener(PREFIX + msg, func);
+ }
+ }
+
+ if (typeof chromeTouchEventCounter.chromeHelper == "undefined") {
+ // This is the first time chromeTouchEventCounter is being called; do initialization
+ chromeTouchEventCounter.chromeHelper = SpecialPowers.loadChromeScript(chromeProcessCounter);
+ ApzCleanup.register(function() { chromeTouchEventCounter.chromeHelper.destroy(); });
+ }
+
+ return SpecialPowers.Services.cpmm.sendSyncMessage(`apz:ctec:${operation}`, "")[0];
+}
+
+// Simple wrapper that waits until the chrome process has seen |count| instances
+// of the |eventType| event. Returns true on success, and false if 10 seconds
+// go by without the condition being satisfied.
+function waitFor(eventType, count) {
+ var start = Date.now();
+ while (JSON.parse(chromeTouchEventCounter("report"))[eventType] != count) {
+ if (Date.now() - start > 10000) {
+ // It's taking too long, let's abort
+ return false;
+ }
+ }
+ return true;
+}
+
+function RunAfterProcessedQueuedInputEvents(aCallback) {
+ let tm = SpecialPowers.Services.tm;
+ tm.dispatchToMainThread(aCallback, SpecialPowers.Ci.nsIRunnablePriority.PRIORITY_INPUT_HIGH);
+}
+
+var scrollerPosition;
+async function getScrollerPosition() {
+ const scroller = document.getElementById("scroller");
+ scrollerPosition = await coordinatesRelativeToScreen({
+ offsetX: 0,
+ offsetY: 0,
+ target: scroller,
+ });
+}
+
+function* test(testDriver) {
+ // The main part of this test should run completely before the child process'
+ // main-thread deals with the touch event, so check to make sure that happens.
+ document.body.addEventListener("touchstart", failure, { passive: true });
+
+ // What we want here is to synthesize all of the touch events (from this code in
+ // the child process), and have the chrome process generate and process them,
+ // but not allow the events to be dispatched back into the child process until
+ // later. This allows us to ensure that the APZ in the chrome process is not
+ // waiting for the child process to send notifications upon processing the
+ // events. If it were doing so, the APZ would block and this test would fail.
+
+ // In order to actually implement this, we call the synthesize functions with
+ // a async callback in between. The synthesize functions just queue up a
+ // runnable on the child process main thread and return immediately, so with
+ // the async callbacks, the child process main thread queue looks like
+ // this after we're done setting it up:
+ // synthesizeTouchStart
+ // callback testDriver
+ // synthesizeTouchMove
+ // callback testDriver
+ // ...
+ // synthesizeTouchEnd
+ // callback testDriver
+ //
+ // If, after setting up this queue, we yield once, the first synthesization and
+ // callback will run - this will send a synthesization message to the chrome
+ // process, and return control back to us right away. When the chrome process
+ // processes with the synthesized event, it will dispatch the DOM touch event
+ // back to the child process over IPC, which will go into the end of the child
+ // process main thread queue, like so:
+ // synthesizeTouchStart (done)
+ // invoke testDriver (done)
+ // synthesizeTouchMove
+ // invoke testDriver
+ // ...
+ // synthesizeTouchEnd
+ // invoke testDriver
+ // handle DOM touchstart <-- touchstart goes at end of queue
+ //
+ // As we continue yielding one at a time, the synthesizations run, and the
+ // touch events get added to the end of the queue. As we yield, we take
+ // snapshots in the chrome process, to make sure that the APZ has started
+ // scrolling even though we know we haven't yet processed the DOM touch events
+ // in the child process yet.
+ //
+ // Note that the "async callback" we use here is SpecialPowers.tm.dispatchToMainThread
+ // with priority = input, because nothing else does exactly what we want:
+ // - setTimeout(..., 0) does not maintain ordering, because it respects the
+ // time delta provided (i.e. the callback can jump the queue to meet its
+ // deadline).
+ // - SpecialPowers.spinEventLoop and SpecialPowers.executeAfterFlushingMessageQueue
+ // are not e10s friendly, and can get arbitrarily delayed due to IPC
+ // round-trip time.
+ // - SimpleTest.executeSoon has a codepath that delegates to setTimeout, so
+ // is less reliable if it ever decides to switch to that codepath.
+ // - SpecialPowers.executeSoon dispatches a task to main thread. However,
+ // normal runnables may be preempted by input events and be executed in an
+ // unexpected order.
+
+ // Also note that this test is intentionally kept as a yield-style test using
+ // the runContinuation helper, even though all other similar tests have since
+ // been migrated to using async/await and Promise-based architectures. This is
+ // because yield and async/await have different semantics with respect to
+ // timing, and this test requires very specific timing behaviour (as described
+ // above).
+
+ // The other problem we need to deal with is the asynchronicity in the chrome
+ // process. That is, we might request a snapshot before the chrome process has
+ // actually synthesized the event and processed it. To guard against this, we
+ // register a thing in the chrome process that counts the touch events that
+ // have been dispatched, and poll that thing synchronously in order to make
+ // sure we only snapshot after the event in question has been processed.
+ // That's what the chromeTouchEventCounter business is all about. The sync
+ // polling looks bad but in practice only ends up needing to poll once or
+ // twice before the condition is satisfied, and as an extra precaution we add
+ // a time guard so it fails after 10s of polling.
+
+ // So, here we go...
+
+ // Set up the chrome process touch listener
+ ok(chromeTouchEventCounter("start"), "Chrome touch counter registered");
+
+ // Set up the child process events and callbacks
+ var scroller = document.getElementById("scroller");
+ var utils = utilsForTarget(window);
+ utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT,
+ scrollerPosition.x + 10, scrollerPosition.y + 110,
+ 1, 90, null);
+ RunAfterProcessedQueuedInputEvents(testDriver);
+ for (let i = 1; i < 10; i++) {
+ utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT,
+ scrollerPosition.x + 10,
+ scrollerPosition.y + 110 - (i * 10),
+ 1, 90, null);
+ RunAfterProcessedQueuedInputEvents(testDriver);
+ }
+ utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE,
+ scrollerPosition.x + 10,
+ scrollerPosition.y + 10,
+ 1, 90, null);
+ RunAfterProcessedQueuedInputEvents(testDriver);
+ ok(true, "Finished setting up event queue");
+
+ // Get our baseline snapshot
+ var rect = rectRelativeToScreen(scroller);
+ var lastSnapshot = getSnapshot(rect);
+ ok(true, "Got baseline snapshot");
+ var numDifferentSnapshotPairs = 0;
+
+ yield; // this will tell the chrome process to synthesize the touchstart event
+ // and then we wait to make sure it got processed:
+ ok(waitFor("touchstart", 1), "Touchstart processed in chrome process");
+
+ // Loop through the touchmove events
+ for (let i = 1; i < 10; i++) {
+ yield;
+ ok(waitFor("touchmove", i), "Touchmove processed in chrome process");
+
+ // Take a snapshot after each touch move event. This forces
+ // a composite each time, even we don't get a vsync in this
+ // interval.
+ var snapshot = getSnapshot(rect);
+ if (lastSnapshot != snapshot) {
+ numDifferentSnapshotPairs += 1;
+ }
+ lastSnapshot = snapshot;
+ }
+
+ // Check that the snapshot has changed since the baseline, indicating
+ // that the touch events caused async scrolling. Note that, since we
+ // orce a composite after each touch event, even if there is a frame
+ // of delay between APZ processing a touch event and the compositor
+ // applying the async scroll (bug 1375949), by the end of the gesture
+ // the snapshot should have changed.
+ ok(numDifferentSnapshotPairs > 0,
+ "The number of different snapshot pairs was " + numDifferentSnapshotPairs);
+
+ // Wait for the touchend as well, to clear all pending testDriver resumes
+ yield;
+ ok(waitFor("touchend", 1), "Touchend processed in chrome process");
+
+ // Clean up the chrome process hooks
+ chromeTouchEventCounter("end");
+
+ // Now we are going to release our grip on the child process main thread,
+ // so that all the DOM events that were queued up can be processed. We
+ // register a touchstart listener to make sure this happens.
+ document.body.removeEventListener("touchstart", failure);
+ var listenerFunc = listener(testDriver);
+ document.body.addEventListener("touchstart", listenerFunc, { passive: true });
+ dump("done registering listener, going to yield\n");
+ yield;
+ document.body.removeEventListener("touchstart", listenerFunc);
+}
+
+// Despite what this function name says, this does not *directly* run the
+// provided continuation testFunction. Instead, it returns a function that
+// can be used to run the continuation. The extra level of indirection allows
+// it to be more easily added to a promise chain, like so:
+// waitUntilApzStable().then(runContinuation(myTest));
+function runContinuation(testFunction) {
+ return function() {
+ return new Promise(function(resolve, reject) {
+ var testContinuation = null;
+
+ function driveTest() {
+ if (!testContinuation) {
+ testContinuation = testFunction(driveTest);
+ }
+ var ret = testContinuation.next();
+ if (ret.done) {
+ resolve();
+ }
+ }
+
+ try {
+ driveTest();
+ } catch (ex) {
+ ok(
+ false,
+ "APZ test continuation failed with exception: " + ex
+ );
+ }
+ });
+ };
+}
+
+if (SpecialPowers.isMainProcess()) {
+ // This is probably android, where everything is single-process. The
+ // test structure depends on e10s, so the test won't run properly on
+ // this platform. Skip it
+ ok(true, "Skipping test because it is designed to run from the content process");
+ subtestDone();
+} else {
+ waitUntilApzStable()
+ .then(async () => { await getScrollerPosition(); })
+ .then(runContinuation(test))
+ .then(subtestDone, subtestFailed);
+}
+
+ </script>
+</head>
+<body>
+ <div id="scroller" style="width: 400px; height: 400px; overflow: scroll; touch-action: pan-y">
+ <div style="width: 200px; height: 200px; background-color: lightgreen;">
+ This is a colored div that will move on the screen as the scroller scrolls.
+ </div>
+ <div style="width: 1000px; height: 1000px; background-color: lightblue">
+ This is a large div to make the scroller scrollable.
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_touch_action_zero_opacity_bug1500864.html b/gfx/layers/apz/test/mochitest/helper_touch_action_zero_opacity_bug1500864.html
new file mode 100644
index 0000000000..d31483f4f6
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_touch_action_zero_opacity_bug1500864.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Touch-action on a zero-opacity element</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+async function test() {
+ var target = document.getElementById("target");
+
+ let touchEndPromise = promiseTouchEnd(document.body);
+
+ // drag the page up to scroll down by 50px
+ ok(await synthesizeNativeTouchDrag(target, 10, 100, 0, -50),
+ "Synthesized native vertical drag, waiting for touch-end event...");
+ await touchEndPromise;
+
+ await promiseOnlyApzControllerFlushed();
+
+ is(window.scrollX, 0, "X scroll offset didn't change");
+ is(window.scrollY, 0, "Y scroll offset didn't change");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body style="border: solid 1px green">
+ <div id="spacer" style="height: 2000px">
+ Inside the black border is a zero-opacity touch-action none.
+ <div id="border" style="border: solid 1px black">
+ <div style="opacity: 0; height: 300px;">
+ <div style="transform:translate(0px)">
+ <div id="target" style="height: 300px; touch-action: none">this text shouldn't be visible</div>
+ </div>
+ </div>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_touch_drag_root_scrollbar.html b/gfx/layers/apz/test/mochitest/helper_touch_drag_root_scrollbar.html
new file mode 100644
index 0000000000..018ef78087
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_touch_drag_root_scrollbar.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Touch Drag on the viewport's scrollbar</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ .content {
+ width: 1000px;
+ height: 5000px;
+ }
+ </style>
+ <script type="text/javascript">
+
+async function test() {
+ let transformEndPromise = promiseTransformEnd();
+ let scrollbarPresent = await promiseVerticalScrollbarTouchDrag(window, 20);
+ if (!scrollbarPresent) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ await transformEndPromise;
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ // After dragging the scrollbar 20px on a 1000px-high viewport, we should
+ // have scrolled approx 2% of the 5000px high content. There might have been
+ // scroll arrows and such so let's just have a minimum bound of 50px to be safe.
+ ok(window.scrollY > 50, "Scrollbar drag resulted in a vertical scroll position of " + window.scrollY);
+
+ // Check that we did not get spurious horizontal scrolling, as we might if the
+ // drag gesture is mishandled by content as a select-drag rather than a scrollbar
+ // drag.
+ is(window.scrollX, 0, "Scrollbar drag resulted in a horizontal scroll position of " + window.scrollX);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div class="content">Some content to ensure the root scrollframe is scrollable</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_touch_synthesized_mouseevents.html b/gfx/layers/apz/test/mochitest/helper_touch_synthesized_mouseevents.html
new file mode 100644
index 0000000000..3930cec3c3
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_touch_synthesized_mouseevents.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+html, body { margin: 0; }
+
+#scrollable {
+ height: 50vh;
+ width: 50vw;
+ background: yellow;
+ overflow: scroll;
+}
+
+#scrollabletarget {
+ height: 200vh;
+ width: 200vh;
+ background: green;
+}
+
+#scrollabletarget:active {
+ background: red;
+}
+
+#notscrollable {
+ height: 50vh;
+ width: 50vw;
+ background: green;
+}
+
+#notscrollable:active {
+ background: red;
+}
+ </style>
+</head>
+<body>
+ <div id="scrollable">
+ <div id="scrollabletarget">
+ </div>
+ </div>
+ <div id="notscrollable">
+ </div>
+</body>
+<script>
+
+const searchParams = new URLSearchParams(location.search);
+
+async function test() {
+ let activeQuery = null;
+ let targetElem = null;
+
+ switch (searchParams.get("scrollable")) {
+ case "true":
+ activeQuery = "#scrollabletarget:active"
+ targetElem = scrollabletarget
+ break;
+ case "false":
+ activeQuery = "#notscrollable:active"
+ targetElem = notscrollable;
+ break;
+ default:
+ ok(false, "Unsupported scrollable value: " + searchParams.get("scrollable"));
+ break;
+ }
+
+ // Create a promise for each of the expected mouse events that are
+ // synthesized for a tap gesture
+ let mouseEventPromises = [
+ promiseOneEvent(targetElem, "mousemove"),
+ promiseOneEvent(targetElem, "mousedown"),
+ promiseOneEvent(targetElem, "mouseup"),
+ promiseOneEvent(targetElem, "click"),
+ ];
+
+ // Perform a tap gesture
+ await synthesizeNativeTap(targetElem, 50, 50);
+
+ // Set a timeout that will fail the test if the synthesized events
+ // are not immediately dispatched.
+ let failTimeout = setTimeout(() => {
+ ok(false, "The synthesized mouse events should be emitted in a timely manner");
+ }, 45000);
+
+ // The value of ui.touch_activation.duration_ms should be set to
+ // a large value. If we did not delay sending the synthesized
+ // mouse events, this test will not timeout.
+ await Promise.all(mouseEventPromises);
+
+ clearTimeout(failTimeout);
+
+ isnot(document.querySelector(activeQuery), null,
+ "Target element should still be active due to the large activation duration");
+}
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+ </script>
+</head>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_touchpad_pinch_and_pan.html b/gfx/layers/apz/test/mochitest/helper_touchpad_pinch_and_pan.html
new file mode 100644
index 0000000000..24eb920a74
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_touchpad_pinch_and_pan.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Sanity check for Touchpad pinch zooming and panning</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var initial_resolution = await getResolution();
+ var initial_page_left = window.visualViewport.pageLeft;
+ var initial_page_top = window.visualViewport.pageTop;
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+ is(initial_page_left, 0,
+ "The initial_page_left is " + initial_page_left + ", which is correct");
+ is(initial_page_top, 0,
+ "The initial_page_top is " + initial_page_top + ", which is correct");
+ await pinchZoomInAndPanWithTouchpad();
+ // Flush state and get the resolution we're at now
+ await promiseApzFlushedRepaints();
+ let final_resolution = await getResolution();
+
+ let final_page_left = window.visualViewport.pageLeft;
+ let final_page_top = window.visualViewport.pageTop;
+ ok(final_resolution > initial_resolution,
+ "The final resolution (" + final_resolution + ") is greater after zooming in");
+ ok(final_page_left > 300,
+ "The final pageLeft (" + final_page_left + ") is greater than 300 after panning");
+ ok(final_page_top > 200,
+ "The final pageTop (" + final_page_top + ") is greater than 200 after panning");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ Here is some text to stare at as the test runs. It serves no functional
+ purpose, but gives you an idea of the zoom level. It's harder to tell what
+ the zoom level is when the page is just solid white.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_transform_end_on_keyboard_scroll.html b/gfx/layers/apz/test/mochitest/helper_transform_end_on_keyboard_scroll.html
new file mode 100644
index 0000000000..20bee3cefd
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_transform_end_on_keyboard_scroll.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/NativeKeyCodes.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+html, body { margin: 0; }
+
+body {
+ height: 10000px;
+}
+</style>
+
+<script>
+async function test() {
+ // Send a native key event which doesn't cause any scroll, so that now
+ // subsequent native key events will be able to be handled by APZ. See bug
+ // 1774519 about what happens without this event.
+ const UpArrowKeyCode = nativeArrowUpKey();
+ await new Promise(resolve => {
+ synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US,
+ UpArrowKeyCode, {} /* no modifier */,
+ "", "", resolve); });
+
+ // On test verify runs there's still a race condition where the next key event
+ // isn't handled by APZ since the focus sequence number hasn't yet been
+ // reflected to APZ, so we explicitly flush APZ state here.
+ await promiseApzFlushedRepaints();
+
+ const transformEndPromise = promiseTransformEnd();
+ const DownArrowKeyCode = nativeArrowDownKey();
+ await new Promise(resolve => {
+ synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US,
+ DownArrowKeyCode, {} /* no modifier */,
+ "", "", resolve); });
+ await transformEndPromise;
+ ok(true, "Got an APZ:TransformEnd ");
+}
+
+function isOnChaosMode() {
+ return SpecialPowers.Services.env.get("MOZ_CHAOSMODE");
+}
+
+if ((getPlatform() == "mac" || getPlatform() == "windows") &&
+ !isOnChaosMode()) {
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+} else {
+ ok(true, "Skipping test because native key events are not supported on " +
+ getPlatform());
+ subtestDone();
+}
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_transform_end_on_wheel_scroll.html b/gfx/layers/apz/test/mochitest/helper_transform_end_on_wheel_scroll.html
new file mode 100644
index 0000000000..af4f72cf44
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_transform_end_on_wheel_scroll.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+html, body { margin: 0; }
+
+body {
+ height: 10000px;
+}
+</style>
+
+<script>
+async function test() {
+ let transformEndPromise = promiseTransformEnd();
+ await promiseMoveMouseAndScrollWheelOver(document.documentElement, 100, 100);
+
+ await transformEndPromise;
+ ok(true, "Got an APZ:TransformEnd ");
+}
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_visual_scrollbars_pagescroll.html b/gfx/layers/apz/test/mochitest/helper_visual_scrollbars_pagescroll.html
new file mode 100644
index 0000000000..ae3025930f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_visual_scrollbars_pagescroll.html
@@ -0,0 +1,119 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Clicking on the scrollbar track in quick succession should scroll the right amount</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript">
+
+// A helper to synthesize a native mouse click on a scrollbar track,
+// and wait long enough such that subsequent ticking of the refresh
+// driver will progress any resulting scroll animation.
+// In particular, just `await promiseNativeMouseEventWithApz(...)`
+// is not enough, it waits for the synthesization messages to arrive
+// in the parent process, but the native events may still be in the
+// OS event queue. Instead, we need to wait for a synthesized event
+// to arrive at content. While we're synthesizing a "click", if the
+// target is a scrollbar the window only gets the "mousedown" and
+// "mouseup", not a "click". Waiting for the "mousedown" is not enough
+// (the "mouseup" can still be stuck in the event queue), so we wait
+// for "mouseup".
+async function promiseNativeMouseClickOnScrollbarTrack(anchor, xOffset, yOffset) {
+ await promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "click",
+ target: anchor,
+ offsetX: xOffset,
+ offsetY: yOffset,
+ eventTypeToWait: "mouseup"
+ });
+}
+
+async function test() {
+ var scroller = document.documentElement;
+ var verticalScrollbarWidth = window.innerWidth - scroller.clientWidth;
+
+ if (verticalScrollbarWidth == 0) {
+ ok(true, "Scrollbar width is zero on this platform, test is useless here");
+ return;
+ }
+
+ // The anchor is the fixed-pos div that we use to calculate coordinates to
+ // click on the scrollbar. That way we don't have to recompute coordinates
+ // as the page scrolls. The anchor is at the bottom-right corner of the
+ // content area.
+ var anchor = document.getElementById('anchor');
+
+ var xoffset = (verticalScrollbarWidth / 2);
+ // Get a y-coord near the bottom of the vertical scrollbar track. Assume the
+ // vertical thumb is near the top of the scrollback track (since scroll
+ // position starts off at zero) and won't get in the way. Also assume the
+ // down arrow button, if there is one, is square.
+ var yoffset = 0 - verticalScrollbarWidth - 5;
+
+ // Take control of the refresh driver
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ utils.advanceTimeAndRefresh(0);
+
+ // Click at the bottom of the scrollbar track to trigger a page-down kind of
+ // scroll. This should use "desktop zooming" scrollbar code which should
+ // trigger an APZ scroll animation.
+ await promiseNativeMouseClickOnScrollbarTrack(anchor, xoffset, yoffset);
+
+ // Run 1000 frames, that should be enough to let the scroll animation start
+ // and run to completion. We check that it scrolled at least half the visible
+ // height, since we expect about a full screen height minus a few lines.
+ for (let i = 0; i < 1000; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ await promiseOnlyApzControllerFlushed();
+
+ let pageScrollAmount = scroller.scrollTop;
+ ok(pageScrollAmount > scroller.clientHeight / 2,
+ `Scroll offset is ${pageScrollAmount}, should be near clientHeight ${scroller.clientHeight}`);
+
+ // Now we do two clicks in quick succession, but with a few frames in between
+ // to verify the scroll animation from the first click is active before the
+ // second click happens.
+ await promiseNativeMouseClickOnScrollbarTrack(anchor, xoffset, yoffset);
+ for (let i = 0; i < 5; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ await promiseOnlyApzControllerFlushed();
+ let curPos = scroller.scrollTop;
+ ok(curPos > pageScrollAmount, `Scroll offset has increased to ${curPos}`);
+ ok(curPos < pageScrollAmount * 2, "Second page-scroll is not yet complete");
+ await promiseNativeMouseClickOnScrollbarTrack(anchor, xoffset, yoffset);
+
+ // Run to completion and check that we are around 3x pageScrollAmount, with
+ // some allowance for fractional rounding.
+ for (let i = 0; i < 1000; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ await promiseOnlyApzControllerFlushed();
+ curPos = scroller.scrollTop;
+ ok(Math.abs(curPos - (pageScrollAmount * 3)) < 3,
+ `Final scroll offset ${curPos} is close to 3x${pageScrollAmount}`);
+
+ utils.restoreNormalRefresh();
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div style="position:fixed; bottom: 0; right: 0; width: 1px; height: 1px" id="anchor"></div>
+ <div style="height: 300vh; margin-bottom: 10000px; background-image: linear-gradient(red,blue)"></div>
+ The above div is sized to 3x screen height so the linear gradient is more steep in terms of
+ color/pixel. We only scroll a few pages worth so we don't need the gradient all the way down.
+ And then we use a bottom-margin to make the page really big so the scrollthumb is
+ relatively small, giving us lots of space to click on the scrolltrack.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_visual_smooth_scroll.html b/gfx/layers/apz/test/mochitest/helper_visual_smooth_scroll.html
new file mode 100644
index 0000000000..9d278ee23c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_visual_smooth_scroll.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, minimum-scale=1.0">
+ <title>Tests that the (internal) visual smooth scrolling API is not restricted to the layout scroll range</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ div {
+ position: absolute;
+ }
+ </style>
+</head>
+<body>
+ <div style="width: 100%; height: 200%; background-color: green"></div>
+ <div style="width: 100%; height: 100%; background-color: blue"></div>
+ <script type="application/javascript">
+ const utils = SpecialPowers.getDOMWindowUtils(window);
+
+ async function test() {
+ // Pick a destination to scroll to that's outside the layout scroll range
+ // but within the visual scroll range.
+ const destY = window.scrollMaxY + 100;
+
+ // Register a TransformEnd observer so we can tell when the smooth scroll
+ // animation stops.
+ let transformEndPromise = promiseTransformEnd();
+
+ // Use scrollToVisual() to smooth-scroll to the destination.
+ utils.scrollToVisual(0, destY, utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_SMOOTH);
+
+ // Wait for the TransformEnd.
+ await transformEndPromise;
+
+ // Give scroll offsets a chance to sync.
+ await promiseApzFlushedRepaints();
+
+ // Check that the visual viewport scrolled to the desired destination.
+ is(visualViewport.pageTop, destY,
+ "The visual viewport should have scrolled past the layout scroll range");
+ }
+
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_visualscroll_clamp_restore.html b/gfx/layers/apz/test/mochitest/helper_visualscroll_clamp_restore.html
new file mode 100644
index 0000000000..69c0590a56
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_visualscroll_clamp_restore.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, minimum-scale=1.0">
+<title>Tests scroll position is properly synchronized when visual position is temporarily clamped on the main thread</title>
+<script src="apz_test_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+.hoverthingy, button {
+ width: 100%;
+ height: 200px;
+ text-align: center;
+ border: solid 1px black;
+ background-color: white;
+}
+
+.hoverthingy:hover {
+ background-color: lightgray;
+}
+</style>
+<div id="filler" style="height: 5000px">This test runs automatically in automation. To run manually, follow the steps: 1. scroll all the way down</div>
+<div class="hoverthingy">3. move the mouse. this div should have a hover effect exactly when the mouse is on top of it</div>
+<button onclick="clampRestore()">2. click this button</div>
+<script>
+/* eslint-disable no-unused-vars */
+function clampRestore() {
+ // Shorten doc to clamp scroll position
+ let filler = document.getElementById('filler');
+ filler.style.height = '4800px';
+ // Force scroll position update
+ let scrollPos = document.scrollingElement.scrollTop;
+ // Restore height
+ filler.style.height = '5000px';
+}
+
+function getAsyncScrollOffset() {
+ let apzcTree = getLastApzcTree();
+ let rcd = findRcdNode(apzcTree);
+ if (rcd == null) {
+ return {x: -1, y: -1};
+ }
+ return parsePoint(rcd.asyncScrollOffset);
+}
+
+async function test() {
+ document.scrollingElement.scrollTop = document.scrollingElement.scrollTopMax;
+ await promiseApzFlushedRepaints();
+ clampRestore();
+ await promiseApzFlushedRepaints();
+ let apzScrollOffset = getAsyncScrollOffset();
+ dump(`Got apzScrollOffset ${JSON.stringify(apzScrollOffset)}\n`);
+ // The bug this test is exercising resulted in a situation where the
+ // main-thread scroll offset and the APZ scroll offset remained out of sync
+ // while in the steady state. This resulted mouse hover effects and clicks
+ // being offset from where the user visually saw the content/mouse. We
+ // check to make sure the scroll offset is in sync to ensure the bug is fixed.
+ is(apzScrollOffset.y, document.scrollingElement.scrollTop,
+ "RCD y-scroll should match between APZ and main thread");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_visualscroll_nonrcd_rsf.html b/gfx/layers/apz/test/mochitest/helper_visualscroll_nonrcd_rsf.html
new file mode 100644
index 0000000000..e905749a89
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_visualscroll_nonrcd_rsf.html
@@ -0,0 +1,87 @@
+<!DOCTYPE HTML>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, minimum-scale=1.0">
+<title>Tests that pending visual scroll positions on RSFs of non-RCDs get cleared properly</title>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<body>
+<iframe style="width: 300px; height: 300px" id="scroller"></iframe>
+<script>
+function populateScroller() {
+ let text = '<div id="line0">line 0</div><br>';
+ for (let i = 1; i < 100; i++) {
+ text += 'line ' + i + '<br>';
+ }
+ document.querySelector('#scroller').contentDocument.body.innerHTML = text;
+}
+
+function reconstructScroller() {
+ let scroller = document.querySelector('#scroller');
+ scroller.style.display = 'none';
+ /* eslint-disable no-unused-vars */
+ let dummyToForceFlush = scroller.scrollTop;
+ scroller.style.display = '';
+ dummyToForceFlush = scroller.scrollTop;
+}
+
+async function test() {
+ let scroller = document.querySelector('#scroller');
+ let subwin = scroller.contentWindow;
+
+ populateScroller();
+ subwin.scrollTo(0, 100);
+ is(subwin.scrollY, 100, 'Scroller scrolled down to y=100');
+
+ // let the visual scroll position round-trip through APZ
+ await promiseApzFlushedRepaints();
+
+ // frame reconstruction does a ScrollToVisual. The bug is that the pending
+ // visual scroll offset update never gets cleared even though the paint
+ // transaction should clear it.
+ reconstructScroller();
+ await promiseApzFlushedRepaints();
+
+ // Scroll back up to the top using APZ-side scrolling, and wait for the APZ
+ // wheel animation to complete and the final scroll position to get synced
+ // back to the main thread. The large -250 scroll delta required here is due
+ // to bug 1662487.
+ await promiseMoveMouseAndScrollWheelOver(subwin, 10, 10, true, -250);
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ for (let i = 0; i < 60; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ utils.restoreNormalRefresh();
+ await promiseApzFlushedRepaints();
+ is(subwin.scrollY, 0, 'Scroller scrolled up to y=0');
+
+ // Do a mouse-drag-selection. I couldn't find any simpler way to reproduce
+ // the problem.
+ const kMouseMovePixels = 10;
+ let promiseMouseMovesDone = new Promise((resolve) => {
+ let mouseDownX = 0;
+ subwin.document.documentElement.addEventListener('mousedown', (e) => {
+ dump(`Got mousedown at ${e.screenX}\n`);
+ mouseDownX = e.screenX;
+ });
+ subwin.document.documentElement.addEventListener('mousemove', (e) => {
+ // Mousemove events can get squashed together so we check the coord
+ // instead.
+ dump(`Got mousemove at ${e.screenX}\n`);
+ if (e.screenX - mouseDownX >= kMouseMovePixels) {
+ resolve();
+ }
+ });
+ });
+ let line0 = subwin.document.querySelector('#line0');
+ let dragFinisher = await promiseNativeMouseDrag(line0, 1, 5, kMouseMovePixels, 0, kMouseMovePixels);
+ await promiseMouseMovesDone;
+ await dragFinisher();
+
+ is(subwin.scrollY, 0, 'Scroller should remain at y=0');
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe.html b/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe.html
new file mode 100644
index 0000000000..79101b2ca5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe.html
@@ -0,0 +1,52 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test that wheel events on an unscrollable OOP iframe are handoff-ed</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ iframe {
+ height: 201px;
+ border: none;
+ }
+ </style>
+</head>
+<body>
+ <div id="iframe-container" style="overflow-y: scroll; height: 200px;">
+ <iframe src="https://example.com/tests/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe_child.html"></iframe>
+ </div>
+ <div style="height: 200vh;"></div>
+ <script type="application/javascript">
+async function test() {
+ const scrollContainer = document.querySelector("#iframe-container");
+ let scrollEventPromise = waitForScrollEvent(scrollContainer);
+
+ // Send a wheel event on the iframe.
+ const iframe = document.querySelector("iframe");
+ await synthesizeNativeWheel(iframe, 50, 50, 0, -50);
+ await scrollEventPromise;
+
+ // The wheel event should be handoff-ed to the parent scroll container.
+ is(scrollContainer.scrollTop, scrollContainer.scrollTopMax,
+ "The scroll position in the parent scroll container should be at the bottom");
+
+ // Make sure the wheel event wasn't handoff-ed to the root scroller.
+ is(window.scrollY, 0, "The root scroll position should be 0");
+
+ await promiseFrame();
+ scrollEventPromise = waitForScrollEvent(window);
+ // Send a wheel event on the iframe again.
+ await synthesizeNativeWheel(iframe, 50, 50, 0, -50);
+ await scrollEventPromise;
+
+ // Now it should be handoff-ed to the root scroller.
+ ok(window.scrollY > 0,
+ "The wheel event should have been handoff-ed to the root scroller");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe_child.html b/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe_child.html
new file mode 100644
index 0000000000..aca9dfdbd4
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe_child.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<style>
+html {
+ overflow: hidden;
+}
+body {
+ margin: 0px;
+ padding: 0px;
+}
+</style>
+<div>overflow: hidden on html</div>
diff --git a/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_non_scrollable_iframe.html b/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_non_scrollable_iframe.html
new file mode 100644
index 0000000000..22963459a0
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_non_scrollable_iframe.html
@@ -0,0 +1,113 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>scroll handoff on non scrollable iframe document with overscroll-behavior: none</title>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+</head>
+<style>
+iframe {
+ width: 500px;
+ height: 500px;
+}
+</style>
+<body>
+<iframe></iframe>
+<div style="height:1000vh"></div>
+<script>
+
+// This test runs twice, with a same origin iframe first then with a cross
+// origin iframe second.
+async function test(targetOrigin) {
+ const iframe = document.querySelector("iframe");
+ const targetURL =
+ SimpleTest.getTestFileURL("helper_empty.html")
+ .replace(window.location.origin, targetOrigin);
+ iframe.src = targetURL;
+
+ await new Promise(resolve => {
+ iframe.onload = resolve;
+ });
+
+ await SpecialPowers.spawn(iframe, [], async () => {
+ content.document.documentElement.style =
+ "overscroll-behavior-y: none; overflow-y: scroll;";
+
+ // Flush the style change.
+ content.document.documentElement.getBoundingClientRect();
+ // Make sure the style change reaches to APZ.
+ await content.window.wrappedJSObject.promiseApzFlushedRepaints();
+ });
+
+ let scrollEventPromise = new Promise(resolve => {
+ window.addEventListener("scroll", resolve, { once: true });
+ });
+
+ await synthesizeNativeWheel(iframe, 100, 100, 0, -10);
+ await scrollEventPromise;
+ await waitToClearOutAnyPotentialScrolls(window);
+
+ ok(window.scrollY > 0,
+ "Mouse wheel scrolling on an OOP iframe where the iframe document is " +
+ "not scrollable but has overscroll-behavior: none property should be " +
+ "handed off to the parent");
+
+ // Make sure the wheel scrolling has finished.
+ await waitToClearOutAnyPotentialScrolls(window);
+
+ // Make the iframe document scrollable and try to scroll up on the iframe
+ // document.
+ await SpecialPowers.spawn(iframe, [], async () => {
+ content.document.body.style = "height: 500vh;";
+
+ // Flush the style change.
+ content.document.documentElement.getBoundingClientRect();
+ // Make sure the style change reaches to APZ.
+ await content.window.wrappedJSObject.promiseApzFlushedRepaints();
+ });
+
+ const mousemoveEventPromise = SpecialPowers.spawn(iframe, [], async () => {
+ await new Promise(resolve => {
+ content.window.addEventListener("mousemove", resolve, { once: true });
+ });
+ });
+
+ // Make sure the above event listener is registered.
+ await SpecialPowers.spawn(iframe, [], async () => {
+ await content.window.wrappedJSObject.promiseApzFlushedRepaints();
+ });
+
+ const scrollPos = window.scrollY;
+
+ // Send a mousemove event on the iframe to finish the last wheel event block.
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "mousemove",
+ target: iframe,
+ offsetX: 100,
+ offsetY: 100 + scrollPos,
+ });
+ await mousemoveEventPromise;
+
+ // Try to scroll up by a new wheel event on the iframe.
+ await synthesizeNativeWheel(iframe, 100, 100 + scrollPos, 0, 10);
+ await waitToClearOutAnyPotentialScrolls(window);
+
+ // The wheel event should not be handed off to the root scroller since the
+ // iframe document has `overscroll-behavior-y: none`.
+ is(window.scrollY, scrollPos,
+ "The root scroller's position should never be changed");
+
+ // Restore the root scroll position for the next test case.
+ window.scrollTo(0, 0);
+}
+
+waitUntilApzStable()
+.then(async () => test(window.location.origin))
+.then(async () => test("http://example.com/"))
+.then(subtestDone, subtestFailed);
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe.html b/gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe.html
new file mode 100644
index 0000000000..bdcef59229
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test cross origin fission iframes get displayport that covers whole width</title>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+</head>
+<body>
+ <iframe id="iframe" style="width:700px; height:150px; border:2px solid blue;"></iframe>
+<script>
+
+ window.addEventListener("message", event => {
+ if (event.data == "wereDone") {
+ subtestDone();
+ } else if (event.data.x != undefined && event.data.y != undefined && event.data.width != undefined && event.data.height != undefined) {
+ info("event.data " + event.data.x + " " + event.data.y + " " + event.data.width + " " + event.data.height);
+ // 683 = 700 width of iframe minus maximum width of scrollbars seen on try
+ ok(event.data.width >= 683, "dp is wide enough");
+ }
+ });
+
+ function test() {
+ document.getElementById("iframe").setAttribute("src", "http://example.org/tests/gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe_child.html");
+ }
+
+ waitUntilApzStable().then(test);
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe_child.html b/gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe_child.html
new file mode 100644
index 0000000000..edf3ad4728
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe_child.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<html id="helper_wide_crossorigin_iframe_child_docelement">
+<meta charset=utf-8>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+<style>
+html {
+ border: 5px solid lime;
+ background: yellow;
+ box-sizing: border-box;
+ overflow-y: scroll;
+}
+</style>
+<script>
+ // negative means keep sending forever
+ // we flip this to 10 when we hit onload, so that we send several
+ // before load and some after.
+ let numMoreTimesToSend = -1;
+ function sendDpToParent() {
+ if (numMoreTimesToSend > 0) {
+ numMoreTimesToSend--;
+ }
+ if (numMoreTimesToSend == 0) {
+ clearInterval(intervalId);
+ parent.postMessage("wereDone", "*");
+ return;
+ }
+ let dp = getLastContentDisplayportFor("helper_wide_crossorigin_iframe_child_docelement", /* expectPainted = */ false);
+ if (dp != null) {
+ info("result " + dp.x + " " + dp.y + " " + dp.width + " " + dp.height);
+
+ parent.postMessage(dp, "*");
+ } else {
+ info("no dp yet");
+ }
+ }
+
+ sendDpToParent();
+ setTimeout(sendDpToParent,0);
+
+ let intervalId = setInterval(sendDpToParent, 100);
+
+ addEventListener("MozAfterPaint", sendAndSetTimeout);
+ function sendAndSetTimeout() {
+ sendDpToParent();
+ setTimeout(sendDpToParent,0);
+ }
+
+ window.requestAnimationFrame(checkAndSendRaf);
+ function checkAndSendRaf() {
+ if (numMoreTimesToSend != 0) {
+ window.requestAnimationFrame(checkAndSendRaf);
+ }
+ sendDpToParent();
+ setTimeout(sendDpToParent,0);
+ }
+
+ window.onload = onloaded;
+ window.onDOMContentLoaded = sendDpToParent;
+ document.onreadystatechange = sendDpToParent;
+ document.onafterscriptexecute = sendDpToParent;
+ document.onbeforescriptexecute = sendDpToParent;
+ document.onvisibilitychange = sendDpToParent;
+ function onloaded() {
+ numMoreTimesToSend = 10;
+ sendDpToParent();
+ }
+
+</script>
+<div style="background: blue; height: 400vh;"></div>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_dynamic_toolbar_bug1828235.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_dynamic_toolbar_bug1828235.html
new file mode 100644
index 0000000000..160e1624e5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_dynamic_toolbar_bug1828235.html
@@ -0,0 +1,54 @@
+<!DOCTYPE>
+<html>
+
+<head>
+ <title>Bug 1828235 - Dynamic toolbar causes errant zoom on input focus for certain page heights</title>
+ <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1" />
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+</head>
+<style type="text/css">
+ .touch-none {
+ /* This size ensures the page height is taller than the viewport height without the dynamic toolbar
+ * but shorter than the viewport height with the dyanmic toolbar, which we need to trigger the bug. */
+ height: 98lvh;
+ margin: 0;
+ touch-action: none;
+ }
+</style>
+
+<body>
+ <div class="touch-none">
+ <input id="input1">
+ </div>
+ <script type="application/javascript">
+ async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ document.getElementById('input1').focus();
+ await waitToClearOutAnyPotentialScrolls(window);
+ await promiseApzFlushedRepaints();
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "focusing input1 did not change resolution " + resolution);
+
+ let transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ await waitToClearOutAnyPotentialScrolls(window);
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "zoomToFocusedInput input1 did not change resolution " + resolution);
+ }
+
+ SpecialPowers.getDOMWindowUtils(window).setDynamicToolbarMaxHeight(100);
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_fixed_bug1673511.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_fixed_bug1673511.html
new file mode 100644
index 0000000000..c63794fdb1
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_fixed_bug1673511.html
@@ -0,0 +1,42 @@
+<!DOCTYPE>
+<html>
+ <head>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Checking zoomToFocusedInput scrolls on position: fixed</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ </head>
+<body>
+<div>
+ <div style="z-index: 100; position: fixed; width: 300px; height: 300px; overflow-y: scroll;" id="container">
+ <div style="height: 1000px;">ABC</div>
+ <input type="text">
+ </div>
+ <!-- Leave additional room below the element so it can be scrolled to the center -->
+ <div style="height: 3000px;">ABC</div>
+</div>
+<script type="application/javascript">
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ let input = document.querySelector("input");
+ let container = document.querySelector("#container");
+ let originalScrollTop = container.scrollTop;
+
+ input.focus({ preventScroll: true });
+ await waitToClearOutAnyPotentialScrolls(window);
+ ok(container.scrollTop == originalScrollTop, "scroll position keeps top");
+
+ let waitForScroll = waitForScrollEvent(container);
+ utils.zoomToFocusedInput();
+ await waitForScroll;
+ await promiseApzFlushedRepaints();
+
+ ok(container.scrollTop > originalScrollTop, "scroll position isn't top");
+}
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_iframe-2.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_iframe-2.html
new file mode 100644
index 0000000000..68b2db35ca
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_iframe-2.html
@@ -0,0 +1,104 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100,initial-scale=0.4"/>
+ <title>Tests that zoomToFocusedInput in iframe works regardless whether cross-origin or not</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html {
+ /* To avoid bug 1865573 */
+ scrollbar-width: none;
+ }
+ iframe {
+ position: absolute;
+ top: 100px;
+ left: 100px;
+ border: none;
+ }
+ </style>
+</head>
+<body>
+<iframe width="500" height="500"></iframe>
+<script>
+async function setupIframe(aURL) {
+ const iframe = document.querySelector("iframe");
+ const iframeLoadPromise = promiseOneEvent(iframe, "load", null);
+ iframe.src = aURL;
+ await iframeLoadPromise;
+ info(`${aURL} loaded`);
+
+ await SpecialPowers.spawn(iframe, [], async () => {
+ await content.wrappedJSObject.waitUntilApzStable();
+ await SpecialPowers.contentTransformsReceived(content);
+ });
+}
+
+let initial_resolution;
+async function test(aTestFile) {
+ let iframeURL = SimpleTest.getTestFileURL(aTestFile);
+
+ // Load the test document in the same origin.
+ await setupIframe(iframeURL);
+
+ initial_resolution = await getResolution();
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+
+ const iframe = document.querySelector("iframe");
+ let transformEndPromise = promiseTransformEnd();
+ await SpecialPowers.spawn(iframe, [], async () => {
+ SpecialPowers.DOMWindowUtils.zoomToFocusedInput();
+ });
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+
+ const zoomedInState = cloneVisualViewport();
+
+ // Reset the scale to the initial value.
+ SpecialPowers.DOMWindowUtils.setResolutionAndScaleTo(initial_resolution);
+ await promiseApzFlushedRepaints();
+
+ // Now load the document in an OOP iframe.
+ iframeURL = iframeURL.replace(window.location.origin, "https://example.com");
+ await setupIframe(iframeURL);
+
+ transformEndPromise = promiseTransformEnd();
+ await SpecialPowers.spawn(iframe, [], async () => {
+ SpecialPowers.DOMWindowUtils.zoomToFocusedInput();
+ });
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+
+ compareVisualViewport(zoomedInState, cloneVisualViewport(), "zoomed-in state");
+}
+
+async function moveIframe() {
+ const iframe = document.querySelector("iframe");
+ iframe.style.top = "500vh";
+
+ // Scroll to the bottom to make the layout scroll offset non-zero.
+ window.scrollTo(0, document.documentElement.scrollHeight);
+ ok(window.scrollY > 0, "The root scroll position should be non-zero");
+
+ await SpecialPowers.spawn(iframe, [], async () => {
+ await SpecialPowers.contentTransformsReceived(content);
+ });
+}
+
+waitUntilApzStable()
+.then(async () => test("helper_zoomToFocusedInput_iframe_subframe.html?margin-top=200vh"))
+.then(async () => {
+ // Reset the scale to the initial value.
+ SpecialPowers.DOMWindowUtils.setResolutionAndScaleTo(initial_resolution);
+ await promiseApzFlushedRepaints();
+})
+// A test case where the layout scroll offset isn't zero.
+.then(async () => moveIframe())
+.then(async () => test("helper_zoomToFocusedInput_iframe_subframe.html"))
+.then(subtestDone, subtestFailed);
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_iframe.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_iframe.html
new file mode 100644
index 0000000000..d785af1399
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_iframe.html
@@ -0,0 +1,105 @@
+<!DOCTYPE>
+<html>
+ <head>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Checking zoomToFocusedInput scrolls that focused element is into iframe</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ </head>
+<body>
+<div style="height: 8000px;">ABC</div>
+<iframe style="height: 30em;"></iframe>
+</div>
+<!-- Leave additional room below the element so it can be scrolled to the center -->
+<div style="height: 1000px;">ABC</div>
+<script type="application/javascript">
+async function test() {
+ let isCrossOrigin = (location.search == "?cross-origin");
+ let iframeURL =
+ SimpleTest.getTestFileURL("helper_iframe_textarea.html")
+ if (isCrossOrigin) {
+ iframeURL.replace(window.location.origin, "https://example.com/");
+ }
+
+ let iframe = document.querySelector("iframe");
+ const iframeLoadPromise = promiseOneEvent(iframe, "load", null);
+ iframe.src = iframeURL;
+ await iframeLoadPromise;
+
+ await SpecialPowers.spawn(iframe, [], async () => {
+ await content.wrappedJSObject.waitUntilApzStable();
+ });
+
+ iframe.focus();
+
+ await SpecialPowers.spawn(iframe, [], async () => {
+ let textarea = content.document.querySelector("textarea");
+ for (let i = 0; i < 20; i++) {
+ textarea.value += "foo\n";
+ }
+ textarea.focus();
+ });
+
+ await SpecialPowers.spawn(iframe, [], async () => {
+ await content.wrappedJSObject.waitToClearOutAnyPotentialScrolls(content.window);
+ });
+
+ await SpecialPowers.spawn(iframe, [], async () => {
+ let textarea = content.document.querySelector("textarea");
+ textarea.setSelectionRange(0, 0);
+ });
+
+ window.scrollTo(0, 0);
+ await SpecialPowers.spawn(iframe, [], async () => {
+ await content.wrappedJSObject.waitToClearOutAnyPotentialScrolls(content.window);
+ });
+ is(0, window.scrollY, "scroll position is reset");
+
+ let transformEndPromise = promiseTransformEnd();
+ await SpecialPowers.spawn(iframe, [], async () => {
+ SpecialPowers.DOMWindowUtils.zoomToFocusedInput();
+ });
+ await promiseApzFlushedRepaints();
+
+ ok(window.scrollY > 0, "scroll position isn't top");
+ let iframeWindowScrollY = await SpecialPowers.spawn(iframe, [], () => {
+ return content.window.scrollY;
+ });
+ ok(iframeWindowScrollY > 0, "scroll position into iframe isn't top");
+ let prevPosY = window.scrollY;
+
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+
+ window.scrollTo(0, 0);
+ await SpecialPowers.spawn(iframe, [], async () => {
+ await content.wrappedJSObject.waitToClearOutAnyPotentialScrolls(content.window);
+ });
+ is(0, window.scrollY, "scroll position is reset");
+
+ SpecialPowers.spawn(iframe, [], async () => {
+ let textarea = content.document.querySelector("textarea");
+ textarea.setSelectionRange(textarea.value.length, textarea.value.length);
+ });
+
+ transformEndPromise = promiseTransformEnd();
+ await SpecialPowers.spawn(iframe, [], async () => {
+ SpecialPowers.DOMWindowUtils.zoomToFocusedInput();
+ });
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+
+ ok(window.scrollY > 0, "scroll position isn't top");
+ iframeWindowScrollY = await SpecialPowers.spawn(iframe, [], () => {
+ return content.window.scrollY;
+ });
+ ok(iframeWindowScrollY > 0, "scroll position into iframe isn't top");
+ ok(prevPosY < window.scrollY,
+ "scroll position is different from first line of textarea");
+}
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_iframe_subframe.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_iframe_subframe.html
new file mode 100644
index 0000000000..e06f3cb33e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_iframe_subframe.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<script src="apz_test_utils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<input id="target">
+<script>
+ const params = new URLSearchParams(location.search);
+ target.style.marginLeft = params.get("margin-left");
+ target.style.marginTop = params.get("margin-top");
+ target.focus();
+
+ // Silence SimpleTest warning about missing assertions by having it wait
+ // indefinitely. We don't need to give it an explicit finish because the
+ // entire window this test runs in will be closed after the main browser test
+ // finished.
+ SimpleTest.waitForExplicitFinish();
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_multiline.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_multiline.html
new file mode 100644
index 0000000000..ff5912dcee
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_multiline.html
@@ -0,0 +1,94 @@
+<!DOCTYPE>
+<html>
+ <head>
+ <title>Checking zoomToFocusedInput scrolls that focused non-input element is visible position</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ </head>
+<body>
+<div style="height: 8000px;">ABC</div>
+<div id="content">
+</div>
+<!-- Leave additional room below the element so it can be scrolled to the center -->
+<div style="height: 1000px;">ABC</div>
+<script type="application/javascript">
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ // contenteditable
+ let div = document.createElement("div");
+ div.setAttribute("contenteditable", "true");
+ for (let i = 0; i < 200; i++) {
+ div.innerHTML += "foo<br>";
+ }
+ div.innerHTML += "<span id=last>bar</span>";
+ document.getElementById("content").appendChild(div);
+
+ let selection = window.getSelection();
+ selection.collapse(div.firstChild, 0);
+ window.scrollTo(0, 0);
+ await waitToClearOutAnyPotentialScrolls(window);
+ is(0, window.scrollY, "scroll position is reset");
+ let transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+ ok(window.scrollY > 0, "scroll position isn't top");
+
+ let prevY = window.scrollY;
+
+ selection.collapse(document.getElementById("last").firstChild, 0);
+ window.scrollTo(0, 0);
+ await waitToClearOutAnyPotentialScrolls(window);
+ is(0, window.scrollY, "scroll position is reset");
+ transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ await promiseApzFlushedRepaints();
+ ok(prevY < window.scrollY, "scroll position is visibile position of caret");
+
+ await transformEndPromise;
+
+ document.getElementById("content").removeChild(div);
+
+ // <textarea> element
+ let textarea = document.createElement("textarea");
+ textarea.rows = 200;
+ for (let i = 0; i < 200; i++) {
+ textarea.value += "foo\n";
+ }
+ document.getElementById("content").appendChild(textarea);
+ textarea.focus();
+
+ await waitToClearOutAnyPotentialScrolls(window);
+
+ textarea.setSelectionRange(0, 0);
+ window.scrollTo(0, 0);
+ await waitToClearOutAnyPotentialScrolls(window);
+ is(0, window.scrollY, "scroll position is reset");
+ transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ await promiseApzFlushedRepaints();
+ ok(window.scrollY > 0, "scroll position isn't top");
+ prevY = window.scrollY;
+
+ await transformEndPromise;
+
+ textarea.setSelectionRange(textarea.value.length, textarea.value.length);
+ window.scrollTo(0, 0);
+ await waitToClearOutAnyPotentialScrolls(window);
+ is(0, window.scrollY, "scroll position is reset");
+ transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ await promiseApzFlushedRepaints();
+ ok(prevY < window.scrollY, "scroll position is visibile position of caret");
+
+ await transformEndPromise;
+
+ document.getElementById("content").removeChild(textarea);
+}
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nested_position_fixed.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nested_position_fixed.html
new file mode 100644
index 0000000000..053c17e29b
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nested_position_fixed.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Tests that zoomToFocuedInput doesn't reset the scroll position for nested position:fixed element</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ .container, textarea {
+ bottom: 10px;
+ height: 100px;
+ width: calc(100% - 30px);
+ position: fixed;
+ }
+ html {
+ height: 500vh;
+ }
+ </style>
+</head>
+<body>
+<div class="container">
+ <textarea></textarea>
+</div>
+<script>
+async function test() {
+ window.scrollTo(0, document.documentElement.scrollHeight);
+ const expectedScrollPosition = window.scrollY;
+
+ document.querySelector("textarea").focus();
+ is(window.scrollY, expectedScrollPosition);
+
+ const transformEndPromise = promiseTransformEnd();
+ SpecialPowers.DOMWindowUtils.zoomToFocusedInput();
+ await transformEndPromise;
+
+ is(window.scrollY, expectedScrollPosition);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom.html
new file mode 100644
index 0000000000..5edd181a2d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom.html
@@ -0,0 +1,39 @@
+<!DOCTYPE>
+<html>
+ <head>
+ <title>Checking zoomToFocusedInput does not zoom if meta viewport does not allow it</title>
+ <meta name="viewport" content="width=device-width, height=device-height, initial-scale=0.5 minimum-scale=0.5, maximum-scale=1, user-scalable=no" />
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ </head>
+<body>
+<input id="input1" type="text" style="border: 5px solid black"/>
+<script type="application/javascript">
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ document.getElementById('input1').focus();
+ await waitToClearOutAnyPotentialScrolls(window);
+ await promiseApzFlushedRepaints();
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "focusing input did not change resolution " + resolution);
+
+ let transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ await waitToClearOutAnyPotentialScrolls(window);
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "zoomToFocusedInput input did not change resolution " + resolution);
+}
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom_bug1738696.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom_bug1738696.html
new file mode 100644
index 0000000000..4320e391b7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom_bug1738696.html
@@ -0,0 +1,51 @@
+<!DOCTYPE>
+<html>
+ <head>
+ <title>Checking zoomToFocusedInput does not zoom is meta viewport does not allow it</title>
+ <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, minimum-scale=1, maximum-scale=1, user-scalable=no" />
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ </head>
+<body>
+<input id="input1" type="text" style="width:100%; height: 200px; border: 5px solid black"/>
+<script type="application/javascript">
+/* This test does not always exercise the bug it was written for, in the sense
+ that the test would fail without the corresponding patch. It seems as though
+ when hitting bug 1743602 the test does not exercise the bug. If bug 1743602
+ gets fixed hopefully this test will end up exercising the bug. A dev pixel
+ ratio of 1 seems to make it hard (but not impossible) for the test to
+ exercise the bug. This is what you get when running the emulator locally or
+ in our CI infrastructure. A dev pixel ratio of ~2.6 (23 appunits = 1 dev
+ pixel) seems to make it easier (but not 100%) for the test to exercise the
+ bug. This is what I got when running the test on a real phone locally (pixel 2).
+*/
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ document.getElementById('input1').focus();
+ await waitToClearOutAnyPotentialScrolls(window);
+ await promiseApzFlushedRepaints();
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "focusing input did not change resolution " + resolution);
+
+ let transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ await waitToClearOutAnyPotentialScrolls(window);
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "zoomToFocusedInput input did not change resolution " + resolution);
+}
+
+SpecialPowers.getDOMWindowUtils(window).setDynamicToolbarMaxHeight(300);
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_scroll.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_scroll.html
new file mode 100644
index 0000000000..4054b51657
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_scroll.html
@@ -0,0 +1,51 @@
+<!DOCTYPE>
+<html>
+ <head>
+ <title>Checking zoomToFocusedInput scrolls that focused input element is visible position</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ </head>
+<body>
+<div style="height: 8000px;">ABC</div>
+<input id="input1">
+<!-- Leave additional room below the element so it can be scrolled to the center -->
+<div style="height: 1000px;">ABC</div>
+<script type="application/javascript">
+async function test() {
+ is(0, window.scrollY, "scroll position starts at zero");
+ input1.focus();
+ await waitToClearOutAnyPotentialScrolls(window);
+ isnot(0, window.scrollY, "scroll position isn't top");
+ window.scrollTo(0, 0);
+ await waitToClearOutAnyPotentialScrolls(window);
+ is(0, window.scrollY, "scroll position is top");
+
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ let transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ isnot(0, window.scrollY, "scroll position isn't top");
+
+ // Test for bug 1669588: check that the zoom animation did not get
+ // cancelled by a main thread scroll position update triggered by
+ // the ScrollContentIntoView() operation performed by zoomToFocusedInput().
+
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+
+ // Check that the zoom animation performed additional scrolling
+ // beyond the ScrollContentIntoView(). The ScrollContentIntoView()
+ // just scrolls enough to bring `input1` into the viewport, while
+ // the zoom animation will scroll further to center it. To
+ // distinguish the two cases, check that we scrolled enough that
+ // the element's top is above the middle of the visual viewport.
+ let inputTop = input1.getBoundingClientRect().top;
+ inputTop -= window.visualViewport.offsetTop;
+ ok(inputTop < (window.visualViewport.height / 2),
+ "input was scrolled at least as far as the middle of the viewport");
+}
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_touch-action.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_touch-action.html
new file mode 100644
index 0000000000..bdb49f4b84
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_touch-action.html
@@ -0,0 +1,67 @@
+<!DOCTYPE>
+<html>
+ <head>
+ <title>Checking zoomToFocusedInput zooms if touch-action allows it</title>
+ <meta name="viewport" content="width=device-width, height=device-height, initial-scale=0.5 minimum-scale=0.5, maximum-scale=1" />
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ </head>
+ <style type="text/css">
+ .touch-none {
+ touch-action: none;
+ }
+ .touch-auto {
+ touch-action: auto;
+ }
+ </style>
+<body>
+ <div class="touch-none">
+ <input id="input1" type="text" style="border: 5px solid black">
+ </div>
+ <br>
+ <div class="touch-auto">
+ <input id="input2" type="text" style="border: 5px solid black">
+ </div>
+<script type="application/javascript">
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ document.getElementById('input1').focus();
+ await waitToClearOutAnyPotentialScrolls(window);
+ await promiseApzFlushedRepaints();
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "focusing input1 did not change resolution " + resolution);
+
+ let transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ await waitToClearOutAnyPotentialScrolls(window);
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "zoomToFocusedInput input1 did not change resolution " + resolution);
+
+ document.getElementById('input2').focus();
+ await waitToClearOutAnyPotentialScrolls(window);
+ await promiseApzFlushedRepaints();
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "focusing input2 did not change resolution " + resolution);
+
+ transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ await waitToClearOutAnyPotentialScrolls(window);
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+ resolution = await getResolution();
+ ok(resolution != prev_resolution, "zoomToFocusedInput input2 changed resolution " + resolution);
+}
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_zoom.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_zoom.html
new file mode 100644
index 0000000000..c74fe521b4
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_zoom.html
@@ -0,0 +1,39 @@
+<!DOCTYPE>
+<html>
+ <head>
+ <title>Checking zoomToFocusedInput zooms if meta viewport allows it</title>
+ <meta name="viewport" content="width=device-width, height=device-height, initial-scale=0.5 minimum-scale=0.5, maximum-scale=1" />
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ </head>
+<body>
+<input id="input1" type="text" style="border: 5px solid black"/>
+<script type="application/javascript">
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ document.getElementById('input1').focus();
+ await waitToClearOutAnyPotentialScrolls(window);
+ await promiseApzFlushedRepaints();
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "focusing input did not change resolution " + resolution);
+
+ let transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ await waitToClearOutAnyPotentialScrolls(window);
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+ resolution = await getResolution();
+ ok(resolution != prev_resolution, "zoomToFocusedInput input changed resolution " + resolution);
+}
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_zoom_in_position_fixed.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_zoom_in_position_fixed.html
new file mode 100644
index 0000000000..946ffffa58
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_zoom_in_position_fixed.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100,initial-scale=0.4"/>
+ <title>Tests that zoomToFocuedInput zooms in element in position:fixed element</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ .container {
+ top: 0px;
+ height: 100px;
+ width: calc(100% - 30px);
+ position: fixed;
+ }
+ html {
+ height: 500vh;
+ }
+ </style>
+</head>
+<body>
+<div class="container">
+ <input>
+</div>
+<script>
+async function test() {
+ const initial_resolution = await getResolution();
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+
+ document.querySelector("input").focus();
+
+ const transformEndPromise = promiseTransformEnd();
+ SpecialPowers.DOMWindowUtils.zoomToFocusedInput();
+ await transformEndPromise;
+
+ let resolution = await getResolution();
+ ok(resolution > initial_resolution, "zoomToFocusedInput zooms in to " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_after_gpu_process_restart.html b/gfx/layers/apz/test/mochitest/helper_zoom_after_gpu_process_restart.html
new file mode 100644
index 0000000000..292d8f4600
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoom_after_gpu_process_restart.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Sanity check for pinch zooming after GPU process restart</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let initial_resolution = await getResolution();
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+
+ // Kill the GPU process
+ await SpecialPowers.spawnChrome([], async () => {
+ const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ if (gfxInfo.usingGPUProcess) {
+ const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+ );
+ let promise = TestUtils.topicObserved("compositor-reinitialized");
+
+ gfxInfo.killGPUProcessForTests();
+ await promise;
+ }
+ });
+
+ // Ensure resolution is unchanged by GPU process restart
+ await waitUntilApzStable();
+ let resolution = await getResolution();
+ ok(
+ resolution == initial_resolution,
+ "The resolution (" + resolution + ") is the same after GPU process restart"
+ );
+
+ // Perform the zoom
+ await pinchZoomInWithTouch(150, 300);
+
+ // Flush state and get the resolution we're at now
+ await promiseApzFlushedRepaints();
+ let final_resolution = await getResolution();
+ ok(
+ final_resolution > initial_resolution,
+ "The final resolution (" + final_resolution + ") is greater after zooming in"
+ );
+}
+
+waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ Here is some text to stare at as the test runs. It serves no functional
+ purpose, but gives you an idea of the zoom level. It's harder to tell what
+ the zoom level is when the page is just solid white.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_keyboardscroll.html b/gfx/layers/apz/test/mochitest/helper_zoom_keyboardscroll.html
new file mode 100644
index 0000000000..1a7b38fabd
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoom_keyboardscroll.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, minimum-scale=1.0">
+ <title>Tests that keyboard arrow keys scroll after zooming in when there was no scrollable overflow before zooming</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+</head>
+<body>
+ <div style="height: 20000px; background-color: green"></div>
+ <script type="application/javascript">
+ const utils = SpecialPowers.getDOMWindowUtils(window);
+
+ async function test() {
+ is(await getResolution(), 1.0, "should not be zoomed (1)");
+
+ is(window.scrollX, 0, "shouldn't have scrolled (2)");
+ is(window.scrollY, 0, "shouldn't have scrolled (3)");
+ is(visualViewport.pageTop, 0, "shouldn't have scrolled (4)");
+ is(visualViewport.pageLeft, 0, "shouldn't have scrolled (5)");
+
+ // Zoom in
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+ await promiseApzFlushedRepaints();
+ await promiseFrame();
+
+ is(await getResolution(), 2.0, "should have zoomed (6)");
+
+ is(window.scrollX, 0, "shouldn't have scrolled (7)");
+ is(window.scrollY, 0, "shouldn't have scrolled (8)");
+ is(visualViewport.pageTop, 0, "shouldn't have scrolled (9)");
+ is(visualViewport.pageLeft, 0, "shouldn't have scrolled (10)");
+
+ window.synthesizeKey("KEY_ArrowRight");
+
+ await promiseApzFlushedRepaints();
+ await promiseFrame();
+
+ is(await getResolution(), 2.0, "should be zoomed (11)");
+
+ is(window.scrollX, 0, "shouldn't have scrolled (12)");
+ is(window.scrollY, 0, "shouldn't have scrolled (13)");
+ is(visualViewport.pageTop, 0, "shouldn't have scrolled (14)");
+ isnot(visualViewport.pageLeft, 0, "should have scrolled (15)");
+
+ window.synthesizeKey("KEY_ArrowDown");
+
+ await promiseApzFlushedRepaints();
+ await promiseFrame();
+
+ is(await getResolution(), 2.0, "should be zoomed (16)");
+
+ is(window.scrollX, 0, "shouldn't have scrolled (17)");
+ is(window.scrollY, 0, "shouldn't have scrolled (18)");
+ isnot(visualViewport.pageTop, 0, "should have scrolled (19)");
+ isnot(visualViewport.pageLeft, 0, "should have scrolled (20)");
+
+ // Zoom back out
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(1.0);
+ await promiseApzFlushedRepaints();
+ await promiseFrame();
+
+ is(await getResolution(), 1.0, "should not be zoomed (21)");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_oopif.html b/gfx/layers/apz/test/mochitest/helper_zoom_oopif.html
new file mode 100644
index 0000000000..788fa31bbb
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoom_oopif.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Sanity check for pinch zooming oop iframe</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let thetarget = document.getElementById("target");
+ let r = thetarget.getBoundingClientRect();
+ let x = r.x + r.width/2;
+ let y = r.y + r.height/2;
+
+ let initial_resolution = await getResolution();
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+ if (useTouchpad) {
+ await pinchZoomInWithTouchpad(x, y);
+ } else {
+ await pinchZoomInWithTouch(x, y);
+ }
+ // Flush state and get the resolution we're at now
+ await promiseApzFlushedRepaints();
+ let final_resolution = await getResolution();
+ ok(final_resolution > initial_resolution, "The final resolution (" + final_resolution + ") is greater after zooming in");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+<style>
+iframe {
+ margin: 0;
+ padding: 0;
+ border: 1px solid black;
+}
+</style>
+
+</head>
+<body>
+
+<iframe id="target" width="100" height="100" src="http://example.org/"></iframe>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_out_clamped_scrollpos.html b/gfx/layers/apz/test/mochitest/helper_zoom_out_clamped_scrollpos.html
new file mode 100644
index 0000000000..2948df9ae3
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoom_out_clamped_scrollpos.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, minimum-scale=1.0">
+ <title>Tests that zooming out with an unchanging scroll pos still works properly</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+</head>
+<body>
+ <div style="height: 2000px; background-color: linear-gradient(green,blue)"></div>
+ <script type="application/javascript">
+ const utils = SpecialPowers.getDOMWindowUtils(window);
+
+ async function test() {
+ // Initial state
+ is(await getResolution(), 1.0, "should not be zoomed");
+
+ // Zoom in
+ utils.setResolutionAndScaleTo(2.0);
+ await promiseApzFlushedRepaints();
+ // Check that we're still at 0,0 in both layout and visual viewport
+ is(await getResolution(), 2.0, "should be zoomed to 2.0");
+ is(window.scrollX, 0, "shouldn't have scrolled (1)");
+ is(window.scrollY, 0, "shouldn't have scrolled (2)");
+ is(visualViewport.pageLeft, 0, "shouldn't have scrolled (3)");
+ is(visualViewport.pageTop, 0, "shouldn't have scrolled (4)");
+
+ // Freeze the main-thread refresh driver to stop it from processing
+ // paint requests
+ utils.advanceTimeAndRefresh(0);
+
+ // Zoom out. This will send a series of paint requests to the main
+ // thread with zooms that go down from 2.0 to 1.0.
+ // Use a similar touch sequence to what pinchZoomOutWithTouchAtCenter()
+ // does, except keep the first touch point anchored and only move the
+ // second touch point. In particular, we drag the second touch point
+ // from the top-left quadrant of the screen to the bottom-right, so that
+ // the scroll position never changes from 0,0. If we move either finger
+ // upwards at all, the synthesization can generate intermediate touch
+ // events with just that change which can cause the page to scroll down
+ // which we don't want here.
+ // The key here is that each of the repaint requests keeps the scroll
+ // position at 0,0, which in terms of the bug, means that only the first
+ // repaint request actually takes effect and the rest are discarded.
+ // The first repaint request has a zoom somewhere between 1.0 and 2.0,
+ // and therefore after the pinch is done, the zoom ends up stuck there
+ // instead of going all the way back to 1.0 like we would expect.
+ const deltaX = window.visualViewport.width / 16;
+ const deltaY = window.visualViewport.height / 16;
+ const centerX =
+ window.visualViewport.pageLeft + window.visualViewport.width / 2;
+ const centerY =
+ window.visualViewport.pageTop + window.visualViewport.height / 2;
+ const anchorFinger = { x: centerX + (deltaX * 6), y: centerY + (deltaY * 6) };
+ var zoom_out = [];
+ for (var i = -6; i < 6; i++) {
+ var movingFinger = { x: centerX + (deltaX * i), y: centerY + (deltaY * i) };
+ zoom_out.push([anchorFinger, movingFinger]);
+ }
+ var touchIds = [0, 1];
+ await synthesizeNativeTouchAndWaitForTransformEnd(zoom_out, touchIds);
+
+ // Release the refresh driver
+ utils.restoreNormalRefresh();
+
+ // Flush all the things, reach stable state
+ await promiseApzFlushedRepaints();
+
+ // Check that we're back at 1.0 resolution
+ is(await getResolution(), 1.0, "should be back at initial resolution");
+
+ // More sanity checks
+ is(window.scrollX, 0, "shouldn't have scrolled (5)");
+ is(window.scrollY, 0, "shouldn't have scrolled (6)");
+ is(visualViewport.pageLeft, 0, "shouldn't have scrolled (7)");
+ is(visualViewport.pageTop, 0, "shouldn't have scrolled (8)");
+ }
+
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_out_with_mainthread_clamping.html b/gfx/layers/apz/test/mochitest/helper_zoom_out_with_mainthread_clamping.html
new file mode 100644
index 0000000000..c15622872a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoom_out_with_mainthread_clamping.html
@@ -0,0 +1,110 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, minimum-scale=1.0">
+ <title>Tests that zooming out in a way that triggers main-thread scroll re-clamping works properly</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+</head>
+<body>
+ <div style="width: 200vw; height: 2000px; background-color: linear-gradient(green,blue)"></div>
+ <script type="application/javascript">
+ const utils = SpecialPowers.getDOMWindowUtils(window);
+
+ async function test() {
+ // Initial state
+ is(await getResolution(), 1.0, "should not be zoomed");
+
+ // Zoom in and go to the bottom-right corner. This ensures the layout
+ // and visual scroll offsets are nonzero, which increases the chances
+ // that the scroll position layer alignment code will mutate the scroll
+ // position (see comment below).
+ utils.setResolutionAndScaleTo(5.0);
+ await promiseApzFlushedRepaints();
+ utils.scrollToVisual(document.scrollingElement.clientWidth * 5,
+ document.scrollingElement.clientHeight * 5,
+ utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_INSTANT);
+ await promiseApzFlushedRepaints();
+
+ // Check that we're at the right place
+ is(await getResolution(), 5.0, "should be zoomed to 5.0");
+ is(window.scrollX, window.scrollMaxX, "layout x-coord should be maxed");
+ is(window.scrollY, window.scrollMaxY, "layout y-coord should be maxed");
+ ok(visualViewport.offsetLeft > 0, "visual x-coord should be even further");
+ ok(visualViewport.offsetTop > 0, "visual y-coord should be even further");
+
+ // Zoom out. This will trigger repaint requests to the main thread,
+ // at various intermediate resolutions. The repaint requests will
+ // trigger reflows, which will trigger the root scrollframe to re-clamp
+ // and layer-align the scroll position as part of the post-reflow action.
+ // The test is checking that these mutations don't end up sending a scroll
+ // position update to APZ that interrupts the zoom action (see bug 1671284
+ // comment 9 for the exact mechanism). In order to maximize the chances of
+ // catching the bug, we wait for the main thread repaint after each of the
+ // pinch inputs.
+
+ let zoom_out = pinchZoomOutTouchSequenceAtCenter();
+ // Do coordinate conversion up-front using the current resolution and
+ // visual viewport.
+ for (let entry of zoom_out) {
+ for (let i = 0; i < entry.length; i++) {
+ entry[i] = await coordinatesRelativeToScreen({
+ offsetX: entry[i].x,
+ offsetY: entry[i].y,
+ target: document.body,
+ });
+ }
+ }
+ // Dispatch the touch events, waiting for paints after each row in
+ // zoom_out.
+ let touchIds = [0, 1];
+ for (let i = 0; i < zoom_out.length; i++) {
+ let entry = zoom_out[i];
+ for (let j = 0; j < entry.length; j++) {
+ await new Promise(resolve => {
+ utils.sendNativeTouchPoint(
+ touchIds[j],
+ utils.TOUCH_CONTACT,
+ entry[j].x,
+ entry[j].y,
+ 1,
+ 90,
+ resolve
+ );
+ });
+ }
+ await promiseAllPaintsDone();
+
+ // On the last row also do the touch-up events
+ if (i == zoom_out.length - 1) {
+ for (let j = 0; j < entry.length; j++) {
+ await new Promise(resolve => {
+ utils.sendNativeTouchPoint(
+ touchIds[j],
+ utils.TOUCH_REMOVE,
+ entry[j].x,
+ entry[j].y,
+ 1,
+ 90,
+ resolve
+ );
+ });
+ }
+ }
+ }
+
+ // Wait for everything to stabilize
+ await promiseApzFlushedRepaints();
+
+ // Verify that the zoom completed and we're back at 1.0 resolution
+ isfuzzy(await getResolution(), 1.0, 0.0001, "should be back at initial resolution");
+ }
+
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_prevented.html b/gfx/layers/apz/test/mochitest/helper_zoom_prevented.html
new file mode 100644
index 0000000000..b756c873f2
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoom_prevented.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Checking prevent-default for zooming</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function testPreventDefault(aTouchStartToCancel) {
+ var initial_resolution = await getResolution();
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+
+ // preventDefault exactly one touchstart based on the value of aTouchStartToCancel
+ var touchStartCount = 0;
+ var canceller = function(e) {
+ dump("touchstart listener hit, count: " + touchStartCount + "\n");
+ touchStartCount++;
+ if (touchStartCount == aTouchStartToCancel) {
+ dump("calling preventDefault on touchstart\n");
+ e.preventDefault();
+ document.documentElement.removeEventListener("touchstart", canceller, {passive: false});
+ }
+ };
+ document.documentElement.addEventListener("touchstart", canceller, {passive: false});
+
+ let touchEndPromise = new Promise(resolve => {
+ document.documentElement.addEventListener("touchend", resolve, {passive: true, once: true});
+ });
+
+ // Ensure that APZ gets updated hit-test info
+ await promiseAllPaintsDone();
+
+ await pinchZoomInTouchSequence(150, 300);
+ await touchEndPromise; // wait for the touchend listener to fire
+
+ // Flush state and get the resolution we're at now
+ await promiseApzFlushedRepaints();
+ let final_resolution = await getResolution();
+ is(final_resolution, initial_resolution, "The final resolution (" + final_resolution + ") matches the initial resolution");
+}
+
+function transformFailer() {
+ ok(false, "The test fired an unexpected APZ:TransformEnd");
+}
+
+async function test() {
+ // Register a listener that fails the test if the APZ:TransformEnd event fires,
+ // because this test shouldn't actually be triggering any transforms
+ SpecialPowers.Services.obs.addObserver(transformFailer, "APZ:TransformEnd");
+
+ await testPreventDefault(1);
+ await testPreventDefault(2);
+}
+
+function cleanup() {
+ SpecialPowers.Services.obs.removeObserver(transformFailer, "APZ:TransformEnd");
+}
+
+waitUntilApzStable()
+.then(test)
+.finally(cleanup)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ Here is some text to stare at as the test runs. It serves no functional
+ purpose, but gives you an idea of the zoom level. It's harder to tell what
+ the zoom level is when the page is just solid white.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_restore_position_tabswitch.html b/gfx/layers/apz/test/mochitest/helper_zoom_restore_position_tabswitch.html
new file mode 100644
index 0000000000..0373b321da
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoom_restore_position_tabswitch.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Switching tabs back to a zoomed page should restore visual offset</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let visResEvt = new EventCounter(window.visualViewport, "resize");
+
+ // Do a pinch-zoom in, wait for everything to settle on the APZ side.
+ await pinchZoomInWithTouch(400, 300);
+ await promiseApzFlushedRepaints();
+
+ // Force a new layer tree to the compositor, because otherwise tab-switching
+ // away and back can end up reusing the last layer tree sent to the compositor,
+ // and that may not have the latest visual viewport offset sent from APZ
+ // to the main thread. This should be removed once bug 1640730 is fixed.
+ await forceLayerTreeToCompositor();
+
+ // Wait until we get a visual viewport resized event, if we haven't yet gotten
+ // one.
+ if (visResEvt.count == 0) {
+ await promiseOneEvent(window.visualViewport, "resize", null);
+ }
+ let resizeCount = visResEvt.count;
+ ok(resizeCount > 0, `Visual viewport got resized ${resizeCount} times`);
+
+ // Record the current visual viewport and ensure it reflects a zoomed state.
+ let zoomedViewport = visualViewportAsZoomedRect();
+ ok(visualViewport.offsetLeft > 0, "Visual viewport should not be same as layout viewport (left)");
+ ok(visualViewport.offsetTop > 0, "Visual viewport should not be same as layout viewport (top)");
+ ok(zoomedViewport.x > 0, "Sanity check to ensure visual viewport is not at 0,0 (left)");
+ ok(zoomedViewport.y > 0, "Sanity check to ensure visual viewport is not at 0,0 (top)");
+ ok(zoomedViewport.z > 1, "Sanity check to ensure visual viewport scale is > 1");
+
+ // Open a new foreground tab and wait until it closes itself. The tab itself
+ // waits for APZ stability before closing, so we know that the APZ state
+ // was updated to the other document and back to this window.
+ let focusPromise = promiseOneEvent(window, "focus", null);
+ window.open("helper_self_closer.html", "_blank");
+ await focusPromise;
+ ok(true, "Got focus back after self-closer closed");
+
+ // Wait for the dust to settle.
+ await promiseApzFlushedRepaints();
+
+ // Ensure the visual viewport is just as we left it.
+ let restoredViewport = visualViewportAsZoomedRect();
+ for (field in zoomedViewport) {
+ is(restoredViewport[field], zoomedViewport[field], `Field ${field} of the zoomed viewport restored`);
+ }
+
+ // Just for good measure. This might help with debugging unexpected failures.
+ is(visResEvt.count, resizeCount, "No more VV resizes should have occurred");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ Here is some text to stare at as the test runs. It serves no functional
+ purpose, but gives you an idea of the zoom level. It's harder to tell what
+ the zoom level is when the page is just solid white.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_with_dynamic_toolbar.html b/gfx/layers/apz/test/mochitest/helper_zoom_with_dynamic_toolbar.html
new file mode 100644
index 0000000000..261bca1377
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoom_with_dynamic_toolbar.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Zooming out to the initial scale with the dynamic toolbar</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+
+ <style>
+ html,body {
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ }
+ </style>
+
+ <script type="application/javascript">
+
+async function test() {
+ ok(window.visualViewport.scale > 1.0,
+ "The scale value should be greater than 1.0");
+
+ // Do a pinch-zoom out to restore the initial scale.
+ await pinchZoomOutWithTouchAtCenter();
+ await promiseApzFlushedRepaints();
+
+ is(visualViewport.scale, 1.0,
+ "The initial scale value should be restored to 1.0");
+}
+
+SpecialPowers.getDOMWindowUtils(window).setDynamicToolbarMaxHeight(100);
+SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(1.1)
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_with_touchpad.html b/gfx/layers/apz/test/mochitest/helper_zoom_with_touchpad.html
new file mode 100644
index 0000000000..6836864964
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoom_with_touchpad.html
@@ -0,0 +1,110 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Sanity check for Touchpad pinch zooming</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ // Scenario 1: zoom in
+ var initial_resolution = await getResolution();
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+ await pinchZoomInWithTouchpad(641, 465);
+ // Flush state and get the resolution we're at now
+ await promiseApzFlushedRepaints();
+ let final_resolution = await getResolution();
+ ok(final_resolution > initial_resolution, "The final resolution (" + final_resolution + ") is greater after zooming in");
+
+ // Scenario 2: zoom out
+ initial_resolution = final_resolution;
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+ await pinchZoomOutWithTouchpad(641, 465);
+ await promiseApzFlushedRepaints();
+ final_resolution = await getResolution();
+ ok(final_resolution < initial_resolution, "The final resolution (" + final_resolution + ") is smaller after zooming Out");
+
+ // Scenario 3: zoom in and out in the same gesture
+ initial_resolution = final_resolution;
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+ await pinchZoomInOutWithTouchpad (641, 465);
+ await promiseApzFlushedRepaints();
+ final_resolution = await getResolution();
+ isfuzzy(initial_resolution, final_resolution, 0.0001, "The final resolution approximatly the same after zooming In and Out");
+
+ // Scenario 4: zoom in, with the page using preventDefault()
+ var resolveWheelPromise;
+ var wheelPromise = new Promise(resolve => { resolveWheelPromise = resolve; });
+ var deltaSum = 0;
+ initial_resolution = final_resolution;
+ var onWheel = function(e) {
+ if (e.ctrlKey) {
+ e.preventDefault();
+ deltaSum += e.deltaY;
+ // We observed that deltaSum will be around -42 by the time all wheel events have arrived.
+ if (deltaSum < -40) {
+ ok(true, "Accumulated a deltaY of -40");
+ resolveWheelPromise();
+ }
+ }
+ };
+
+ document.addEventListener("wheel", onWheel, { passive: false });
+ // Give APZ a chance to become aware of the listener, so it knows
+ // to queue events while it waits for a content response.
+ await promiseApzFlushedRepaints();
+ // Calling preventDefault() means the APZ:TransformEnd notification will never be sent.
+ await pinchZoomInWithTouchpad(641, 465, { waitForTransformEnd: false });
+ await wheelPromise;
+ document.removeEventListener("wheel", onWheel, { passive: false });
+ final_resolution = await getResolution();
+ is(final_resolution, initial_resolution,
+ "Calling preventDefault() on wheel event successfully prevents zooming");
+
+ // Scenario 5: check that page receives DOMMouseScroll event
+ var resolveDOMMouseScrollPromise;
+ var DOMMouseScrollPromise = new Promise(resolve => { resolveDOMMouseScrollPromise = resolve; });
+ deltaSum = 0;
+ initial_resolution = final_resolution;
+ var onDOMMouseScroll = function(e) {
+ if (e.ctrlKey) {
+ e.preventDefault();
+ deltaSum += e.detail;
+ if (deltaSum < -40) {
+ ok(true, "Accumulated a deltaSum of -40");
+ resolveDOMMouseScrollPromise();
+ }
+ }
+ };
+ document.addEventListener("DOMMouseScroll", onDOMMouseScroll, { passive: false });
+ await promiseApzFlushedRepaints();
+ await pinchZoomInWithTouchpad(641, 465, {
+ waitForTransformEnd: false,
+ waitForFrames: true
+ });
+ await DOMMouseScrollPromise;
+ document.removeEventListener("DOMMouseScroll", onDOMMouseScroll, { passive: false });
+ final_resolution = await getResolution();
+ is(final_resolution, initial_resolution,
+ "Calling preventDefault() on DOMMouseScroll event successfully prevents zooming");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ Here is some text to stare at as the test runs. It serves no functional
+ purpose, but gives you an idea of the zoom level. It's harder to tell what
+ the zoom level is when the page is just solid white.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomed_pan.html b/gfx/layers/apz/test/mochitest/helper_zoomed_pan.html
new file mode 100644
index 0000000000..98547fb73f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomed_pan.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0,minimum-scale=1.0">
+ <title>Ensure layout viewport responds to panning while pinched</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ margin: 0;
+ padding: 0;
+ }
+ #content {
+ height: 5000px;
+ width: 5000px;
+ background: repeating-linear-gradient(#EEE, #EEE 100px, #DDD 100px, #DDD 200px);
+ }
+ </style>
+</head>
+<body>
+ <div id="content"></div>
+ <script type="application/javascript">
+ const RESOLUTION = 4;
+ const OFFSET_SCREEN_PX = 50;
+ const OFFSET_CSS_PX = OFFSET_SCREEN_PX / RESOLUTION;
+
+ function computeDelta(visual) {
+ // Compute the distance from the right/bottom edge of the visual
+ // viewport to the same edge of the layout viewport and add the desired
+ // offset to that.
+ // We can ignore scrollbar width here since the scrollbar is layouted at
+ // the right/bottom edge of this content, not of this window in the case
+ // of containerful scrolling.
+ return visual - (visual / RESOLUTION) + OFFSET_CSS_PX;
+ }
+
+ async function test() {
+ const cases = [
+ {
+ x: 0,
+ y: 0,
+ dx: (width) => -computeDelta(width),
+ dy: (height) => 0,
+ expected: {
+ x: [OFFSET_CSS_PX, "x-offset was adjusted"],
+ y: [0, "y-offset was not affected"],
+ },
+ },
+ {
+ x: OFFSET_CSS_PX,
+ y: 0,
+ dx: (width) => 0,
+ dy: (height) => -computeDelta(height),
+ expected: {
+ x: [OFFSET_CSS_PX, "x-offset was not affected"],
+ y: [OFFSET_CSS_PX, "y-offset was adjusted"],
+ },
+ },
+ ];
+
+ for (let c of cases) {
+ await promiseNativeTouchDrag(window,
+ c.x,
+ c.y,
+ c.dx(document.documentElement.clientWidth),
+ c.dy(document.documentElement.clientHeight));
+ await promiseApzFlushedRepaints();
+ is(window.scrollX, c.expected.x[0], c.expected.x[1]);
+ is(window.scrollY, c.expected.y[0], c.expected.y[1]);
+ }
+ }
+
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(RESOLUTION);
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/mochitest.toml b/gfx/layers/apz/test/mochitest/mochitest.toml
new file mode 100644
index 0000000000..bcbefbe547
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/mochitest.toml
@@ -0,0 +1,234 @@
+[DEFAULT]
+prefs = ["gfx.font_loader.delay=0"]
+support-files = [
+ "apz_test_native_event_utils.js",
+ "apz_test_utils.js",
+ "green100x100.png",
+ "helper_*.*",
+]
+tags = "apz"
+
+["test_abort_smooth_scroll_by_instant_scroll.html"]
+
+["test_bug1151667.html"]
+skip-if = ["os == 'android'"] # wheel events not supported on mobile
+
+["test_bug1253683.html"]
+skip-if = [
+ "os == 'android'", # wheel events not supported on mobile
+ "display == 'wayland' && os_version == '22.04'", # Bug 1857059
+]
+
+["test_bug1277814.html"]
+skip-if = [
+ "os == 'android'", # wheel events not supported on mobile
+ "display == 'wayland' && os_version == '22.04'", # Bug 1857059
+]
+
+["test_bug1304689-2.html"]
+
+["test_bug1304689.html"]
+
+["test_frame_reconstruction.html"]
+
+["test_group_bug1534549.html"]
+
+["test_group_checkerboarding.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_group_displayport.html"]
+
+["test_group_double_tap_zoom-2.html"]
+run-if = [ # FIXME: enable on more desktop platforms (see bug 1608506 comment 4)
+ "os == 'android'",
+ "os == 'mac'",
+]
+
+["test_group_double_tap_zoom.html"]
+run-if = [ # FIXME: enable on more desktop platforms (see bug 1608506 comment 4)
+ "os == 'android'",
+ "os == 'mac'",
+]
+
+["test_group_fullscreen.html"]
+run-if = ["os == 'android'"]
+
+["test_group_hittest-1.html"]
+skip-if = ["os == 'android'"] # mouse events not supported on mobile
+
+["test_group_hittest-2.html"]
+skip-if = [
+ "os == 'android'", # mouse events not supported on mobile
+ "win11_2009 && bits == 32",
+ "win11_2009 && asan",
+ "os == 'linux' && asan", # stack is not large enough for the test
+ "display == 'wayland' && os_version == '22.04'", # Bug 1857059
+ "http3",
+ "http2",
+]
+
+["test_group_hittest-3.html"]
+skip-if = [
+ "os == 'android'", # mouse events not supported on mobile
+ "win11_2009 && bits == 32",
+ "win11_2009 && asan",
+ "http3",
+ "http2",
+]
+
+["test_group_hittest-overscroll.html"]
+skip-if = ["os == 'android'"] # mouse events not supported on mobile
+
+["test_group_keyboard-2.html"]
+
+["test_group_keyboard.html"]
+
+["test_group_mainthread.html"]
+
+["test_group_minimum_scale_size.html"]
+run-if = ["os == 'android'"]
+
+["test_group_mouseevents.html"]
+skip-if = [
+ "os == 'android'", # mouse events not supported on mobile
+ "display == 'wayland' && os_version == '22.04'", # Bug 1857059
+]
+
+["test_group_overrides.html"]
+skip-if = [
+ "os == 'android'", # wheel events not supported on mobile
+ "display == 'wayland' && os_version == '22.04'", # Bug 1857059
+]
+
+["test_group_overscroll.html"]
+skip-if = [
+ "os == 'android'", # wheel events not supported on mobile
+ "display == 'wayland' && os_version == '22.04'", # Bug 1857059
+]
+
+["test_group_overscroll_handoff.html"]
+skip-if = [
+ "os == 'android'", # wheel events not supported on mobile
+ "display == 'wayland' && os_version == '22.04'", # Bug 1857059
+ "http3",
+ "http2",
+]
+
+["test_group_pointerevents.html"]
+skip-if = [
+ "win10_2009", # Bug 1404836
+ "win11_2009", # Bug 1404836
+]
+
+["test_group_programmatic_scroll_behavior.html"]
+
+["test_group_scroll_linked_effect.html"]
+skip-if = [
+ "os == 'android'", # wheel events not supported on mobile
+ "http3",
+ "http2",
+]
+
+["test_group_scroll_snap.html"]
+skip-if = [
+ "os == 'android'", # wheel events not supported on mobile
+ "display == 'wayland' && os_version == '22.04'", # Bug 1857059
+]
+
+["test_group_scrollend.html"]
+skip-if = [
+ "os == 'android'", # wheel events not supported on mobile
+ "display == 'wayland' && os_version == '22.04'", # Bug 1857059
+]
+
+["test_group_scrollframe_activation.html"]
+skip-if = ["display == 'wayland' && os_version == '22.04'"] # Bug 1857059
+
+["test_group_touchevents-2.html"]
+
+["test_group_touchevents-3.html"]
+
+["test_group_touchevents-4.html"]
+
+["test_group_touchevents-5.html"]
+skip-if = ["display == 'wayland' && os_version == '22.04'"] # Bug 1857059
+
+["test_group_touchevents.html"]
+
+["test_group_wheelevents.html"]
+skip-if = [
+ "os == 'android'", # wheel events not supported on mobile
+ "display == 'wayland' && os_version == '22.04'", # Bug 1857059
+]
+
+["test_group_zoom-2.html"]
+skip-if = ["os == 'win'"] # see bug 1495580 for Windows
+
+["test_group_zoom.html"]
+skip-if = ["os == 'win'"] # see bug 1495580 for Windows
+
+["test_group_zoomToFocusedInput.html"]
+
+["test_interrupted_reflow.html"]
+
+["test_layerization.html"]
+skip-if = [
+ "os == 'android'", # wheel events not supported on mobile
+ "os == 'linux' && fission && headless", # Bug 1722907
+ "display == 'wayland' && os_version == '22.04'", # Bug 1857059
+]
+
+["test_relative_update.html"]
+skip-if = [
+ "os == 'android'", # wheel events not supported on mobile
+ "display == 'wayland' && os_version == '22.04'", # Bug 1857059
+]
+
+["test_scroll_inactive_bug1190112.html"]
+skip-if = [
+ "os == 'android'", # wheel events not supported on mobile
+ "display == 'wayland' && os_version == '22.04'", # Bug 1857059
+]
+
+["test_scroll_inactive_flattened_frame.html"]
+skip-if = [
+ "os == 'android'", # wheel events not supported on mobile
+ "display == 'wayland' && os_version == '22.04'", # Bug 1857059
+]
+
+["test_scroll_subframe_scrollbar.html"]
+skip-if = [
+ "os == 'android'", # wheel events not supported on mobile
+ "display == 'wayland' && os_version == '22.04'", # Bug 1857059
+]
+
+["test_smoothness.html"]
+# hardware vsync only on win/mac
+# Frame Uniformity recording is not implemented for webrender
+run-if = [
+ "os == 'mac'",
+ "os == 'win'",
+]
+skip-if = ["true"] # Don't run in CI yet, see bug 1657477
+
+["test_touch_listeners_impacting_wheel.html"]
+skip-if = [
+ "os == 'android'", # wheel events not supported on mobile
+ "os == 'mac'", # synthesized wheel smooth-scrolling not supported on OS X
+ "display == 'wayland' && os_version == '22.04'", # Bug 1857059
+]
+
+["test_wheel_scroll.html"]
+skip-if = [
+ "os == 'android'", # wheel events not supported on mobile
+ "display == 'wayland' && os_version == '22.04'", # Bug 1857059
+]
+
+["test_wheel_transactions.html"]
+skip-if = [
+ "os == 'android'", # wheel events not supported on mobile
+ "display == 'wayland' && os_version == '22.04'", # Bug 1857059
+]
diff --git a/gfx/layers/apz/test/mochitest/test_abort_smooth_scroll_by_instant_scroll.html b/gfx/layers/apz/test/mochitest/test_abort_smooth_scroll_by_instant_scroll.html
new file mode 100644
index 0000000000..650c21cac7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_abort_smooth_scroll_by_instant_scroll.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test to make sure an on-going smooth scroll is aborted by a new
+ instant absolute scroll operation</title>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<div style="height: 100000px;"></div>
+<script type="application/javascript">
+async function test() {
+ // Trigger a smooth scroll.
+ document.scrollingElement.scrollTo({ top: 90000, behavior: "smooth" });
+ // Need to wait for a scroll event here since with the very small
+ // layout.css.scroll-behavior.spring-constant value it's possible that the
+ // scroll position hasn't yet been changed after a promiseApzFlushedRepaints
+ // call.
+ await waitForScrollEvent(window);
+ await promiseApzFlushedRepaints();
+
+ ok(document.scrollingElement.scrollTop > 0,
+ "Should have scrolled. scroll position: " + document.scrollingElement.scrollTop);
+
+ // Trigger an instant scroll.
+ document.scrollingElement.scrollTo({ top: 0, behavior: "instant" });
+ is(document.scrollingElement.scrollTop, 0,
+ "The previous smooth scroll operation should have been superseded by " +
+ "the instant scroll");
+
+ // Double check after a repaint request.
+ await promiseApzFlushedRepaints();
+ is(document.scrollingElement.scrollTop, 0,
+ "The scroll postion should have stayed after a repaint request");
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// Use a very small spring constant value for smooth scrolling so that the
+// smooth scrollling keeps running at least for a few seconds.
+pushPrefs([["layout.css.scroll-behavior.spring-constant", 1]])
+.then(waitUntilApzStable)
+.then(test)
+.then(SimpleTest.finish, SimpleTest.finishWithFailure);
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_bug1151667.html b/gfx/layers/apz/test/mochitest/test_bug1151667.html
new file mode 100644
index 0000000000..12a46b9094
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_bug1151667.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1151667
+-->
+<head>
+ <title>Test for Bug 1151667</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ #subframe {
+ margin-top: 100px;
+ height: 500px;
+ width: 500px;
+ overflow: scroll;
+ }
+ #subframe-content {
+ height: 1000px;
+ width: 500px;
+ /* the background is so that we can see it scroll*/
+ background: repeating-linear-gradient(#EEE, #EEE 100px, #DDD 100px, #DDD 200px);
+ }
+ #page-content {
+ height: 5000px;
+ width: 500px;
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1151667">Mozilla Bug 1151667</a>
+<p id="display"></p>
+<div id="subframe">
+ <!-- This makes sure the subframe is scrollable -->
+ <div id="subframe-content"></div>
+</div>
+<!-- This makes sure the page is also scrollable, so it (rather than the subframe)
+ is considered the primary async-scrollable frame, and so the subframe isn't
+ layerized upon page load. -->
+<div id="page-content"></div>
+<pre id="test">
+<script type="application/javascript">
+
+async function test() {
+ var subframe = document.getElementById("subframe");
+ await promiseNativeWheelAndWaitForScrollEvent(subframe, 100, 150, 0, -10);
+
+ is(subframe.scrollTop > 0, true, "We should have scrolled the subframe down");
+ is(document.documentElement.scrollTop, 0, "We should not have scrolled the page");
+}
+
+SimpleTest.waitForExplicitFinish();
+waitUntilApzStable()
+ .then(test)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_bug1253683.html b/gfx/layers/apz/test/mochitest/test_bug1253683.html
new file mode 100644
index 0000000000..f12455fb3a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_bug1253683.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1253683
+-->
+<head>
+ <title>Test to ensure non-scrollable frames don't get layerized</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <p id="display"></p>
+ <div id="container" style="height: 500px; overflow:scroll">
+ <pre id="no_layer" style="background-color: #f5f5f5; margin: 15px; padding: 15px; margin-top: 100px; border: 1px solid #eee; overflow:scroll">sample code here</pre>
+ <div style="height: 5000px">spacer to make the 'container' div the root scrollable element</div>
+ </div>
+<pre id="test">
+<script type="application/javascript">
+
+async function test() {
+ var container = document.getElementById("container");
+ var no_layer = document.getElementById("no_layer");
+
+ // Check initial state
+ is(container.scrollTop, 0, "Initial scrollY should be 0");
+ ok(!isLayerized("no_layer"), "initially 'no_layer' should not be layerized");
+
+ // Scrolling over outer1 should layerize outer1, but not inner1.
+ await promiseMoveMouseAndScrollWheelOver(no_layer, 10, 10, true);
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+
+ ok(container.scrollTop > 0, "We should have scrolled the body");
+ ok(!isLayerized("no_layer"), "no_layer should still not be layerized");
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+
+ // Turn off displayport expiry so that we don't miss failures where the
+ // displayport is set and expired before we check for layerization.
+ // Also enable APZ test logging, since we use that data to determine whether
+ // a scroll frame was layerized.
+ pushPrefs([["apz.displayport_expiry_ms", 0],
+ ["apz.test.logging_enabled", true]])
+ .then(waitUntilApzStable)
+ .then(forceLayerTreeToCompositor)
+ .then(test)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_bug1277814.html b/gfx/layers/apz/test/mochitest/test_bug1277814.html
new file mode 100644
index 0000000000..28c4619e4e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_bug1277814.html
@@ -0,0 +1,105 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1277814
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1277814</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+ async function test() {
+ // Trigger the buggy scenario
+ var subframe = document.getElementById("bug1277814-div");
+ subframe.classList.add("a");
+
+ // The transform change is animated, so let's step through 1s of animation
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ for (var i = 0; i < 60; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ utils.restoreNormalRefresh();
+
+ // Wait for the layer tree with any updated dispatch-to-content region to
+ // get pushed over to the APZ
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+
+ // Trigger layerization of the subframe by scrolling the wheel over it
+ await promiseMoveMouseAndScrollWheelOver(subframe, 10, 10);
+
+ // Give APZ the chance to compute a displayport, and content
+ // to render based on it.
+ await promiseApzFlushedRepaints();
+
+ // Examine the content-side APZ test data
+ var contentTestData = utils.getContentAPZTestData();
+
+ // Test that the scroll frame for the div 'bug1277814-div' appears in
+ // the APZ test data. The bug this test is for causes the displayport
+ // calculation for this scroll frame to go wrong, causing it not to
+ // become layerized.
+ contentTestData = convertTestData(contentTestData);
+ var foundIt = false;
+ for (var seqNo in contentTestData.paints) {
+ var paint = contentTestData.paints[seqNo];
+ for (var scrollId in paint) {
+ var scrollFrame = paint[scrollId];
+ if ("contentDescription" in scrollFrame &&
+ scrollFrame.contentDescription.includes("bug1277814-div")) {
+ foundIt = true;
+ }
+ }
+ }
+ SimpleTest.ok(foundIt, "expected to find APZ test data for bug1277814-div");
+ }
+
+ if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+
+ pushPrefs([["apz.test.logging_enabled", true]])
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ }
+ </script>
+ <style>
+ #bug1277814-div
+ {
+ position: absolute;
+ left: 0;
+ top: 0;
+ padding: .5em;
+ overflow: auto;
+ color: white;
+ background: green;
+ max-width: 30em;
+ max-height: 6em;
+ visibility: hidden;
+ transform: scaleY(0);
+ transition: transform .15s ease-out, visibility 0s ease .15s;
+ }
+ #bug1277814-div.a
+ {
+ visibility: visible;
+ transform: scaleY(1);
+ transition: transform .15s ease-out;
+ }
+ </style>
+</head>
+<body>
+ <!-- Use a unique id because we'll be checking for it in the content
+ description logged in the APZ test data -->
+ <div id="bug1277814-div">
+ CoolCmd<br>CoolCmd<br>CoolCmd<br>CoolCmd<br>
+ CoolCmd<br>CoolCmd<br>CoolCmd<br>CoolCmd<br>
+ CoolCmd<br>CoolCmd<br>CoolCmd<br>CoolCmd<br>
+ CoolCmd<br>CoolCmd<br>CoolCmd<br>CoolCmd<br>
+ CoolCmd<br>CoolCmd<br>CoolCmd<br>CoolCmd<br>
+ <button>click me</button>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_bug1304689-2.html b/gfx/layers/apz/test/mochitest/test_bug1304689-2.html
new file mode 100644
index 0000000000..1c7b1255d9
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_bug1304689-2.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1304689
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1285070</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style type="text/css">
+ #outer {
+ height: 400px;
+ width: 415px;
+ overflow: scroll;
+ position: relative;
+ scroll-behavior: smooth;
+ }
+ #outer.contentBefore::before {
+ top: 0;
+ content: '';
+ display: block;
+ height: 2px;
+ position: absolute;
+ width: 100%;
+ z-index: 99;
+ }
+ </style>
+ <script type="application/javascript">
+
+async function test() {
+ var utils = SpecialPowers.DOMWindowUtils;
+ var elm = document.getElementById("outer");
+
+ // Set margins on the element, to ensure it is layerized
+ utils.setDisplayPortMarginsForElement(0, 0, 0, 0, elm, /* priority*/ 1);
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+
+ // Take control of the refresh driver
+ utils.advanceTimeAndRefresh(0);
+
+ // Start a smooth-scroll animation in the compositor and let it go a few
+ // frames, so that there is some "user scrolling" going on (per the comment
+ // in AsyncPanZoomController::NotifyLayersUpdated)
+ elm.scrollTop = 10;
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+
+ // Do another scroll update but also do a frame reconstruction within the same
+ // tick of the refresh driver.
+ elm.scrollTop = 100;
+ elm.classList.add("contentBefore");
+
+ // Now let everything settle and all the animations run out
+ for (var i = 0; i < 60; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ utils.restoreNormalRefresh();
+
+ await promiseOnlyApzControllerFlushed();
+ is(elm.scrollTop, 100, "The scrollTop now should be y=100");
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ pushPrefs([["apz.displayport_expiry_ms", 0]])
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+}
+
+ </script>
+</head>
+<body>
+ <div id="outer">
+ <div id="inner">
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_bug1304689.html b/gfx/layers/apz/test/mochitest/test_bug1304689.html
new file mode 100644
index 0000000000..85ca3d5503
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_bug1304689.html
@@ -0,0 +1,134 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1304689
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1285070</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style type="text/css">
+ #outer {
+ height: 400px;
+ width: 415px;
+ overflow: scroll;
+ position: relative;
+ scroll-behavior: smooth;
+ }
+ #outer.instant {
+ scroll-behavior: auto;
+ }
+ #outer.contentBefore::before {
+ top: 0;
+ content: '';
+ display: block;
+ height: 2px;
+ position: absolute;
+ width: 100%;
+ z-index: 99;
+ }
+ </style>
+ <script type="application/javascript">
+
+async function test() {
+ var utils = SpecialPowers.DOMWindowUtils;
+ var elm = document.getElementById("outer");
+
+ // Set margins on the element, to ensure it is layerized
+ utils.setDisplayPortMarginsForElement(0, 0, 0, 0, elm, /* priority*/ 1);
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+
+ // Take control of the refresh driver
+ utils.advanceTimeAndRefresh(0);
+
+ // Start a smooth-scroll animation in the compositor and let it go a few
+ // frames, so that there is some "user scrolling" going on (per the comment
+ // in AsyncPanZoomController::NotifyLayersUpdated)
+ elm.scrollTop = 10;
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+
+ // Do another scroll update but also do a frame reconstruction within the same
+ // tick of the refresh driver.
+ elm.classList.add("instant");
+ elm.scrollTop = 100;
+ elm.classList.add("contentBefore");
+
+ // Now let everything settle and all the animations run out
+ for (var i = 0; i < 60; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ utils.restoreNormalRefresh();
+
+ await promiseOnlyApzControllerFlushed();
+ is(elm.scrollTop, 100, "The scrollTop now should be y=100");
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ pushPrefs([["apz.displayport_expiry_ms", 0]])
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+}
+
+ </script>
+</head>
+<body>
+ <div id="outer">
+ <div id="inner">
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_frame_reconstruction.html b/gfx/layers/apz/test/mochitest/test_frame_reconstruction.html
new file mode 100644
index 0000000000..1031701a3b
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_frame_reconstruction.html
@@ -0,0 +1,218 @@
+<!DOCTYPE html>
+<html>
+ <!--
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1235899
+ -->
+ <head>
+ <title>Test for bug 1235899</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ .outer {
+ height: 400px;
+ width: 415px;
+ overflow: hidden;
+ position: relative;
+ }
+ .inner {
+ height: 100%;
+ outline: none;
+ overflow-x: hidden;
+ overflow-y: scroll;
+ position: relative;
+ scroll-behavior: smooth;
+ }
+ .outer.contentBefore::before {
+ top: 0;
+ content: '';
+ display: block;
+ height: 2px;
+ position: absolute;
+ width: 100%;
+ z-index: 99;
+ }
+ </style>
+ </head>
+ <body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1235899">Mozilla Bug 1235899</a>
+<p id="display"></p>
+<div id="content">
+ <p>You should be able to fling this list without it stopping abruptly</p>
+ <div class="outer">
+ <div class="inner">
+ <ol>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ </ol>
+ </div>
+ </div>
+</div>
+
+<pre id="test">
+<script type="application/javascript">
+async function test() {
+ var elm = document.getElementsByClassName("inner")[0];
+ elm.scrollTop = 0;
+ await promiseOnlyApzControllerFlushed();
+
+ // Take over control of the refresh driver and compositor
+ var utils = SpecialPowers.DOMWindowUtils;
+ utils.advanceTimeAndRefresh(0);
+
+ // Kick off an APZ smooth-scroll to 0,200
+ elm.scrollTo(0, 200);
+ await promiseAllPaintsDone();
+
+ // Let's do a couple of frames of the animation, and make sure it's going
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+ await promiseOnlyApzControllerFlushed();
+ ok(elm.scrollTop > 0, "APZ animation in progress, scrollTop is now " + elm.scrollTop);
+ ok(elm.scrollTop < 200, "APZ animation not yet completed, scrollTop is now " + elm.scrollTop);
+
+ var frameReconstructionTriggered = 0;
+ // Register the listener that triggers the frame reconstruction
+ elm.onscroll = function() {
+ // Do the reconstruction
+ elm.parentNode.classList.add("contentBefore");
+ frameReconstructionTriggered++;
+ // schedule a thing to undo the changes above
+ setTimeout(function() {
+ elm.parentNode.classList.remove("contentBefore");
+ }, 0);
+ };
+
+ // and do a few more frames of the animation, this should trigger the listener
+ // and the frame reconstruction
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+ await promiseOnlyApzControllerFlushed();
+ ok(elm.scrollTop < 200, "APZ animation not yet completed, scrollTop is now " + elm.scrollTop);
+ ok(frameReconstructionTriggered > 0, "Frame reconstruction triggered, reconstruction triggered " + frameReconstructionTriggered + " times");
+
+ // and now run to completion
+ for (var i = 0; i < 100; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ utils.restoreNormalRefresh();
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+
+ is(elm.scrollTop, 200, "Element should have scrolled by 200px");
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.expectAssertions(0, 1); // this test triggers an assertion, see bug 1247050
+ waitUntilApzStable()
+ .then(test)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+}
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_bug1534549.html b/gfx/layers/apz/test/mochitest/test_group_bug1534549.html
new file mode 100644
index 0000000000..6ab8afa6f7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_bug1534549.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests for bug 1534549</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="application/javascript">
+ var touch_action_prefs = getPrefs("TOUCH_ACTION");
+
+ var subtests = [
+ // Tests that z-index ordering is respected by hit-test info.
+ { "file": "helper_touch_action_ordering_zindex.html", "prefs": touch_action_prefs },
+ // Tests that complex block/inline background ordering is respected by hit-test info.
+ { "file": "helper_touch_action_ordering_block.html", "prefs": touch_action_prefs },
+ ];
+
+ if (isApzEnabled()) {
+ ok(window.TouchEvent, "Check if TouchEvent is supported (it should be, the test harness forces it on everywhere)");
+ if (getPlatform() == "android") {
+ // This has a lot of subtests, and Android emulators are slow.
+ SimpleTest.requestLongerTimeout(2);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+ }
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_checkerboarding.html b/gfx/layers/apz/test/mochitest/test_group_checkerboarding.html
new file mode 100644
index 0000000000..5386ffd740
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_checkerboarding.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+ if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+
+
+ var prefs = [
+ ["apz.test.logging_enabled", true],
+ ["apz.displayport_expiry_ms", 0],
+ ["general.smoothScroll", false],
+ // Avoid extra paints from scrollbar fading out
+ ["layout.testing.overlay-scrollbars.always-visible", true],
+ ];
+
+ var px_ratio_1_prefs = [
+ ...prefs,
+ ["layout.css.devPixelsPerPx", 1.0],
+ ];
+
+ var zoom_and_pan_prefs = [
+ ...prefs,
+ ...getPrefs("TOUCH_EVENTS:PAN"),
+ ];
+
+ var no_multiplier_prefs = [
+ ...zoom_and_pan_prefs,
+ ["apz.x_skate_size_multiplier", "0.0"],
+ ["apz.y_skate_size_multiplier", "0.0"],
+ ["apz.x_stationary_size_multiplier", "0.0"],
+ ["apz.y_stationary_size_multiplier", "0.0"],
+ ];
+
+ var subtests = [
+ { file: "helper_checkerboard_apzforcedisabled.html", prefs },
+ { file: "helper_checkerboard_scrollinfo.html", prefs },
+ { file: "helper_horizontal_checkerboard.html", "prefs": px_ratio_1_prefs },
+ { file: "helper_checkerboard_no_multiplier.html", "prefs": no_multiplier_prefs },
+ { file: "helper_checkerboard_zoom_during_load.html", "prefs": no_multiplier_prefs },
+ { file: "helper_wide_crossorigin_iframe.html", prefs },
+ { file: "helper_reset_zoom_bug1818967.html", prefs },
+ ];
+
+ let platform = getPlatform();
+ if (platform != "windows") {
+ subtests.push(
+ { file: "helper_checkerboard_zoomoverflowhidden.html", "prefs": zoom_and_pan_prefs }
+ );
+ }
+
+ var scrollbarbutton_prefs = [
+ ...prefs,
+ ["general.smoothScroll", true],
+ // bug 1682919 only affects the main thread scrollbar button repeat codepath
+ ["apz.scrollbarbuttonrepeat.enabled", false]
+ ];
+
+ // Only Windows has scrollbar buttons in automation.
+ if (platform == "windows") {
+ subtests.push(
+ { file: "helper_scrollbarbuttonclick_checkerboard.html", "prefs": scrollbarbutton_prefs},
+ );
+ }
+
+ // Run the actual test in its own window, because it requires that the
+ // root APZC be scrollable. Mochitest pages themselves often run
+ // inside an iframe which means we have no control over the root APZC.
+ window.onload = () => {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+ }
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_displayport.html b/gfx/layers/apz/test/mochitest/test_group_displayport.html
new file mode 100644
index 0000000000..9ff8c524ad
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_displayport.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests for DisplayPorts</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="application/javascript">
+
+ // Set the displayport expiry timeout to a small value, but in order to
+ // trigger the behavior seen in bug 1758760 this value must be greater
+ // than zero.
+ let displayportExpiryPrefs = [["apz.displayport_expiry_ms", 5]];
+ let subtests = [
+ { "file": "helper_displayport_expiry.html", "prefs": displayportExpiryPrefs },
+ ];
+
+ if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+ }
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_double_tap_zoom-2.html b/gfx/layers/apz/test/mochitest/test_group_double_tap_zoom-2.html
new file mode 100644
index 0000000000..7572909386
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_double_tap_zoom-2.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various zoom-related tests that spawn in new windows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+// Increase the tap timeouts so the double-tap is still detected in case of
+// random delays during testing.
+var doubletap_prefs = [
+ ["ui.click_hold_context_menus.delay", 10000],
+ ["apz.max_tap_time", 10000],
+];
+
+var longeranimation_doubletap_prefs = [
+ ...doubletap_prefs,
+ ["apz.zoom_animation_duration_ms", 1000],
+];
+
+var logging_and_doubletap_prefs = [
+ ...doubletap_prefs,
+ ["apz.test.logging_enabled", true],
+];
+
+var disable_default_zoomin_and_doubletap_prefs = [
+ ...doubletap_prefs,
+ ["apz.doubletapzoom.defaultzoomin", "1.0"],
+];
+
+var meta_viewport_and_doubletap_prefs = [
+ ...doubletap_prefs,
+ ["dom.meta-viewport.enabled", true],
+];
+
+var subtests = [
+ {"file": "helper_doubletap_zoom_smooth.html", "prefs": longeranimation_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_fixedpos_overflow.html", "prefs": logging_and_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_hscrollable.html", "prefs": disable_default_zoomin_and_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_scrolled_overflowhidden.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_shadowdom.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_tablecell.html", "prefs": disable_default_zoomin_and_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_gencon.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_hscrollable2.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_nothing.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_noscroll.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_square.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_oopif.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_oopif-2.html", "prefs": [
+ ...meta_viewport_and_doubletap_prefs,
+ ["layout.scroll.disable-pixel-alignment", true],
+ ]},
+ {"file": "helper_disallow_doubletap_zoom_inside_oopif.html", "prefs": meta_viewport_and_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_nothing_listener.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_touch_action_manipulation.html", "prefs": meta_viewport_and_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_touch_action_manipulation_in_iframe.html", "prefs": meta_viewport_and_doubletap_prefs},
+];
+
+if (getPlatform() == "mac") {
+ subtests.push(
+ {"file": "helper_doubletap_zoom_smooth.html?touchpad", "prefs": longeranimation_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_fixedpos_overflow.html?touchpad", "prefs": logging_and_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_hscrollable.html?touchpad", "prefs": disable_default_zoomin_and_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_scrolled_overflowhidden.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_shadowdom.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_tablecell.html?touchpad", "prefs": disable_default_zoomin_and_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_gencon.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_hscrollable2.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_nothing.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_noscroll.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_square.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_oopif.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_oopif-2.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_disallow_doubletap_zoom_inside_oopif.html?touchpad", "prefs": meta_viewport_and_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_nothing_listener.html?touchpad", "prefs": [
+ ...doubletap_prefs,
+ ["layout.scroll.disable-pixel-alignment", true]]},
+ {"file": "helper_doubletap_zoom_touch_action_manipulation.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_touch_action_manipulation_in_iframe.html?touchpad", "prefs": doubletap_prefs},
+ );
+}
+
+if (isApzEnabled()) {
+ // This has a lot of subtests, and Android emulators are slow.
+ SimpleTest.requestLongerTimeout(2);
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_double_tap_zoom.html b/gfx/layers/apz/test/mochitest/test_group_double_tap_zoom.html
new file mode 100644
index 0000000000..fe4a0784a9
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_double_tap_zoom.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various zoom-related tests that spawn in new windows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+// Increase the tap timeouts so the double-tap is still detected in case of
+// random delays during testing.
+var doubletap_prefs = [
+ ["ui.click_hold_context_menus.delay", 10000],
+ ["apz.max_tap_time", 10000],
+];
+
+var logging_and_doubletap_prefs = [
+ ...doubletap_prefs,
+ ["apz.test.logging_enabled", true],
+];
+
+var subtests = [
+ {"file": "helper_doubletap_zoom.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_img.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_textarea.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_horizontal_center.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_bug1702464.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_large_overflow.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_fixedpos.html", "prefs": logging_and_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_tallwide.html", "prefs": doubletap_prefs},
+];
+
+if (getPlatform() == "mac") {
+ subtests.push(
+ {"file": "helper_doubletap_zoom.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_img.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_textarea.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_horizontal_center.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_small.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_small.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_bug1702464.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_htmlelement.html", "prefs": doubletap_prefs}, // scrollbars don't receive events or take space on android
+ {"file": "helper_doubletap_zoom_htmlelement.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_large_overflow.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_fixedpos.html?touchpad", "prefs": logging_and_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_tallwide.html?touchpad", "prefs": doubletap_prefs},
+ );
+}
+
+if (isApzEnabled()) {
+ // This has a lot of subtests, and Android emulators are slow.
+ SimpleTest.requestLongerTimeout(2);
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_fullscreen.html b/gfx/layers/apz/test/mochitest/test_group_fullscreen.html
new file mode 100644
index 0000000000..c31a3abffb
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_fullscreen.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+ if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+
+ const subtests = [
+ { file: "helper_fullscreen.html",
+ prefs: [
+ ["apz.test.logging_enabled", true],
+ ["full-screen-api.allow-trusted-requests-only", false],
+ ],
+ },
+ ];
+ // Run the actual test in its own window, because it requires that the
+ // root APZC be scrollable. Mochitest pages themselves often run
+ // inside an iframe which means we have no control over the root APZC.
+ window.onload = () => {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+ }
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_hittest-1.html b/gfx/layers/apz/test/mochitest/test_group_hittest-1.html
new file mode 100644
index 0000000000..34bf245d6c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_hittest-1.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various hit-testing tests that spawn in new windows - Part 1</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ // Turn off displayport expiry so that we don't miss failures where the
+ // displayport is set and then expires before we get around to doing the
+ // hit-test inside the activated scrollframe.
+ ["apz.displayport_expiry_ms", 0],
+ // Always layerize the scrollbar track, so as to get consistent results
+ // across platforms. Eventually we should probably get rid of this and make
+ // the tests more robust in terms of testing all the different cross-platform
+ // variations.
+ ["layout.scrollbars.always-layerize-track", true],
+ // We need this pref to allow the synthetic mouse events to propagate to APZ,
+ // and to allow the MozMouseHittest event in particular to be dispatched to
+ // APZ as a MouseInput so the hit result is recorded.
+ ["test.events.async.enabled", true],
+ // Turns on APZTestData logging which we use to obtain the hit test results.
+ ["apz.test.logging_enabled", true],
+];
+
+var overscroll_prefs = [...prefs,
+ ["apz.overscroll.enabled", true],
+ ["apz.overscroll.test_async_scroll_offset.enabled", true],
+ ];
+
+var subtests = [
+ {"file": "helper_hittest_basic.html", "prefs": prefs},
+ {"file": "helper_hittest_fixed_in_scrolled_transform.html", "prefs": prefs},
+ {"file": "helper_hittest_float_bug1434846.html", "prefs": prefs},
+ {"file": "helper_hittest_float_bug1443518.html", "prefs": prefs},
+ {"file": "helper_hittest_checkerboard.html", "prefs": prefs},
+ {"file": "helper_hittest_backface_hidden.html", "prefs": prefs},
+ {"file": "helper_hittest_touchaction.html", "prefs": prefs},
+ {"file": "helper_hittest_nested_transforms_bug1459696.html", "prefs": prefs},
+ {"file": "helper_hittest_sticky_bug1478304.html", "prefs": prefs},
+ {"file": "helper_hittest_clipped_fixed_modal.html", "prefs": prefs},
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_hittest-2.html b/gfx/layers/apz/test/mochitest/test_group_hittest-2.html
new file mode 100644
index 0000000000..d144aab840
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_hittest-2.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various hit-testing tests that spawn in new windows - Part 2</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ // Turn off displayport expiry so that we don't miss failures where the
+ // displayport is set and then expires before we get around to doing the
+ // hit-test inside the activated scrollframe.
+ ["apz.displayport_expiry_ms", 0],
+ // Always layerize the scrollbar track, so as to get consistent results
+ // across platforms. Eventually we should probably get rid of this and make
+ // the tests more robust in terms of testing all the different cross-platform
+ // variations.
+ ["layout.scrollbars.always-layerize-track", true],
+ // We need this pref to allow the synthetic mouse events to propagate to APZ,
+ // and to allow the MozMouseHittest event in particular to be dispatched to
+ // APZ as a MouseInput so the hit result is recorded.
+ ["test.events.async.enabled", true],
+ // Turns on APZTestData logging which we use to obtain the hit test results.
+ ["apz.test.logging_enabled", true],
+ // Prefs to ensure we can produce a precise amount of scrolling via
+ // synthesized touch-drag gestures.
+ ["apz.touch_start_tolerance", "0.0"],
+ ["apz.fling_min_velocity_threshold", "10000"],
+];
+
+var subtests = [
+ {"file": "helper_hittest_deep_scene_stack.html", "prefs": prefs},
+ {"file": "helper_hittest_pointerevents_svg.html", "prefs": prefs},
+ {"file": "helper_hittest_clippath.html", "prefs": prefs},
+ {"file": "helper_hittest_hoisted_scrollinfo.html", "prefs": prefs},
+ {"file": "helper_hittest_hidden_inactive_scrollframe.html", "prefs": prefs},
+ {"file": "helper_hittest_bug1715187.html", "prefs": prefs},
+ {"file": "helper_hittest_bug1715369.html", "prefs": prefs},
+ {"file": "helper_hittest_fixed.html", "prefs": prefs},
+ {"file": "helper_hittest_fixed-2.html", "prefs": prefs},
+ {"file": "helper_hittest_fixed-3.html", "prefs": prefs},
+ {"file": "helper_hittest_fixed_bg.html", "prefs": prefs},
+ {"file": "helper_hittest_fixed_item_over_oop_iframe.html", "prefs": prefs},
+ {"file": "helper_hittest_bug1730606-1.html", "prefs": prefs},
+ {"file": "helper_hittest_bug1730606-2.html", "prefs": prefs},
+ {"file": "helper_hittest_bug1730606-3.html", "prefs": prefs},
+ {"file": "helper_hittest_bug1730606-4.html", "prefs": prefs},
+ {"file": "helper_hittest_bug1257288.html", "prefs": prefs},
+ {"file": "helper_hittest_bug1119497.html", "prefs": prefs},
+ {"file": "helper_hittest_obscuration.html", "prefs": prefs},
+ // Note: iframe_perspective.html and iframe_perspective-3.html
+ // have been moved to test_group_hittest-3 due to bug 1829021.
+ {"file": "helper_hittest_iframe_perspective-2.html", "prefs": prefs},
+ // This test should be at the end, because it's prone to timeout.
+ {"file": "helper_hittest_spam.html", "prefs": prefs},
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_hittest-3.html b/gfx/layers/apz/test/mochitest/test_group_hittest-3.html
new file mode 100644
index 0000000000..f5675ee790
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_hittest-3.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various hit-testing tests that spawn in new windows - Part 3</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ // Turn off displayport expiry so that we don't miss failures where the
+ // displayport is set and then expires before we get around to doing the
+ // hit-test inside the activated scrollframe.
+ ["apz.displayport_expiry_ms", 0],
+ // Always layerize the scrollbar track, so as to get consistent results
+ // across platforms. Eventually we should probably get rid of this and make
+ // the tests more robust in terms of testing all the different cross-platform
+ // variations.
+ ["layout.scrollbars.always-layerize-track", true],
+ // We need this pref to allow the synthetic mouse events to propagate to APZ,
+ // and to allow the MozMouseHittest event in particular to be dispatched to
+ // APZ as a MouseInput so the hit result is recorded.
+ ["test.events.async.enabled", true],
+ // Turns on APZTestData logging which we use to obtain the hit test results.
+ ["apz.test.logging_enabled", true],
+ // Prefs to ensure we can produce a precise amount of scrolling via
+ // synthesized touch-drag gestures.
+ ["apz.touch_start_tolerance", "0.0"],
+ ["apz.fling_min_velocity_threshold", "10000"],
+];
+
+var subtests = [
+ {"file": "helper_hittest_iframe_perspective.html", "prefs": prefs},
+ {"file": "helper_hittest_iframe_perspective-3.html", "prefs": prefs},
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_hittest-overscroll.html b/gfx/layers/apz/test/mochitest/test_group_hittest-overscroll.html
new file mode 100644
index 0000000000..ee40c5dcdb
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_hittest-overscroll.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various hit-testing tests relevant with overscroll that spawn in new windows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ // Turn off displayport expiry so that we don't miss failures where the
+ // displayport is set and then expires before we get around to doing the
+ // hit-test inside the activated scrollframe.
+ ["apz.displayport_expiry_ms", 0],
+ // Always layerize the scrollbar track, so as to get consistent results
+ // across platforms. Eventually we should probably get rid of this and make
+ // the tests more robust in terms of testing all the different cross-platform
+ // variations.
+ ["layout.scrollbars.always-layerize-track", true],
+ // We need this pref to allow the synthetic mouse events to propagate to APZ,
+ // and to allow the MozMouseHittest event in particular to be dispatched to
+ // APZ as a MouseInput so the hit result is recorded.
+ ["test.events.async.enabled", true],
+ // Turns on APZTestData logging which we use to obtain the hit test results.
+ ["apz.test.logging_enabled", true],
+ // Prefs to ensure we can produce a precise amount of scrolling via
+ // synthesized touch-drag gestures.
+ ["apz.touch_start_tolerance", "0.0"],
+ ["apz.fling_min_velocity_threshold", "10000"],
+ // Overscroll relevant prefs.
+ ["apz.overscroll.enabled", true],
+ ["apz.overscroll.test_async_scroll_offset.enabled", true],
+];
+
+var subtests = [
+ {"file": "helper_hittest_overscroll.html", "prefs": prefs},
+ {"file": "helper_hittest_overscroll_subframe.html", "prefs": prefs},
+ {"file": "helper_hittest_overscroll_contextmenu.html", "prefs": prefs},
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_keyboard-2.html b/gfx/layers/apz/test/mochitest/test_group_keyboard-2.html
new file mode 100644
index 0000000000..248d327830
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_keyboard-2.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various keyboard scrolling tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var bug1756529_prefs= [
+ // Increase distance of line scroll (units here are "number of lines") so
+ // that a smooth scroll animation reliably takes multiple frames.
+ ["toolkit.scrollbox.verticalScrollDistance", 5],
+ // The test performs a single-tap gesture between test cases to cancel the
+ // animation from the previous test case. For test cases that run fast (e.g.
+ // instant scrolling), two single-taps could occur close enough in succession
+ // that they get interpreted as a double-tap.
+ ["apz.allow_double_tap_zooming", false]
+];
+
+var subtests = [
+ // Run helper_bug1756529.html twice, first exercising the main-thread keyboard
+ // scrolling codepaths (e.g. PresShell::ScrollPage()), and once exercising the
+ // APZ keyboard scrolling codepaths.
+ {"file": "helper_bug1756529.html", prefs: bug1756529_prefs},
+ {"file": "helper_bug1756529.html", prefs: [...bug1756529_prefs,
+ ["test.events.async.enabled", true]]},
+ {"file": "helper_tab_scroll_scrollIntoView.html"},
+];
+
+if (isKeyApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+} else {
+ SimpleTest.ok(true, "Keyboard APZ is disabled");
+}
+ </script>
+</head>
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1383365">Async key scrolling test</a>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_keyboard.html b/gfx/layers/apz/test/mochitest/test_group_keyboard.html
new file mode 100644
index 0000000000..30050366bd
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_keyboard.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various keyboard scrolling tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var subtests = [
+ {"file": "helper_key_scroll.html", prefs: [["apz.test.logging_enabled", true],
+ ["test.events.async.enabled", true]]},
+ {"file": "helper_bug1674935.html", prefs: []},
+ {"file": "helper_bug1719330.html", prefs: [["general.smoothScroll", false]]},
+ {"file": "helper_bug1695598.html"},
+ {"file": "helper_scroll_snap_on_page_down_scroll.html"},
+ {"file": "helper_scroll_snap_on_page_down_scroll.html",
+ prefs: [["test.events.async.enabled", true]]},
+ {"file": "helper_transform_end_on_keyboard_scroll.html",
+ prefs: [["general.smoothScroll", false]] },
+];
+subtests.push(...buildRelativeScrollSmoothnessVariants("key", ["scrollBy"]));
+subtests.push(...buildRelativeScrollSmoothnessVariants("native-key", ["scrollBy", "scrollTo", "scrollTop"]));
+
+if (isKeyApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+} else {
+ SimpleTest.ok(true, "Keyboard APZ is disabled");
+}
+ </script>
+</head>
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1383365">Async key scrolling test</a>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_mainthread.html b/gfx/layers/apz/test/mochitest/test_group_mainthread.html
new file mode 100644
index 0000000000..f86bf6e805
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_mainthread.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests that perform main-thread scrolling</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var subtests = [
+ {"file": "helper_scrollby_bug1531796.html"},
+ // This test checks that scroll anchoring isn't invoked just after an async
+ // scroll operation (scrolIntoView) was triggered, in other words, the async
+ // operation happened on the main-thread but it hasn't reached to APZ yet, so
+ // we don't need to set layout.css.scroll-behavior.spring-constant explicitly
+ // since the async scroll animation doesn't need to keep running there.
+ {"file": "helper_scroll_anchoring_smooth_scroll.html"},
+ {"file": "helper_scroll_anchoring_smooth_scroll_with_set_timeout.html", prefs: [
+ // Unlike helper_scroll_anchoring_smooth_scroll.html, this test checks that
+ // scroll anchoring __is__ invoked when an async scroll animation triggered
+ // by mouse wheel is running so the animation needs to keep running for a
+ // while.
+ ["layout.css.scroll-behavior.spring-constant", 10]
+ ]},
+ {"file": "helper_visualscroll_clamp_restore.html", prefs: [
+ ["apz.test.logging_enabled", true],
+ ]},
+ {"file": "helper_smoothscroll_spam.html"},
+ {"file": "helper_smoothscroll_spam_interleaved.html"},
+ {"file": "helper_mainthread_scroll_bug1662379.html", prefs: [
+ ["apz.test.logging_enabled", true],
+ ]},
+ {"file": "helper_bug1519339_hidden_smoothscroll.html"},
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_minimum_scale_size.html b/gfx/layers/apz/test/mochitest/test_group_minimum_scale_size.html
new file mode 100644
index 0000000000..2de924d6bd
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_minimum_scale_size.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script type="application/javascript" src="apz_test_utils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script type="application/javascript">
+const prefs = [
+ // We need the APZ paint logging information
+ ["apz.test.logging_enabled", true],
+ // Dropping the touch slop to 0 makes the tests easier to write because
+ // we can just do a one-pixel drag to get over the pan threshold rather
+ // than having to hard-code some larger value.
+ ["apz.touch_start_tolerance", "0.0"],
+ // The subtests in this test do touch-drags to pan the page, but we don't
+ // want those pans to turn into fling animations, so we increase the
+ // fling-min threshold velocity to an arbitrarily large value.
+ ["apz.fling_min_velocity_threshold", "10000"],
+ // The helper_bug1280013's div gets a displayport on scroll, but if the
+ // test takes too long the displayport can expire before we read the value
+ // out of the test. So we disable displayport expiry for these tests.
+ ["apz.displayport_expiry_ms", 0],
+ // Similarly, explicitly enable support for meta viewport tags (which the
+ // test cases use) so they're processed even on desktop.
+ ["dom.meta-viewport.enabled", true],
+];
+
+const subtests = [
+ { file: "helper_minimum_scale_1_0.html", prefs },
+ { file: "helper_no_scalable_with_initial_scale.html", prefs },
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ // Run the actual test in its own window, because it requires that the
+ // root APZC be scrollable. Mochitest pages themselves often run
+ // inside an iframe which means we have no control over the root APZC.
+ window.onload = () => {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_mouseevents.html b/gfx/layers/apz/test/mochitest/test_group_mouseevents.html
new file mode 100644
index 0000000000..4bd72c860c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_mouseevents.html
@@ -0,0 +1,104 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various mouse tests that spawn in new windows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+let synthesizedMouseEventsTestPrefs = [
+ // Touch activation duration must be set to a large value to ensure the test
+ // fails if touch synthesized mouse event dispatch is delayed.
+ ["ui.touch_activation.duration_ms", 90000]
+];
+
+var subtests = [
+ // Sanity test to synthesize a mouse click
+ {"file": "helper_click.html?dtc=false"},
+ // Same as above, but with a dispatch-to-content region that exercises the
+ // main-thread notification codepaths for mouse events
+ {"file": "helper_click.html?dtc=true"},
+ // Sanity test for click but with some mouse movement between the down and up
+ {"file": "helper_drag_click.html"},
+ // Test for dragging on the scrollbar of the root scrollable element works.
+ // This takes different codepaths with async zooming support enabled and
+ // disabled, and so needs to be tested separately for both.
+ {"file": "helper_drag_root_scrollbar.html", "prefs": [["apz.allow_zooming", false]]},
+ {"file": "helper_drag_root_scrollbar.html", "prefs": [["apz.allow_zooming", true]]},
+ {"file": "helper_drag_scrollbar_hittest.html", "prefs": [
+ // The test applies a scaling zoom to the page.
+ ["apz.allow_zooming", true],
+ // The test uses hitTest(), these two prefs are required for it.
+ ["test.events.async.enabled", true],
+ ["apz.test.logging_enabled", true],
+ // The test performs scrollbar dragging, avoid auto-hiding scrollbars.
+ ["ui.useOverlayScrollbars", 0]
+ ]},
+ // Test for dragging on a fake-scrollbar element that scrolls the page
+ {"file": "helper_drag_scroll.html"},
+ // Test for dragging the scrollbar with a fixed-pos element overlaying it
+ {"file": "helper_bug1346632.html"},
+ // Test for scrollbar-dragging on a scrollframe that's inactive
+ {"file": "helper_bug1326290.html"},
+ // Test for scrollbar-dragging on a scrollframe inside an SVGEffects
+ {"file": "helper_bug1331693.html"},
+ // Test for scrollbar-dragging on a transformed scrollframe inside a fixed-pos item
+ {"file": "helper_bug1462961.html"},
+ // Scrollbar dragging where we exercise the snapback behaviour by moving the
+ // mouse away from the scrollbar during drag
+ {"file": "helper_scrollbar_snap_bug1501062.html"},
+ // Tests for scrollbar-dragging on scrollframes inside nested transforms
+ {"file": "helper_bug1490393.html"},
+ {"file": "helper_bug1490393-2.html"},
+ // Scrollbar-dragging on scrollframes inside filters inside transforms
+ {"file": "helper_bug1550510.html"},
+ // Drag-select some text after reconstructing the RSF of a non-RCD to ensure
+ // the pending visual offset update doesn't get stuck
+ {"file": "helper_visualscroll_nonrcd_rsf.html"},
+ // Scrollbar-dragging on scrollframes inside nested transforms with scale
+ {"file": "helper_bug1662800.html"},
+ // Scrollbar-dragging on subframe with enclosing translation transform
+ {"file": "helper_drag_bug1719913.html"},
+ // Scrollbar dragging when pinch-zoomed in does not work with slider.snapMultiplier
+ {"file": "helper_drag_bug1794590.html", "prefs": [["slider.snapMultiplier", 6]]},
+ // Scrollbar-dragging on scrollframe containing OOP iframe when zoomed in
+ {"file": "helper_drag_bug1827330.html"},
+ // Scrolling with mouse down on the scrollbar
+ {"file": "helper_scrollbartrack_click_overshoot.html",
+ "prefs": [["test.events.async.enabled", true], ["apz.test.logging_enabled", true],
+ ["ui.useOverlayScrollbars", 0],
+ ["layout.scrollbars.click_and_hold_track.continue_to_end", false]]},
+ {"file": "helper_bug1756814.html", "prefs": [
+ ["ui.useOverlayScrollbars", 0]
+ ]},
+ {"file": "helper_touch_synthesized_mouseevents.html?scrollable=true",
+ "prefs": synthesizedMouseEventsTestPrefs},
+ {"file": "helper_touch_synthesized_mouseevents.html?scrollable=false",
+ "prefs": synthesizedMouseEventsTestPrefs},
+];
+
+// Android, mac, and linux (at least in our automation) do not have scrollbar buttons.
+if (getPlatform() == "windows") {
+ subtests.push(
+ // Basic test that click and hold on a scrollbar button works as expected
+ { file: "helper_scrollbarbutton_repeat.html", "prefs": [["general.smoothScroll", false]] }
+ );
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.expectAssertions(0, 2); // from helper_bug1550510.html, bug 1232856
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_overrides.html b/gfx/layers/apz/test/mochitest/test_group_overrides.html
new file mode 100644
index 0000000000..434c32d24e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_overrides.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various tests for event regions overrides</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ // turn off smooth scrolling so that we don't have to wait for
+ // APZ animations to finish before sampling the scroll offset
+ ["general.smoothScroll", false],
+ // Increase the content response timeout because these tests do preventDefault
+ // and we want to make sure APZ actually waits for them.
+ ["apz.content_response_timeout", 10000],
+];
+
+var subtests = [
+ {"file": "helper_override_root.html", "prefs": prefs},
+ {"file": "helper_override_subdoc.html", "prefs": prefs},
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_overscroll.html b/gfx/layers/apz/test/mochitest/test_group_overscroll.html
new file mode 100644
index 0000000000..d94cd3b65f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_overscroll.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests for overscroll</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ ["apz.overscroll.enabled", true],
+ ["apz.overscroll.test_async_scroll_offset.enabled", true],
+ ["apz.test.logging_enabled", true],
+];
+
+var subtests = [
+ {"file": "helper_overscroll_in_subscroller.html",
+ "prefs": [ ...prefs,
+ ["apz.overscroll.damping", 10.0] /* Make overscroll animations slower */ ] },
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_overscroll_handoff.html b/gfx/layers/apz/test/mochitest/test_group_overscroll_handoff.html
new file mode 100644
index 0000000000..2df4ee37b8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_overscroll_handoff.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests for overscroll handoff</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ // turn off smooth scrolling so that we don't have to wait for
+ // APZ animations to finish before sampling the scroll offset
+ ["general.smoothScroll", false],
+ ["apz.test.mac.synth_wheel_input", true],
+ // ensure that any mouse movement will trigger a new wheel transaction,
+ // because in this test we move the mouse a bunch and want to recalculate
+ // the target APZC after each such movement.
+ ["mousewheel.transaction.ignoremovedelay", 0],
+ ["mousewheel.transaction.timeout", 0],
+];
+
+var subtests = [
+ {"file": "helper_position_fixed_scroll_handoff-1.html", prefs},
+ {"file": "helper_position_fixed_scroll_handoff-2.html", prefs},
+ {"file": "helper_position_fixed_scroll_handoff-3.html", prefs},
+ {"file": "helper_position_fixed_scroll_handoff-4.html", prefs},
+ {"file": "helper_position_fixed_scroll_handoff-5.html", prefs},
+ {"file": "helper_position_sticky_scroll_handoff.html", prefs},
+ {"file": "helper_wheelevents_handoff_on_iframe.html", "prefs": prefs},
+ {"file": "helper_wheelevents_handoff_on_non_scrollable_iframe.html", "prefs": prefs},
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_pointerevents.html b/gfx/layers/apz/test/mochitest/test_group_pointerevents.html
new file mode 100644
index 0000000000..9ec03edd59
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_pointerevents.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1285070
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1285070</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ let isWindows = navigator.platform.indexOf("Win") == 0;
+ let isMac = getPlatform() == "mac";
+ var touch_action_prefs = getPrefs("TOUCH_ACTION");
+ var subtests = [
+ {"file": "helper_bug1414336.html", "prefs": [["apz.test.fails_with_native_injection", isWindows]]},
+ {"file": "helper_bug1544966_zoom_on_touch_action_none.html", "prefs": touch_action_prefs},
+ {"file": "helper_bug1648491_no_pointercancel_with_dtc.html", "prefs": touch_action_prefs},
+ {"file": "helper_bug1663731_no_pointercancel_on_second_touchstart.html",
+ "prefs": touch_action_prefs},
+ {"file": "helper_bug1682170_pointercancel_on_touchaction_pinchzoom.html",
+ "prefs": touch_action_prefs},
+ {"file": "helper_bug1719855_pointercancel_on_touchmove_after_contextmenu_prevented.html"},
+ ];
+ if (getPlatform() != "android") {
+ // Bug 1858610: these subtests are flaky on Android.
+ subtests.push(
+ {"file": "helper_bug1285070.html"},
+ {"file": "helper_bug1299195.html", "prefs": [["dom.meta-viewport.enabled", isMac]]},
+ {"file": "helper_bug1502010_unconsumed_pan.html"}
+ )
+ }
+
+ if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+ }
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_programmatic_scroll_behavior.html b/gfx/layers/apz/test/mochitest/test_group_programmatic_scroll_behavior.html
new file mode 100644
index 0000000000..13bdb4efc4
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_programmatic_scroll_behavior.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various programmatic scroll tests that spawn in new windows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+let smoothScrollEnabled = [["general.smoothScroll", true]];
+let smoothScrollDisabled = [["general.smoothScroll", false]];
+
+var subtests = [
+ {"file": "helper_programmatic_scroll_behavior.html?action=scrollIntoView", "prefs": smoothScrollEnabled},
+ {"file": "helper_programmatic_scroll_behavior.html?action=scrollIntoView", "prefs": smoothScrollDisabled},
+ {"file": "helper_programmatic_scroll_behavior.html?action=scrollBy", "prefs": smoothScrollEnabled},
+ {"file": "helper_programmatic_scroll_behavior.html?action=scrollBy", "prefs": smoothScrollDisabled},
+ {"file": "helper_programmatic_scroll_behavior.html?action=scrollTo", "prefs": smoothScrollEnabled},
+ {"file": "helper_programmatic_scroll_behavior.html?action=scrollTo", "prefs": smoothScrollDisabled},
+ {"file": "helper_programmatic_scroll_behavior.html?action=scroll", "prefs": smoothScrollEnabled},
+ {"file": "helper_programmatic_scroll_behavior.html?action=scroll", "prefs": smoothScrollDisabled},
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_scroll_linked_effect.html b/gfx/layers/apz/test/mochitest/test_group_scroll_linked_effect.html
new file mode 100644
index 0000000000..f29a382fb8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_scroll_linked_effect.html
@@ -0,0 +1,33 @@
+<!DOCTYPE>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests for scroll linked effect</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ ["apz.test.mac.synth_wheel_input", true],
+ ["apz.disable_for_scroll_linked_effects", false]
+];
+
+var subtests = [
+ {"file": "helper_scroll_linked_effect_by_wheel.html", prefs},
+ {"file": "helper_scroll_linked_effect_detector.html", prefs}
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_scroll_snap.html b/gfx/layers/apz/test/mochitest/test_group_scroll_snap.html
new file mode 100644
index 0000000000..9a1341c503
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_scroll_snap.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various tests for scroll snap</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+const prefs = [
+ ["general.smoothScroll", false],
+ // ensure that any mouse movement will trigger a new wheel transaction,
+ // because in this test we move the mouse a bunch and want to recalculate
+ // the target APZC after each such movement.
+ ["mousewheel.transaction.ignoremovedelay", 0],
+ ["mousewheel.transaction.timeout", 0],
+];
+
+const subtests = [
+ {"file": "helper_scroll_snap_no_valid_snap_position.html", "prefs": prefs},
+ {"file": "helper_scroll_snap_resnap_after_async_scroll.html",
+ // Specify a small `layout.css.scroll-behavior.spring-constant` value to
+ // keep the smooth scroll animation running long enough so that we can
+ // trigger a reflow during the animation.
+ "prefs": [["layout.css.scroll-behavior.spring-constant", 10],
+ ["apz.test.mac.synth_wheel_input", true]]},
+ {"file": "helper_scroll_snap_resnap_after_async_scroll.html",
+ "prefs": [["general.smoothScroll", false],
+ ["apz.test.mac.synth_wheel_input", true]]},
+ {"file": "helper_scroll_snap_resnap_after_async_scrollBy.html",
+ // Same as above helper_scroll_snap_resnap_after_async_scroll.html.
+ "prefs": [["layout.css.scroll-behavior.spring-constant", 10]]},
+ {"file": "helper_scroll_snap_not_resnap_during_panning.html",
+ // Specify a strong spring constant to make scroll snap animation be
+ // effective in a short span of time.
+ "prefs": [["layout.css.scroll-behavior.spring-constant", 1000]]},
+ {"file": "helper_scroll_snap_not_resnap_during_scrollbar_dragging.html",
+ // Same as above helper_scroll_snap_not_resnap_during_scrollbar_dragging.html.
+ "prefs": [["layout.css.scroll-behavior.spring-constant", 1000]]},
+ {"file": "helper_bug1780701.html"},
+ {"file": "helper_bug1783936.html",
+ // Shorten the scroll snap animation duration.
+ "prefs": [["layout.css.scroll-behavior.spring-constant", 1000],
+ // Avoid fling at the end of pan.
+ ["apz.fling_min_velocity_threshold", "10000"],
+ // This test needs mSimilateMomentum flag on headless mode.
+ ["apz.test.headless.simulate_momentum", true],
+ ["apz.gtk.kinetic_scroll.enabled", true],
+ // Use the pixel mode to make the test predictable easily.
+ ["apz.gtk.kinetic_scroll.delta_mode", 2],
+ ["apz.gtk.kinetic_scroll.pixel_delta_mode_multiplier", 1]]},
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_scrollend.html b/gfx/layers/apz/test/mochitest/test_group_scrollend.html
new file mode 100644
index 0000000000..8647fa79ad
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_scrollend.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various scrollend tests that spawn in new windows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ ["apz.test.mac.synth_wheel_input", true],
+];
+
+var smoothScrollDisabled = [
+ ...prefs,
+ ["general.smoothScroll", false],
+];
+
+var smoothMsdScrollEnabled = [
+ ...prefs,
+ ["general.smoothScroll", true],
+ ["general.smoothScroll.msdPhysics.enabled", true],
+];
+
+var smoothScrollEnabled = [
+ ...prefs,
+ ["general.smoothScroll", true],
+ ["general.smoothScroll.msdPhysics.enabled", false],
+];
+
+var subtests = [
+ {"file": "helper_basic_scrollend.html?chrome-only=true", "prefs": prefs},
+ {"file": "helper_basic_scrollend.html?chrome-only=false", "prefs": prefs},
+ {"file": "helper_scrollend_bubbles.html?scroll-target=document", "prefs": prefs},
+ {"file": "helper_scrollend_bubbles.html?scroll-target=element", "prefs": prefs},
+ {"file": "helper_main_thread_smooth_scroll_scrollend.html", "prefs": prefs},
+ {"file": "helper_scrollend_bubbles.html?scroll-target=document",
+ "prefs": smoothScrollDisabled},
+ {"file": "helper_scrollend_bubbles.html?scroll-target=element",
+ "prefs": smoothScrollDisabled},
+ {"file": "helper_main_thread_smooth_scroll_scrollend.html",
+ "prefs": smoothScrollDisabled},
+ {"file": "helper_keyboard_scrollend.html?direction=up",
+ "prefs": smoothMsdScrollEnabled},
+ {"file": "helper_keyboard_scrollend.html?direction=up",
+ "prefs": smoothScrollEnabled},
+ {"file": "helper_keyboard_scrollend.html?direction=up&flush-before-key",
+ "prefs": smoothScrollEnabled},
+ {"file": "helper_keyboard_scrollend.html?direction=up",
+ "prefs": smoothScrollDisabled},
+ {"file": "helper_keyboard_scrollend.html?direction=up&flush-before-key",
+ "prefs": smoothScrollDisabled},
+ {"file": "helper_keyboard_scrollend.html?direction=down&flush-before-key",
+ "prefs": smoothScrollEnabled},
+ {"file": "helper_keyboard_scrollend.html?direction=down&flush-before-key",
+ "prefs": smoothScrollDisabled},
+ {"file": "helper_keyboard_scrollend.html?direction=down",
+ "prefs": smoothScrollDisabled},
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_scrollframe_activation.html b/gfx/layers/apz/test/mochitest/test_group_scrollframe_activation.html
new file mode 100644
index 0000000000..2e75f45fc8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_scrollframe_activation.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1151663
+-->
+
+<head>
+ <meta charset="utf-8">
+ <title>Tests related to scrollframe activation</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="application/javascript">
+ if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+
+ let prefs = [
+ ["apz.test.logging_enabled", true]
+ ];
+
+ var subtests = [
+ { file: "helper_scrollframe_activation_on_load.html", prefs },
+ { file: "helper_bug982141.html", prefs },
+ ];
+
+ if (getPlatform() != "android") {
+ // promiseMoveMouseAndScrollWheelOver in helper_check_dp_size doesn't
+ // work on android. Trying to use promiseNativeTouchDrag on android
+ // leads to very large display ports (larger than even the bad case
+ // below), not sure why.
+ subtests.push(
+ { file: "helper_check_dp_size.html", prefs }
+ );
+ }
+
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+ }
+ </script>
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1151663">Mozilla Bug 1151663</a>
+</body>
+
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_touchevents-2.html b/gfx/layers/apz/test/mochitest/test_group_touchevents-2.html
new file mode 100644
index 0000000000..decd615795
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_touchevents-2.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various touch tests that spawn in new windows (2)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var isWindows = getPlatform() == "windows";
+
+const shared_prefs = [
+ ["apz.test.fails_with_native_injection", isWindows],
+ ["dom.w3c_touch_events.legacy_apis.enabled", true],
+];
+
+var subtests = [
+ // Taps on media elements to make sure the touchend event is delivered
+ // properly. We increase the long-tap timeout to ensure it doesn't get trip
+ // during the tap.
+ // Also this test (on Windows) cannot satisfy the OS requirement of providing
+ // an injected touch event every 100ms, because it waits for a paint between
+ // the touchstart and the touchend, so we have to use the "fake injection"
+ // code instead.
+ {"file": "helper_bug1162771.html", "prefs": [...shared_prefs,
+ ["ui.click_hold_context_menus.delay", 10000]]},
+
+ // As with the previous test, this test cannot inject touch events every 100ms
+ // because it waits for a long-tap, so we have to use the "fake injection" code
+ // instead.
+ // This test also disables synthesized mousemoves from reflow so it can make
+ // more precise assertions about the order in which events arrive.
+ {"file": "helper_long_tap.html", "prefs": [...shared_prefs,
+ ["layout.reflow.synthMouseMove", false]]},
+
+ // For the following tests, we want to make sure APZ doesn't wait for a content
+ // response that is never going to arrive. To detect this we set the content response
+ // timeout to a day, so that the entire test times out and fails if APZ does
+ // end up waiting.
+ {"file": "helper_tap_passive.html", "prefs": [...shared_prefs,
+ ["apz.content_response_timeout", 24 * 60 * 60 * 1000]]},
+
+ {"file": "helper_tap_default_passive.html", "prefs": [...shared_prefs,
+ ["apz.content_response_timeout", 24 * 60 * 60 * 1000]]},
+
+ // Add new subtests to test_group_touch_events-4.html, not this file.
+];
+
+if (isApzEnabled()) {
+ ok(window.TouchEvent, "Check if TouchEvent is supported (it should be, the test harness forces it on everywhere)");
+ if (getPlatform() == "android") {
+ // This has a lot of subtests, and Android emulators are slow.
+ SimpleTest.requestLongerTimeout(2);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_touchevents-3.html b/gfx/layers/apz/test/mochitest/test_group_touchevents-3.html
new file mode 100644
index 0000000000..a86c80a2ec
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_touchevents-3.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various touch tests that spawn in new windows (3)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var touch_action_prefs = getPrefs("TOUCH_ACTION");
+
+var subtests = [
+ // Simple test to exercise touch-action CSS property
+ {"file": "helper_touch_action.html", "prefs": touch_action_prefs},
+ // More complex touch-action tests, with overlapping regions and such
+ {"file": "helper_touch_action_complex.html", "prefs": touch_action_prefs},
+ // Tests that touch-action CSS properties are handled in APZ without waiting
+ // on the main-thread, when possible
+ {"file": "helper_touch_action_regions.html", "prefs": touch_action_prefs},
+ // Tests that touch-action inside zero-opacity items are respected
+ {"file": "helper_touch_action_zero_opacity_bug1500864.html", "prefs": touch_action_prefs},
+
+ // Add new subtests to test_group_touchevents-4.html, not this file (exceptions
+ // may be made for quick-running tests that need the touch-action prefs)
+];
+
+if (isApzEnabled()) {
+ ok(window.TouchEvent, "Check if TouchEvent is supported (it should be, the test harness forces it on everywhere)");
+ if (getPlatform() == "android") {
+ // This has a lot of subtests, and Android emulators are slow.
+ SimpleTest.requestLongerTimeout(2);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_touchevents-4.html b/gfx/layers/apz/test/mochitest/test_group_touchevents-4.html
new file mode 100644
index 0000000000..266fc72ee8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_touchevents-4.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various touch tests that spawn in new windows (4)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var touch_action_prefs = getPrefs("TOUCH_ACTION");
+
+var subtests = [
+ // clicking on element with :active::after CSS property
+ {"file": "helper_bug1473108.html"},
+ // Resetting isFirstPaint shouldn't clobber the visual viewport
+ {"file": "helper_bug1509575.html", "prefs": getPrefs("TOUCH_EVENTS:PAN")},
+ // Exercise one of the main-thread touch-action determination codepaths.
+ {"file": "helper_bug1506497_touch_action_fixed_on_fixed.html", "prefs": touch_action_prefs},
+ {"file": "helper_bug1637113_main_thread_hit_test.html"},
+ {"file": "helper_bug1638458_contextmenu.html"},
+ {"file": "helper_bug1638441_fixed_pos_hit_test.html"},
+ {"file": "helper_bug1637135_narrow_viewport.html", "prefs": [["dom.meta-viewport.enabled", true]]},
+ {"file": "helper_bug1714934_mouseevent_buttons.html"},
+
+ // Add new subtests here. If this starts timing out because it's taking too
+ // long, create a test_group_touchevents-5.html file. Refer to 1423011#c57
+ // for more details.
+ // test_group_touchevents-5.html already exists because a new test would
+ // timeout (without making any process) with fission x-origin tests if added
+ // here. So you can add tests here or in test_group_touchevents-5.html until
+ // they start timing out.
+];
+
+if (isApzEnabled()) {
+ ok(window.TouchEvent, "Check if TouchEvent is supported (it should be, the test harness forces it on everywhere)");
+ if (getPlatform() == "android") {
+ // This has a lot of subtests, and Android emulators are slow.
+ SimpleTest.requestLongerTimeout(2);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_touchevents-5.html b/gfx/layers/apz/test/mochitest/test_group_touchevents-5.html
new file mode 100644
index 0000000000..0eee77a3ae
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_touchevents-5.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various touch tests that spawn in new windows (5)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+
+var subtests = [
+ // tests that scrolling doesn't cause extra SchedulePaint calls
+ {"file": "helper_bug1669625.html", "dp_suppression": false},
+ {"file": "helper_touch_drag_root_scrollbar.html", "prefs": [["apz.allow_zooming", true]]},
+ {"file": "helper_touch_drag_root_scrollbar.html", "prefs": [["apz.allow_zooming", false]]},
+ {"file": "helper_overscroll_in_apz_test_data.html", "prefs": [
+ ["apz.overscroll.enabled", true],
+ ["apz.overscroll.test_async_scroll_offset.enabled", true],
+ ["apz.test.logging_enabled", true],
+ ]},
+ {"file": "helper_bug1719855.html?prevent=contextmenu"},
+ {"file": "helper_bug1719855.html"},
+ {"file": "helper_bug1724759.html"},
+ // Add new subtests here. If this starts timing out because it's taking too
+ // long, create a test_group_touchevents-6.html file. Refer to 1423011#c57
+ // for more details.
+ // You can still add tests to test_group_touchevents-4.html, it hasn't gotten
+ // too long yet, but this file was created because adding a specific test to
+ // test_group_touchevents-5.html would timeout (without making any progress)
+ // with fission x-origin tests. So you can add tests here or in
+ // test_group_touchevents-4.html until they start timing out.
+];
+
+if (isApzEnabled()) {
+ ok(window.TouchEvent, "Check if TouchEvent is supported (it should be, the test harness forces it on everywhere)");
+ if (getPlatform() == "android") {
+ // This has a lot of subtests, and Android emulators are slow.
+ SimpleTest.requestLongerTimeout(2);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_touchevents.html b/gfx/layers/apz/test/mochitest/test_group_touchevents.html
new file mode 100644
index 0000000000..df24e24f3d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_touchevents.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various touch tests that spawn in new windows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var basic_pan_prefs = getPrefs("TOUCH_EVENTS:PAN");
+
+var subtests = [
+ // Simple tests to exercise basic panning behaviour
+ {"file": "helper_basic_pan.html", "prefs": basic_pan_prefs},
+ {"file": "helper_div_pan.html", "prefs": basic_pan_prefs},
+ {"file": "helper_iframe_pan.html", "prefs": basic_pan_prefs},
+
+ // Simple test to exercise touch-tapping behaviour
+ {"file": "helper_tap.html"},
+ // Tapping, but with a full-zoom applied
+ {"file": "helper_tap_fullzoom.html"},
+
+ // For the following two tests, disable displayport suppression to make sure it
+ // doesn't interfere with the test by scheduling paints non-deterministically.
+ {"file": "helper_scrollto_tap.html?true",
+ "prefs": [["apz.paint_skipping.enabled", true]],
+ "dp_suppression": false},
+ {"file": "helper_scrollto_tap.html?false",
+ "prefs": [["apz.paint_skipping.enabled", false]],
+ "dp_suppression": false},
+
+ // Add new subtests to test_group_touch_events-4.html, not this file.
+];
+
+if (isApzEnabled()) {
+ ok(window.TouchEvent, "Check if TouchEvent is supported (it should be, the test harness forces it on everywhere)");
+ if (getPlatform() == "android") {
+ // This has a lot of subtests, and Android emulators are slow.
+ SimpleTest.requestLongerTimeout(2);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_wheelevents.html b/gfx/layers/apz/test/mochitest/test_group_wheelevents.html
new file mode 100644
index 0000000000..42ce15a247
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_wheelevents.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various wheel-scrolling tests that spawn in new windows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ // turn off smooth scrolling so that we don't have to wait for
+ // APZ animations to finish before sampling the scroll offset
+ ["general.smoothScroll", false],
+ // ensure that any mouse movement will trigger a new wheel transaction,
+ // because in this test we move the mouse a bunch and want to recalculate
+ // the target APZC after each such movement.
+ ["mousewheel.transaction.ignoremovedelay", 0],
+ ["mousewheel.transaction.timeout", 0],
+];
+
+// For helper_scroll_over_scrollbar, we need to set a pref to force
+// layerization of the scrollbar track to reproduce the bug being fixed.
+// Otherwise, the bug only manifests with overlay scrollbars on macOS,
+// or in a XUL RCD, both of which are hard to materialize in a test.
+var scrollbar_prefs = prefs.slice(); // make a copy
+scrollbar_prefs.push(["layout.scrollbars.always-layerize-track", true]);
+
+// For helper_overscroll_behavior_bug1425573, we need to set the APZ content
+// response timeout to 0, so we exercise the fallback codepath.
+var timeout_prefs = prefs.slice(); // make a copy
+timeout_prefs.push(["apz.content_response_timeout", 0]);
+
+var smoothness_prefs = getSmoothScrollPrefs("wheel");
+
+var subtests = [
+ {"file": "helper_scroll_on_position_fixed.html", "prefs": prefs},
+ {"file": "helper_bug1271432.html", "prefs": prefs},
+ {"file": "helper_overscroll_behavior_bug1425573.html", "prefs": timeout_prefs},
+ {"file": "helper_overscroll_behavior_bug1425603.html", "prefs": prefs},
+ {"file": "helper_overscroll_behavior_bug1494440.html", "prefs": prefs},
+ {"file": "helper_scroll_inactive_perspective.html", "prefs": prefs},
+ {"file": "helper_scroll_inactive_zindex.html", "prefs": prefs},
+ {"file": "helper_scroll_over_scrollbar.html", "prefs": scrollbar_prefs},
+ {"file": "helper_scroll_tables_perspective.html", "prefs": prefs},
+ {"file": "helper_transform_end_on_wheel_scroll.html",
+ prefs: [["general.smoothScroll", false],
+ ["apz.test.mac.synth_wheel_input", true]]},
+ {"file": "helper_scroll_anchoring_on_wheel.html", prefs: smoothness_prefs},
+];
+
+subtests.push(...buildRelativeScrollSmoothnessVariants("wheel", ["scrollBy", "scrollTo", "scrollTop"]));
+
+// Only Windows has the test api implemented for this test.
+if (getPlatform() == "windows") {
+ subtests.push(
+ {"file": "helper_dommousescroll.html", "prefs": prefs}
+ );
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_zoom-2.html b/gfx/layers/apz/test/mochitest/test_group_zoom-2.html
new file mode 100644
index 0000000000..1f94b9b5b6
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_zoom-2.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various zoom-related tests that spawn in new windows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ // We need the APZ paint logging information
+ ["apz.test.logging_enabled", true],
+ // Dropping the touch slop to 0 makes the tests easier to write because
+ // we can just do a one-pixel drag to get over the pan threshold rather
+ // than having to hard-code some larger value.
+ ["apz.touch_start_tolerance", "0.0"],
+ // The subtests in this test do touch-drags to pan the page, but we don't
+ // want those pans to turn into fling animations, so we increase the
+ // fling-min threshold velocity to an arbitrarily large value.
+ ["apz.fling_min_velocity_threshold", "10000"],
+ // The helper_bug1280013's div gets a displayport on scroll, but if the
+ // test takes too long the displayport can expire before we read the value
+ // out of the test. So we disable displayport expiry for these tests.
+ ["apz.displayport_expiry_ms", 0],
+ // Increase the content response timeout because some tests do preventDefault
+ // and we want to make sure APZ actually waits for them.
+ ["apz.content_response_timeout", 60000],
+ // Force consistent scroll-to-click behaviour across all platforms.
+ ["ui.scrollToClick", 0],
+ // Disable touch resampling so that touch events are processed without delay
+ // and we don't zoom more than expected due to overprediction.
+ ["android.touch_resampling.enabled", false],
+];
+
+var instant_repaint_prefs = [
+ // When zooming, trigger repaint requests for each scale event rather than
+ // delaying the repaints
+ ["apz.scale_repaint_delay_ms", 0],
+ ... prefs
+];
+
+var subtests = [
+ {"file": "helper_bug1280013.html", "prefs": prefs},
+ {"file": "helper_zoom_restore_position_tabswitch.html", "prefs": prefs},
+ {"file": "helper_zoom_with_dynamic_toolbar.html", "prefs": prefs},
+ {"file": "helper_visual_scrollbars_pagescroll.html", "prefs": prefs},
+ {"file": "helper_click_interrupt_animation.html", "prefs": prefs},
+ {"file": "helper_overflowhidden_zoom.html", "prefs": prefs},
+ {"file": "helper_zoom_keyboardscroll.html", "prefs": prefs},
+ {"file": "helper_zoom_out_clamped_scrollpos.html", "prefs": instant_repaint_prefs},
+ {"file": "helper_zoom_out_with_mainthread_clamping.html", "prefs": instant_repaint_prefs},
+ {"file": "helper_fixed_html_hittest.html", "prefs": prefs},
+ // {"file": "helper_zoom_oopif.html", "prefs": prefs}, // disabled, see bug 1716127
+ {"file": "helper_zoom_after_gpu_process_restart.html", "prefs": prefs}
+];
+
+if (isApzEnabled()) {
+ // This has a lot of subtests, and Android emulators are slow.
+ SimpleTest.requestLongerTimeout(2);
+ SimpleTest.waitForExplicitFinish();
+
+ if (getPlatform() == "linux") {
+ subtests.push(
+ {"file": "helper_zoom_with_touchpad.html", "prefs": prefs},
+ {"file": "helper_touchpad_pinch_and_pan.html", "prefs": prefs},
+ {"file": "helper_zoom_oopif.html?touchpad", "prefs": prefs},
+ );
+ }
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_zoom.html b/gfx/layers/apz/test/mochitest/test_group_zoom.html
new file mode 100644
index 0000000000..03f9bbebf8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_zoom.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various zoom-related tests that spawn in new windows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ // We need the APZ paint logging information
+ ["apz.test.logging_enabled", true],
+ // Dropping the touch slop to 0 makes the tests easier to write because
+ // we can just do a one-pixel drag to get over the pan threshold rather
+ // than having to hard-code some larger value.
+ ["apz.touch_start_tolerance", "0.0"],
+ // The subtests in this test do touch-drags to pan the page, but we don't
+ // want those pans to turn into fling animations, so we increase the
+ // fling-min threshold velocity to an arbitrarily large value.
+ ["apz.fling_min_velocity_threshold", "10000"],
+ // The helper_bug1280013's div gets a displayport on scroll, but if the
+ // test takes too long the displayport can expire before we read the value
+ // out of the test. So we disable displayport expiry for these tests.
+ ["apz.displayport_expiry_ms", 0],
+ // Increase the content response timeout because some tests do preventDefault
+ // and we want to make sure APZ actually waits for them.
+ ["apz.content_response_timeout", 60000],
+ // Disable touch resampling so that touch events are processed without delay
+ // and we don't zoom more than expected due to overprediction.
+ ["android.touch_resampling.enabled", false],
+];
+
+// Increase the tap timeouts so the one-touch-pinch gesture is still detected
+// in case of random delays during testing. Also ensure that the feature is
+// actually enabled (which it should be by default, but it's good to be safe).
+var onetouchpinch_prefs = [
+ ...prefs,
+ ["apz.one_touch_pinch.enabled", true],
+ ["ui.click_hold_context_menus.delay", 10000],
+ ["apz.max_tap_time", 10000],
+];
+
+// For helper_fixed_pos_displayport the mechanism we use to record the
+// fixed-pos displayport only takes effect when not in a partial display list
+// update. So for this test we just disable retained display lists entirely.
+var no_rdl_prefs = [
+ ...prefs,
+ ["layout.display-list.retain", false],
+];
+
+var subtests = [
+ {"file": "helper_basic_zoom.html", "prefs": prefs},
+ {"file": "helper_basic_onetouchpinch.html", "prefs": onetouchpinch_prefs},
+ {"file": "helper_zoom_prevented.html", "prefs": prefs},
+ {"file": "helper_zoomed_pan.html", "prefs": prefs},
+ {"file": "helper_fixed_position_scroll_hittest.html", "prefs": prefs},
+ {"file": "helper_onetouchpinch_nested.html", "prefs": onetouchpinch_prefs},
+ {"file": "helper_visual_smooth_scroll.html", "prefs": prefs},
+ {"file": "helper_scroll_into_view_bug1516056.html", "prefs": prefs},
+ {"file": "helper_scroll_into_view_bug1562757.html", "prefs": prefs},
+ {"file": "helper_fixed_pos_displayport.html", "prefs": no_rdl_prefs},
+ // If you're adding more tests, add them to test_group_zoom-2.html
+];
+
+if (isApzEnabled()) {
+ // This has a lot of subtests, and Android emulators are slow.
+ SimpleTest.requestLongerTimeout(2);
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_zoomToFocusedInput.html b/gfx/layers/apz/test/mochitest/test_group_zoomToFocusedInput.html
new file mode 100644
index 0000000000..23a472f7b4
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_zoomToFocusedInput.html
@@ -0,0 +1,60 @@
+<!DOCTYPE>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various zoomToFocusedInput tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var subtests = [
+ {"file": "helper_zoomToFocusedInput_scroll.html"},
+ {"file": "helper_zoomToFocusedInput_multiline.html"},
+ {"file": "helper_zoomToFocusedInput_iframe.html"},
+ {"file": "helper_zoomToFocusedInput_iframe.html?cross-origin"},
+ {"file": "helper_zoomToFocusedInput_iframe-2.html",
+ "prefs": [["dom.meta-viewport.enabled", true],
+ ["layout.scroll.disable-pixel-alignment", true]]},
+ {"file": "helper_zoomToFocusedInput_fixed_bug1673511.html"},
+ {"file": "helper_zoomToFocusedInput_nozoom_bug1738696.html"},
+ {"file": "helper_zoomToFocusedInput_nested_position_fixed.html"},
+ {"file": "helper_zoomToFocusedInput_zoom_in_position_fixed.html",
+ "prefs": [["dom.meta-viewport.enabled", true], ["formhelper.autozoom", true]],
+ },
+];
+
+// These tests rely on mobile viewport sizing, so only run them on
+// mobile for now. In the future we can consider running them on
+// on desktop, but only in configurations with overlay scrollbars
+// (see bug 1608506).
+let platform = getPlatform();
+if (platform == "android") {
+ subtests.push(
+ {"file": "helper_zoomToFocusedInput_nozoom.html"}
+ );
+ subtests.push(
+ {"file": "helper_zoomToFocusedInput_zoom.html"}
+ );
+ subtests.push(
+ {"file": "helper_zoomToFocusedInput_touch-action.html"}
+ );
+ subtests.push(
+ {"file": "helper_zoomToFocusedInput_dynamic_toolbar_bug1828235.html"}
+ );
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_interrupted_reflow.html b/gfx/layers/apz/test/mochitest/test_interrupted_reflow.html
new file mode 100644
index 0000000000..8fc72e05a5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_interrupted_reflow.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+ <!--
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1292781
+ -->
+ <head>
+ <title>Test for bug 1292781</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ </head>
+ <body>
+<script type="text/javascript">
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+
+ // Run the actual test in its own window, because it is supposed to be run
+ // in the top level content document to collect APZ test data up to the root
+ // content APZC by using nsIDOMWindowUtils.getCompositorAPZTestData which
+ // is not able to walk up to the root content APZC if the function gets called
+ // from OOP iframes. We could make it work across process boundaries, but so
+ // far running the test in a new top level content document would be fine.
+ var w = null;
+ window.onload = async () => {
+ await pushPrefs([["apz.test.logging_enabled", true],
+ ["apz.displayport_expiry_ms", 0]]);
+
+ w = window.open("helper_interrupted_reflow.html", "_blank");
+ };
+}
+
+function finishTest() {
+ w.close();
+ SimpleTest.finish();
+}
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_layerization.html b/gfx/layers/apz/test/mochitest/test_layerization.html
new file mode 100644
index 0000000000..0ff76de317
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_layerization.html
@@ -0,0 +1,312 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1173580
+-->
+<head>
+ <title>Test for layerization</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <link rel="stylesheet" type="text/css" href="helper_subframe_style.css"/>
+ <style>
+ #container {
+ display: flex;
+ overflow: scroll;
+ height: 500px;
+ }
+ .outer-frame {
+ height: 500px;
+ overflow: scroll;
+ flex-basis: 100%;
+ background: repeating-linear-gradient(#CCC, #CCC 100px, #BBB 100px, #BBB 200px);
+ }
+ #container-content {
+ height: 200%;
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1173580">APZ layerization tests</a>
+<p id="display"></p>
+<div id="container">
+ <div id="outer1" class="outer-frame">
+ <div id="inner1" class="inner-frame">
+ <div class="inner-content"></div>
+ </div>
+ </div>
+ <div id="outer2" class="outer-frame">
+ <div id="inner2" class="inner-frame">
+ <div class="inner-content"></div>
+ </div>
+ </div>
+ <iframe id="outer3" class="outer-frame" src="helper_iframe1.html"></iframe>
+ <iframe id="outer4" class="outer-frame" src="helper_iframe2.html"></iframe>
+<!-- The container-content div ensures 'container' is scrollable, so the
+ optimization that layerizes the primary async-scrollable frame on page
+ load layerizes it rather than its child subframes. -->
+ <div id="container-content"></div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+// Scroll the mouse wheel over |element|.
+async function scrollWheelOver(element) {
+ await promiseMoveMouseAndScrollWheelOver(element, 10, 10, /* waitForScroll = */ false);
+}
+
+const DISPLAYPORT_EXPIRY = 100;
+
+let config = getHitTestConfig();
+let activateAllScrollFrames = config.activateAllScrollFrames;
+
+let heightMultiplier = SpecialPowers.getCharPref("apz.y_stationary_size_multiplier");
+// With WebRender, the effective height multiplier can be reduced
+// for alignment reasons. The reduction should be no more than a
+// factor of two.
+heightMultiplier /= 2;
+info("effective displayport height multipler is " + heightMultiplier);
+
+function hasNonZeroMarginDisplayPort(elementId, containingDoc = null) {
+ let dp = getLastContentDisplayportFor(elementId);
+ if (dp == null) {
+ return false;
+ }
+ let elt = (containingDoc != null ? containingDoc : document).getElementById(elementId);
+ info(elementId);
+ info("window size " + window.innerWidth + " " + window.innerHeight);
+ info("dp " + dp.x + " " + dp.y + " " + dp.width + " " + dp.height);
+ info("eltsize " + elt.clientWidth + " " + elt.clientHeight);
+ return dp.height >= heightMultiplier * Math.min(elt.clientHeight, window.innerHeight);
+}
+
+function hasMinimalDisplayPort(elementId, containingDoc = null) {
+ let dp = getLastContentDisplayportFor(elementId);
+ if (dp == null) {
+ return false;
+ }
+ let elt = (containingDoc != null ? containingDoc : document).getElementById(elementId);
+ info(elementId);
+ info("dp " + dp.x + " " + dp.y + " " + dp.width + " " + dp.height);
+ info("eltsize " + elt.clientWidth + " " + elt.clientHeight);
+ return dp.width <= (elt.clientWidth + 2) && dp.height <= (elt.clientHeight + 2);
+}
+
+function checkDirectActivation(elementId, containingDoc = null) {
+ if (activateAllScrollFrames) {
+ return hasNonZeroMarginDisplayPort(elementId, containingDoc);
+ }
+ return isLayerized(elementId);
+
+}
+
+function checkAncestorActivation(elementId, containingDoc = null) {
+ if (activateAllScrollFrames) {
+ return hasMinimalDisplayPort(elementId, containingDoc);
+ }
+ return isLayerized(elementId);
+
+}
+
+function checkInactive(elementId, containingDoc = null) {
+ if (activateAllScrollFrames) {
+ return hasMinimalDisplayPort(elementId, containingDoc);
+ }
+ return !isLayerized(elementId);
+
+}
+
+async function test() {
+ await SpecialPowers.pushPrefEnv({
+ "set": [
+ // Causes the test to intermittently fail on ASAN opt linux.
+ ["mousewheel.system_scroll_override.enabled", false],
+ ]
+ });
+
+ let outer3Doc = document.getElementById("outer3").contentDocument;
+ let outer4Doc = document.getElementById("outer4").contentDocument;
+
+ // Initially, everything should be inactive.
+ ok(checkInactive("outer1"), "initially 'outer1' should not be active");
+ ok(checkInactive("inner1"), "initially 'inner1' should not be active");
+ ok(checkInactive("outer2"), "initially 'outer2' should not be active");
+ ok(checkInactive("inner2"), "initially 'inner2' should not be active");
+ ok(checkInactive("outer3"), "initially 'outer3' should not be active");
+ ok(checkInactive("inner3", outer3Doc),
+ "initially 'inner3' should not be active");
+ ok(checkInactive("outer4"), "initially 'outer4' should not be active");
+ ok(checkInactive("inner4", outer4Doc),
+ "initially 'inner4' should not be active");
+
+ // Scrolling over outer1 should activate outer1 directly, but not inner1.
+ await scrollWheelOver(document.getElementById("outer1"));
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ ok(checkDirectActivation("outer1"),
+ "scrolling 'outer1' should activate it directly");
+ ok(checkInactive("inner1"),
+ "scrolling 'outer1' should not cause 'inner1' to get activated");
+
+ // Scrolling over inner2 should activate inner2 directly, but outer2 only ancestrally.
+ await scrollWheelOver(document.getElementById("inner2"));
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ ok(checkDirectActivation("inner2"),
+ "scrolling 'inner2' should cause it to be directly activated");
+ ok(checkAncestorActivation("outer2"),
+ "scrolling 'inner2' should cause 'outer2' to be activated as an ancestor");
+
+ // The second half of the test repeats the same checks as the first half,
+ // but with an iframe as the outer scrollable frame.
+
+ // Scrolling over outer3 should activate outer3 directly, but not inner3.
+ await scrollWheelOver(outer3Doc.documentElement);
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ ok(checkDirectActivation("outer3"), "scrolling 'outer3' should cause it to be directly activated");
+ ok(checkInactive("inner3", outer3Doc),
+ "scrolling 'outer3' should not cause 'inner3' to be activated");
+
+ // Scrolling over inner4 should activate inner4 directly, but outer4 only ancestrally.
+ await scrollWheelOver(outer4Doc.getElementById("inner4"));
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ ok(checkDirectActivation("inner4", outer4Doc),
+ "scrolling 'inner4' should cause it to be directly activated");
+ ok(checkAncestorActivation("outer4"),
+ "scrolling 'inner4' should cause 'outer4' to be activated");
+
+ // Now we enable displayport expiry, and verify that things are still
+ // activated as they were before.
+ await SpecialPowers.pushPrefEnv({"set": [["apz.displayport_expiry_ms", DISPLAYPORT_EXPIRY]]});
+ ok(checkDirectActivation("outer1"), "outer1 still has non zero display port after enabling expiry");
+ ok(checkInactive("inner1"), "inner1 is still has zero margin display port after enabling expiry");
+ ok(checkAncestorActivation("outer2"), "outer2 still has zero margin display port after enabling expiry");
+ ok(checkDirectActivation("inner2"), "inner2 still has non zero display port after enabling expiry");
+ ok(checkDirectActivation("outer3"), "outer3 still has non zero display port after enabling expiry");
+ ok(checkInactive("inner3", outer3Doc),
+ "inner3 still has zero margin display port after enabling expiry");
+ ok(checkDirectActivation("inner4", outer4Doc),
+ "inner4 still has non zero display port after enabling expiry");
+ ok(checkAncestorActivation("outer4"), "outer4 still has zero margin display port after enabling expiry");
+
+ // Now we trigger a scroll on some of the things still layerized, so that
+ // the displayport expiry gets triggered.
+
+ // Expire displayport with scrolling on outer1
+ await scrollWheelOver(document.getElementById("outer1"));
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ await SpecialPowers.promiseTimeout(DISPLAYPORT_EXPIRY);
+ await promiseAllPaintsDone();
+ ok(checkInactive("outer1"), "outer1 is inactive after displayport expiry");
+ ok(checkInactive("inner1"), "inner1 is inactive after displayport expiry");
+
+ // Expire displayport with scrolling on inner2
+ await scrollWheelOver(document.getElementById("inner2"));
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ // Once the expiry elapses, it will trigger expiry on outer2, so we check
+ // both, one at a time.
+ await SpecialPowers.promiseTimeout(DISPLAYPORT_EXPIRY);
+ await promiseAllPaintsDone();
+ ok(checkInactive("inner2"), "inner2 is inactive after displayport expiry");
+ await SpecialPowers.promiseTimeout(DISPLAYPORT_EXPIRY);
+ await promiseAllPaintsDone();
+ ok(checkInactive("outer2"), "outer2 is inactive with inner2");
+
+ // We need to wrap the next bit in a loop and keep retrying until it
+ // succeeds. Let me explain why this is the best option at this time. Below
+ // we scroll over inner3, this triggers a 100 ms timer to expire it's display
+ // port. Then when it expires it schedules a paint and triggers another
+ // 100 ms timer on it's parent, outer3, to expire. The paint needs to happen
+ // before the timer fires because the paint is what updates
+ // mIsParentToActiveScrollFrames on outer3, and mIsParentToActiveScrollFrames
+ // being true blocks a display port from expiring. It was true because it
+ // contained inner3, but no longer. In real life the timer is 15000 ms so a
+ // paint will happen, but here in a test the timer is 100 ms so that paint
+ // can not happen in time. We could add some more complication to this code
+ // just for this test, or we could just loop here.
+ let itWorked = false;
+ while (!itWorked) {
+ // Scroll on inner3. inner3 isn't layerized, and this will cause it to
+ // get layerized, but it will also trigger displayport expiration for inner3
+ // which will eventually trigger displayport expiration on inner3 and outer3.
+ // Note that the displayport expiration might actually happen before the wheel
+ // input is processed in the compositor (see bug 1246480 comment 3), and so
+ // we make sure not to wait for a scroll event here, since it may never fire.
+ // However, if we do get a scroll event while waiting for the expiry, we need
+ // to restart the expiry timer because the displayport expiry got reset. There's
+ // no good way that I can think of to deterministically avoid doing this.
+ let inner3 = outer3Doc.getElementById("inner3");
+ await scrollWheelOver(inner3);
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ let timerPromise = new Promise(resolve => {
+ var timeoutTarget = function() {
+ inner3.removeEventListener("scroll", timeoutResetter);
+ resolve();
+ };
+ var timerId = setTimeout(timeoutTarget, DISPLAYPORT_EXPIRY);
+ var timeoutResetter = function() {
+ ok(true, "Got a scroll event; resetting timer...");
+ clearTimeout(timerId);
+ setTimeout(timeoutTarget, DISPLAYPORT_EXPIRY);
+ // by not updating timerId we ensure that this listener resets the timeout
+ // at most once.
+ };
+ inner3.addEventListener("scroll", timeoutResetter);
+ });
+ await timerPromise; // wait for the setTimeout to elapse
+
+ await promiseAllPaintsDone();
+ ok(checkInactive("inner3", outer3Doc),
+ "inner3 is inactive after expiry");
+ await SpecialPowers.promiseTimeout(DISPLAYPORT_EXPIRY);
+ await promiseAllPaintsDone();
+ if (checkInactive("outer3")) {
+ ok(true, "outer3 is inactive after inner3 triggered expiry");
+ itWorked = true;
+ }
+ }
+
+ // Scroll outer4 and wait for the expiry. It should NOT get expired because
+ // inner4 is still layerized
+ await scrollWheelOver(outer4Doc.documentElement);
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ // Wait for the expiry to elapse
+ await SpecialPowers.promiseTimeout(DISPLAYPORT_EXPIRY);
+ await promiseAllPaintsDone();
+ ok(checkDirectActivation("inner4", outer4Doc),
+ "inner4 still is directly activated because it never expired");
+ ok(checkDirectActivation("outer4"),
+ "outer4 still still is directly activated because inner4 is still layerized");
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.requestFlakyTimeout("we are testing code that measures an actual timeout");
+ SimpleTest.expectAssertions(0, 8); // we get a bunch of "ASSERTION: Bounds computation mismatch" sometimes (bug 1232856)
+
+ // Disable smooth scrolling, because it results in long-running scroll
+ // animations that can result in a 'scroll' event triggered by an earlier
+ // wheel event as corresponding to a later wheel event.
+ // Also enable APZ test logging, since we use that data to determine whether
+ // a scroll frame was layerized.
+ pushPrefs([["general.smoothScroll", false],
+ ["apz.displayport_expiry_ms", 0],
+ ["apz.test.logging_enabled", true]])
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_relative_update.html b/gfx/layers/apz/test/mochitest/test_relative_update.html
new file mode 100644
index 0000000000..01c0ee1f9b
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_relative_update.html
@@ -0,0 +1,92 @@
+<!DOCTYPE HTML>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1453425
+-->
+<html>
+<head>
+ <title>Test for relative scroll offset updates (Bug 1453425)</title>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <style type="text/css">
+ #frame {
+ width: 200px;
+ height: 400px;
+ overflow: scroll;
+ border: 1px solid black;
+ }
+ #first {
+ width: 200px;
+ height: 108px;
+ background-color: red;
+ }
+ #second {
+ width: 200px;
+ height: 692px;
+ background-color: green;
+ }
+ </style>
+</head>
+<body>
+ <div id="frame">
+ <div id="first"></div>
+ <div id="second"></div>
+ </div>
+<script type="application/javascript">
+async function test() {
+ var utils = SpecialPowers.DOMWindowUtils;
+
+ var elm = document.querySelector("#frame");
+ // Set a zero-margin displayport to ensure that the element is async-scrollable
+ utils.setDisplayPortMarginsForElement(0, 0, 0, 0, elm, 0);
+ elm.scrollTop = 0;
+
+ // Take over control of the refresh driver and don't allow a layer
+ // transaction until the main thread and APZ have processed two different
+ // scrolls.
+ await promiseApzFlushedRepaints();
+ utils.advanceTimeAndRefresh(0);
+
+ // Scroll instantly on the main thread by (0, 100).
+ elm.scrollBy(0, 100);
+
+ // We are not using `scroll-behavior`
+ is(elm.scrollTop, 100, "the main thread scroll should be instant");
+
+ // Dispatch a wheel event to have APZ scroll by (0, 8). Wait for the wheel
+ // event to ensure that the APZ has processed the scroll.
+ await promiseNativeWheelAndWaitForWheelEvent(elm, 40, 40, 0, -8);
+
+ // APZ should be handling the wheel scroll
+ is(elm.scrollTop, 100, "the wheel scroll should be handled by APZ");
+
+ // Restore control of the refresh driver, allowing the main thread to send a
+ // layer transaction containing the (0, 100) scroll.
+ utils.restoreNormalRefresh();
+
+ // Wait for all paints to finish and for the main thread to receive pending
+ // repaint requests with the scroll offset from the wheel event.
+ await promiseApzFlushedRepaints();
+
+ // The main thread scroll should not have overidden the APZ scroll, and we
+ // should see the effects of both scrolls.
+ ok(elm.scrollTop > 100, `expected element.scrollTop > 100. got element.scrollTop = ${elm.scrollTop}`);
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ // Receiving a relative scroll offset update can cause scroll animations to
+ // be cancelled. This should be fixed, but for now we can still test this
+ // by disabling smooth scrolling.
+ pushPrefs([["general.smoothScroll", false]])
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+}
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_scroll_inactive_bug1190112.html b/gfx/layers/apz/test/mochitest/test_scroll_inactive_bug1190112.html
new file mode 100644
index 0000000000..de54cf93fe
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_scroll_inactive_bug1190112.html
@@ -0,0 +1,553 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test scrolling flattened inactive frames</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+<style>
+p {
+ width:200px;
+ height:200px;
+ border:solid 1px black;
+ overflow:auto;
+}
+</style>
+</head>
+<body>
+<div id="iframe-body" style="overflow: auto; height: 1000px">
+<hr>
+<hr>
+<hr>
+<p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p id="subframe">
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p>
+</div>
+<script clss="testbody" type="text/javascript">
+function ScrollTops() {
+ this.outerScrollTop = document.getElementById("iframe-body").scrollTop;
+ this.innerScrollTop = document.getElementById("subframe").scrollTop;
+}
+
+var DefaultEvent = {
+ deltaMode: WheelEvent.DOM_DELTA_LINE,
+ deltaX: 0, deltaY: 1,
+ lineOrPageDeltaX: 0, lineOrPageDeltaY: 1,
+};
+
+async function test() {
+ var subframe = document.getElementById("subframe");
+ var oldpos = new ScrollTops();
+ await new Promise(resolve => {
+ sendWheelAndPaint(subframe, 10, 10, DefaultEvent, resolve);
+ });
+
+ var newpos = new ScrollTops();
+ ok(oldpos.outerScrollTop == newpos.outerScrollTop, "viewport should not have scrolled");
+ ok(oldpos.innerScrollTop != newpos.innerScrollTop, "subframe should have scrolled");
+ oldpos = newpos;
+
+ // Scroll outer
+ var outer = document.getElementById("iframe-body");
+ await new Promise(resolve => {
+ sendWheelAndPaint(outer, 20, 5, DefaultEvent, resolve);
+ });
+
+ newpos = new ScrollTops();
+ ok(oldpos.outerScrollTop != newpos.outerScrollTop, "viewport should have scrolled");
+ ok(oldpos.innerScrollTop == newpos.innerScrollTop, "subframe should not have scrolled");
+ oldpos = newpos;
+
+ // Scroll inner again
+ // Tick the refresh driver once to make sure the compositor has sent the
+ // updated scroll offset for the outer scroller to WebRender, so that the
+ // hit-test in sendWheelAndPaint takes it into account. (This isn't needed
+ // if using non-WR layers, but doesn't hurt either).
+ var dwu = SpecialPowers.getDOMWindowUtils(window);
+ dwu.advanceTimeAndRefresh(16);
+ dwu.restoreNormalRefresh();
+
+ await new Promise(resolve => {
+ sendWheelAndPaint(subframe, 10, 10, DefaultEvent, resolve);
+ });
+
+ newpos = new ScrollTops();
+ ok(oldpos.outerScrollTop == newpos.outerScrollTop, "viewport should not have scrolled");
+ ok(oldpos.innerScrollTop != newpos.innerScrollTop, "subframe should have scrolled");
+}
+
+SimpleTest.waitForExplicitFinish();
+
+pushPrefs([["general.smoothScroll", false],
+ ["mousewheel.transaction.timeout", 0],
+ ["mousewheel.transaction.ignoremovedelay", 0],
+ ["test.events.async.enabled", true]])
+.then(waitUntilApzStable)
+.then(test)
+.then(SimpleTest.finish, SimpleTest.finishWithFailure);
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_scroll_inactive_flattened_frame.html b/gfx/layers/apz/test/mochitest/test_scroll_inactive_flattened_frame.html
new file mode 100644
index 0000000000..47207cbb9f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_scroll_inactive_flattened_frame.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test scrolling flattened inactive frames</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="container" style="height: 300px; width: 600px; overflow: auto; background: yellow">
+ <div id="outer" style="height: 400px; width: 500px; overflow: auto; background: black">
+ <div id="inner" style="mix-blend-mode: screen; height: 800px; overflow: auto; background: purple">
+ </div>
+ </div>
+</div>
+<script class="testbody" type="text/javascript">
+async function test() {
+ var container = document.getElementById("container");
+ var outer = document.getElementById("outer");
+ var inner = document.getElementById("inner");
+ var outerScrollTop = outer.scrollTop;
+ var containerScrollTop = container.scrollTop;
+ var event = {
+ deltaMode: WheelEvent.DOM_DELTA_LINE,
+ deltaX: 0,
+ deltaY: 10,
+ lineOrPageDeltaX: 0,
+ lineOrPageDeltaY: 10,
+ };
+ await new Promise(resolve => {
+ sendWheelAndPaint(inner, 20, 30, event, resolve);
+ });
+ ok(container.scrollTop == containerScrollTop, "container scrollframe should not have scrolled");
+ ok(outer.scrollTop > outerScrollTop, "nested scrollframe should have scrolled");
+}
+
+SimpleTest.waitForExplicitFinish();
+
+pushPrefs([["general.smoothScroll", false],
+ ["mousewheel.transaction.timeout", 1000000],
+ ["test.events.async.enabled", true]])
+.then(waitUntilApzStable)
+.then(test)
+.then(SimpleTest.finish, SimpleTest.finishWithFailure);
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_scroll_subframe_scrollbar.html b/gfx/layers/apz/test/mochitest/test_scroll_subframe_scrollbar.html
new file mode 100644
index 0000000000..10d53e9d04
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_scroll_subframe_scrollbar.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test scrolling subframe scrollbars</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+<style>
+p {
+ width:200px;
+ height:200px;
+ border:solid 1px black;
+}
+</style>
+</head>
+<body>
+<p id="subframe">
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+</p>
+<script clss="testbody" type="text/javascript">
+
+var DefaultEvent = {
+ deltaMode: WheelEvent.DOM_DELTA_LINE,
+ deltaX: 0, deltaY: 1,
+ lineOrPageDeltaX: 0, lineOrPageDeltaY: 1,
+};
+
+var ScrollbarWidth = 0;
+
+async function test() {
+ var subframe = document.getElementById("subframe");
+ var oldClientWidth = subframe.clientWidth;
+
+ subframe.style.overflow = "auto";
+ subframe.getBoundingClientRect();
+
+ await promiseAllPaintsDone(null, /*flush=*/true);
+
+ ScrollbarWidth = oldClientWidth - subframe.clientWidth;
+ if (!ScrollbarWidth) {
+ // Probably we have overlay scrollbars - abort the test.
+ ok(true, "overlay scrollbars - skipping test");
+ return;
+ }
+
+ ok(subframe.scrollHeight > subframe.clientHeight, "subframe should have scrollable content");
+
+ // Send a wheel event roughly to where we think the trackbar is. We pick a
+ // point at the bottom, in the middle of the trackbar, where the slider is
+ // unlikely to be (since it starts at the top).
+ var posX = subframe.clientWidth + (ScrollbarWidth / 2);
+ var posY = subframe.clientHeight - 20;
+
+ var oldScrollTop = subframe.scrollTop;
+
+ await new Promise(resolve => {
+ sendWheelAndPaint(subframe, posX, posY, DefaultEvent, resolve);
+ });
+
+ ok(subframe.scrollTop > oldScrollTop, "subframe should have scrolled");
+}
+
+SimpleTest.waitForExplicitFinish();
+
+pushPrefs([["general.smoothScroll", false],
+ ["mousewheel.transaction.timeout", 0],
+ ["mousewheel.transaction.ignoremovedelay", 0],
+ ["test.events.async.enabled", true]])
+.then(waitUntilApzStable)
+.then(test)
+.then(SimpleTest.finish, SimpleTest.finishWithFailure);
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_smoothness.html b/gfx/layers/apz/test/mochitest/test_smoothness.html
new file mode 100644
index 0000000000..64cb8bcefa
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_smoothness.html
@@ -0,0 +1,67 @@
+<html>
+<head>
+ <title>Test Frame Uniformity While Scrolling</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+
+ <style>
+ #content {
+ height: 5000px;
+ background: repeating-linear-gradient(#EEE, #EEE 100px, #DDD 100px, #DDD 200px);
+ }
+ </style>
+ <script type="text/javascript">
+ var scrollEvents = 100;
+ var i = 0;
+ // Scroll points
+ var x = 100;
+ var y = 150;
+
+ SimpleTest.waitForExplicitFinish();
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+
+ async function sendScrollEvent(aRafTimestamp) {
+ var scrollDiv = document.getElementById("content");
+
+ if (i < scrollEvents) {
+ i++;
+ // Scroll diff
+ var dx = 0;
+ var dy = -10; // Negative to scroll down
+ await synthesizeNativeWheel(scrollDiv, x, y, dx, dy);
+ window.requestAnimationFrame(sendScrollEvent);
+ } else {
+ // Locally, with silk and apz + e10s, retina 15" mbp usually get ~1.0 - 1.5
+ // w/o silk + e10s + apz, I get up to 7. Lower is better.
+ // Windows, I get ~3. Values are not valid w/o hardware vsync
+ var uniformities = utils.getFrameUniformityTestData();
+ for (var j = 0; j < uniformities.layerUniformities.length; j++) {
+ var layerResult = uniformities.layerUniformities[j];
+ var layerAddr = layerResult.layerAddress;
+ var uniformity = layerResult.frameUniformity;
+ var msg = "Layer: " + layerAddr.toString(16) + " Uniformity: " + uniformity;
+ SimpleTest.ok((uniformity >= 0) && (uniformity < 4.0), msg);
+ }
+ SimpleTest.finish();
+ }
+ }
+
+ function startTest() {
+ window.requestAnimationFrame(sendScrollEvent);
+ }
+
+ if (!isApzEnabled()) {
+ SimpleTest.ok(true, "APZ not enabled, skipping test");
+ SimpleTest.finish();
+ }
+ </script>
+</head>
+
+<body>
+ <div id="content">
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_touch_listeners_impacting_wheel.html b/gfx/layers/apz/test/mochitest/test_touch_listeners_impacting_wheel.html
new file mode 100644
index 0000000000..71147d5238
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_touch_listeners_impacting_wheel.html
@@ -0,0 +1,119 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1203140
+-->
+<head>
+ <title>Test for Bug 1203140</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1203140">Mozilla Bug 1203140</a>
+<p id="display"></p>
+<div id="content" style="overflow-y:scroll; height: 400px">
+ <p>The box below has a touch listener and a passive wheel listener. With touch events disabled, APZ shouldn't wait for any listeners.</p>
+ <div id="box" style="width: 200px; height: 200px; background-color: blue"></div>
+ <div style="height: 1000px; width: 10px">Div to make 'content' scrollable</div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+const kResponseTimeoutMs = 2 * 60 * 1000; // 2 minutes
+
+function takeSnapshots(e) {
+ // Grab some snapshots, and make sure some of them are different (i.e. check
+ // the page is scrolling in the compositor, concurrently with this wheel
+ // listener running).
+ // Note that we want this function to take less time than the content response
+ // timeout, otherwise the scrolling will start even if we haven't returned,
+ // and that would invalidate purpose of the test.
+ var start = Date.now();
+ var lastSnapshot = null;
+ var success = false;
+
+ // Get the position of the 'content' div relative to the screen
+ var rect = rectRelativeToScreen(document.getElementById("content"));
+
+ for (var i = 0; i < 10; i++) {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(16);
+ var snapshot = getSnapshot(rect);
+ // dump("Took snapshot " + snapshot + "\n"); // this might help with debugging
+
+ if (lastSnapshot && lastSnapshot != snapshot) {
+ ok(true, "Found some different pixels in snapshot " + i + " compared to previous");
+ success = true;
+ }
+ lastSnapshot = snapshot;
+ }
+ ok(success, "Found some snapshots that were different");
+ ok((Date.now() - start) < kResponseTimeoutMs, "Snapshotting ran quickly enough");
+
+ // Until now, no scroll events will have been dispatched to content. That's
+ // because scroll events are dispatched on the main thread, which we've been
+ // hogging with the code above. At this point we restore the normal refresh
+ // behaviour and let the main thread go back to C++ code, so the scroll events
+ // fire and we unwind from the main test continuation.
+ SpecialPowers.DOMWindowUtils.restoreNormalRefresh();
+}
+
+async function test() {
+ var box = document.getElementById("box");
+
+ // Ensure the div is layerized by scrolling it
+ await promiseMoveMouseAndScrollWheelOver(box, 10, 10);
+
+ box.addEventListener("touchstart", function(e) {
+ ok(false, "This should never be run");
+ });
+ box.addEventListener("wheel", takeSnapshots, { capture: false, passive: true });
+
+ // Let the event regions and layerization propagate to the APZ
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+
+ await promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "mousemove",
+ target: box,
+ offsetX: 10,
+ offsetY: 10,
+ });
+
+ // Take over control of the refresh driver and compositor
+ var utils = SpecialPowers.DOMWindowUtils;
+ utils.advanceTimeAndRefresh(0);
+
+ // Trigger an APZ scroll using a wheel event. If APZ is waiting for a
+ // content response, it will wait for takeSnapshots to finish running before
+ // it starts scrolling, which will cause the checks in takeSnapshots to fail.
+ await promiseNativeWheelAndWaitForScrollEvent(box, 10, 10, 0, -50);
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ // Disable touch events, so that APZ knows not to wait for touch listeners.
+ // Also explicitly set the content response timeout, so we know how long it
+ // is (see comment in takeSnapshots).
+ // Finally, enable smooth scrolling, so that the wheel-scroll we do as part
+ // of the test triggers an APZ animation rather than doing an instant scroll.
+ // Note that this pref doesn't work for the synthesized wheel events on OS X,
+ // those are hard-coded to be instant scrolls.
+ pushPrefs([["dom.w3c_touch_events.enabled", 0],
+ ["apz.content_response_timeout", kResponseTimeoutMs],
+ ["general.smoothscroll", true]])
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+}
+
+</script>
+</pre>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_wheel_scroll.html b/gfx/layers/apz/test/mochitest/test_wheel_scroll.html
new file mode 100644
index 0000000000..1b50c223ed
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_wheel_scroll.html
@@ -0,0 +1,109 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1013412
+https://bugzilla.mozilla.org/show_bug.cgi?id=1168182
+-->
+<head>
+ <title>Test for Bug 1013412 and 1168182</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ #content {
+ height: 800px;
+ overflow: scroll;
+ }
+
+ #scroller {
+ height: 2000px;
+ background: repeating-linear-gradient(#EEE, #EEE 100px, #DDD 100px, #DDD 200px);
+ }
+
+ #scrollbox {
+ margin-top: 200px;
+ width: 500px;
+ height: 500px;
+ border-radius: 250px;
+ box-shadow: inset 0 0 0 60px #555;
+ background: #777;
+ }
+
+ #circle {
+ position: relative;
+ left: 240px;
+ top: 20px;
+ border: 10px solid white;
+ border-radius: 10px;
+ width: 0px;
+ height: 0px;
+ transform-origin: 10px 230px;
+ will-change: transform;
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1013412">Mozilla Bug 1013412</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1168182">Mozilla Bug 1168182</a>
+<p id="display"></p>
+<div id="content">
+ <p>Scrolling the page should be async, but scrolling over the dark circle should not scroll the page and instead rotate the white ball.</p>
+ <div id="scroller">
+ <div id="scrollbox">
+ <div id="circle"></div>
+ </div>
+ </div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+var rotation = 0;
+var rotationAdjusted = false;
+
+var incrementForMode = function(mode) {
+ switch (mode) {
+ case WheelEvent.DOM_DELTA_PIXEL: return 1;
+ case WheelEvent.DOM_DELTA_LINE: return 15;
+ case WheelEvent.DOM_DELTA_PAGE: return 400;
+ }
+ return 0;
+};
+
+document.getElementById("scrollbox").addEventListener("wheel", function(e) {
+ rotation += e.deltaY * incrementForMode(e.deltaMode) * 0.2;
+ document.getElementById("circle").style.transform = "rotate(" + rotation + "deg)";
+ rotationAdjusted = true;
+ e.preventDefault();
+});
+
+async function test() {
+ var content = document.getElementById("content");
+ // enough iterations that we would scroll to the bottom of 'content'
+ for (let i = 0; i < 600 && content.scrollTop != content.scrollTopMax; i++) {
+ await promiseNativeWheelAndWaitForWheelEvent(content, 100, 150, 0, -5);
+ }
+ is(content.scrollTop > 0, true, "We should have scrolled down somewhat");
+ is(content.scrollTop, content.scrollTopMax, "We should have scrolled to the bottom of the scrollframe");
+ is(rotationAdjusted, false, "The rotation should not have been adjusted");
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// If we allow smooth scrolling the "smooth" scrolling may cause the page to
+// glide past the scrollbox (which is supposed to stop the scrolling) and so
+// we might end up at the bottom of the page.
+pushPrefs([["general.smoothScroll", false],
+ ["mousewheel.transaction.timeout", 100000],
+ ["dom.event.wheel-event-groups.enabled", true]])
+.then(waitUntilApzStable)
+.then(test)
+.then(SimpleTest.finish, SimpleTest.finishWithFailure);
+
+</script>
+</pre>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_wheel_transactions.html b/gfx/layers/apz/test/mochitest/test_wheel_transactions.html
new file mode 100644
index 0000000000..f015ea20be
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_wheel_transactions.html
@@ -0,0 +1,150 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1175585
+-->
+<head>
+ <title>Test for Bug 1175585</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ #outer-frame {
+ height: 500px;
+ overflow: scroll;
+ background: repeating-linear-gradient(#CCC, #CCC 100px, #BBB 100px, #BBB 200px);
+ }
+ #inner-frame {
+ margin-top: 25%;
+ height: 200%;
+ width: 75%;
+ overflow: scroll;
+ }
+ #inner-content {
+ height: 200%;
+ width: 200%;
+ background: repeating-linear-gradient(#EEE, #EEE 100px, #DDD 100px, #DDD 200px);
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1175585">APZ wheel transactions test</a>
+<p id="display"></p>
+<div id="outer-frame">
+ <div id="inner-frame">
+ <div id="inner-content"></div>
+ </div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+async function scrollWheelOver(element, deltaY) {
+ await promiseNativeWheelAndWaitForScrollEvent(element, 10, 10, 0, deltaY);
+}
+
+async function test() {
+ var outer = document.getElementById("outer-frame");
+ var inner = document.getElementById("inner-frame");
+
+ // Register a wheel event listener that records the target of
+ // the last wheel event, so that we can make assertions about it.
+ let lastWheelTarget;
+ let firstWheelTarget;
+ let wheelEventOccurred = false;
+ var wheelTargetRecorder = function(e) {
+ if (!wheelEventOccurred) {
+ firstWheelTarget = e.target;
+ wheelEventOccurred = true;
+ }
+ lastWheelTarget = e.target;
+ };
+ window.addEventListener("wheel", wheelTargetRecorder);
+
+ // Scroll |outer| to the bottom.
+ while (outer.scrollTop < outer.scrollTopMax) {
+ await scrollWheelOver(outer, -10);
+ }
+
+ is(lastWheelTarget, firstWheelTarget,
+ "target " + lastWheelTarget.id + " should be " + lastWheelTarget.id);
+ window.removeEventListener("wheel", wheelTargetRecorder);
+
+ // Immediately after, scroll it back up a bit.
+ await scrollWheelOver(outer, 10);
+
+ // Check that it was |outer| that scrolled back, and |inner| didn't
+ // scroll at all, as all the above scrolls should be in the same
+ // transaction.
+ ok(outer.scrollTop < outer.scrollTopMax, "'outer' should have scrolled back a bit");
+ is(inner.scrollTop, 0, "'inner' should not have scrolled");
+
+ // The next part of the test is related to the transaction timeout.
+ // Turn it down a bit so waiting for the timeout to elapse doesn't
+ // slow down the test harness too much.
+ var timeout = 5;
+ await SpecialPowers.pushPrefEnv({"set": [["mousewheel.transaction.timeout", timeout]]});
+ SimpleTest.requestFlakyTimeout("we are testing code that measures actual elapsed time between two events");
+
+ // Scroll up a bit more. It's still |outer| scrolling because
+ // |inner| is still scrolled all the way to the top.
+ await scrollWheelOver(outer, 10);
+
+ // Wait for the transaction timeout to elapse.
+ // timeout * 5 is used to make it less likely that the timeout is less than
+ // the system timestamp resolution
+ await SpecialPowers.promiseTimeout(timeout * 5);
+
+ // Now scroll down. The transaction having timed out, the event
+ // should pick up a new target, and that should be |inner|.
+ await scrollWheelOver(outer, -10);
+ ok(inner.scrollTop > 0, "'inner' should have been scrolled");
+
+ // Finally, test scroll handoff after a timeout.
+
+ // Continue scrolling |inner| down to the bottom.
+ var prevScrollTop = inner.scrollTop;
+ while (inner.scrollTop < inner.scrollTopMax) {
+ await scrollWheelOver(outer, -10);
+ // Avoid a failure getting us into an infinite loop.
+ ok(inner.scrollTop > prevScrollTop, "scrolling down should increase scrollTop");
+ prevScrollTop = inner.scrollTop;
+ }
+
+ // Wait for the transaction timeout to elapse.
+ // timeout * 5 is used to make it less likely that the timeout is less than
+ // the system timestamp resolution
+ await SpecialPowers.promiseTimeout(timeout * 5);
+
+ // Continued downward scrolling should scroll |outer| to the bottom.
+ prevScrollTop = outer.scrollTop;
+ while (outer.scrollTop < outer.scrollTopMax) {
+ await scrollWheelOver(outer, -10);
+ // Avoid a failure getting us into an infinite loop.
+ ok(outer.scrollTop > prevScrollTop, "scrolling down should increase scrollTop");
+ prevScrollTop = outer.scrollTop;
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// Disable smooth scrolling because it makes the test flaky (we don't have a good
+// way of detecting when the scrolling is finished).
+// Also, on macOS, force the native events to be wheel inputs rather than pan
+// inputs since this test is specifically testing things related to wheel
+// transactions.
+pushPrefs([["general.smoothScroll", false],
+ ["apz.test.mac.synth_wheel_input", true],
+ ["mousewheel.transaction.timeout", 1500],
+ ["dom.event.wheel-event-groups.enabled", true]])
+.then(waitUntilApzStable)
+.then(test)
+.then(SimpleTest.finish, SimpleTest.finishWithFailure);
+
+</script>
+</pre>
+
+</body>
+</html>