Add Chromium-only Blender WebEngine parity work
This commit is contained in:
391
blender-5.2.0/tests/python/ui_simulate/modules/easy_keys.py
Normal file
391
blender-5.2.0/tests/python/ui_simulate/modules/easy_keys.py
Normal file
@@ -0,0 +1,391 @@
|
||||
# SPDX-FileCopyrightText: 2019-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import datetime
|
||||
import string
|
||||
import bpy
|
||||
event_types = tuple(
|
||||
e.identifier.lower()
|
||||
for e in bpy.types.Event.bl_rna.properties["type"].enum_items_static
|
||||
)
|
||||
del bpy
|
||||
|
||||
# We don't normally care about which one.
|
||||
event_types_alias = {
|
||||
"ctrl": "left_ctrl",
|
||||
"shift": "left_shift",
|
||||
"alt": "left_alt",
|
||||
|
||||
# Collides with Python keywords.
|
||||
"delete": "del",
|
||||
}
|
||||
|
||||
|
||||
# Note, we could add support for other keys using control characters,
|
||||
# for example: `\xF12` could be used for the F12 key.
|
||||
#
|
||||
# Besides this, we could encode symbols into a regular string using our own syntax
|
||||
# which can mix regular text and key symbols.
|
||||
#
|
||||
# At the moment this doesn't seem necessary, no need to add it.
|
||||
event_types_text = (
|
||||
('ZERO', "0", False),
|
||||
('ONE', "1", False),
|
||||
('TWO', "2", False),
|
||||
('THREE', "3", False),
|
||||
('FOUR', "4", False),
|
||||
('FIVE', "5", False),
|
||||
('SIX', "6", False),
|
||||
('SEVEN', "7", False),
|
||||
('EIGHT', "8", False),
|
||||
('NINE', "9", False),
|
||||
|
||||
('ONE', "!", True),
|
||||
('TWO', "@", True),
|
||||
('THREE', "#", True),
|
||||
('FOUR', "$", True),
|
||||
('FIVE', "%", True),
|
||||
('SIX', "^", True),
|
||||
('SEVEN', "&", True),
|
||||
('EIGHT', "*", True),
|
||||
('NINE', "(", True),
|
||||
('ZERO', ")", True),
|
||||
|
||||
('MINUS', "-", False),
|
||||
('MINUS', "_", True),
|
||||
|
||||
('EQUAL', "=", False),
|
||||
('EQUAL', "+", True),
|
||||
|
||||
('ACCENT_GRAVE', "`", False),
|
||||
('ACCENT_GRAVE', "~", True),
|
||||
|
||||
('LEFT_BRACKET', "[", False),
|
||||
('LEFT_BRACKET', "{", True),
|
||||
|
||||
('RIGHT_BRACKET', "]", False),
|
||||
('RIGHT_BRACKET', "}", True),
|
||||
|
||||
('SEMI_COLON', ";", False),
|
||||
('SEMI_COLON', ":", True),
|
||||
|
||||
('PERIOD', ".", False),
|
||||
('PERIOD', ">", True),
|
||||
|
||||
('COMMA', ",", False),
|
||||
('COMMA', "<", True),
|
||||
|
||||
('QUOTE', "'", False),
|
||||
('QUOTE', '"', True),
|
||||
|
||||
('SLASH', "/", False),
|
||||
('SLASH', "?", True),
|
||||
|
||||
('BACK_SLASH', "\\", False),
|
||||
('BACK_SLASH', "|", True),
|
||||
|
||||
|
||||
*((ch_upper, ch, False) for (ch_upper, ch) in zip(string.ascii_uppercase, string.ascii_lowercase)),
|
||||
*((ch, ch, True) for ch in string.ascii_uppercase),
|
||||
|
||||
('SPACE', " ", False),
|
||||
('RET', "\n", False),
|
||||
('TAB', "\t", False),
|
||||
)
|
||||
|
||||
event_types_text_from_char = {ch: (ty, is_shift) for (ty, ch, is_shift) in event_types_text}
|
||||
event_types_text_from_event = {(ty, is_shift): ch for (ty, ch, is_shift) in event_types_text}
|
||||
|
||||
|
||||
class _EventBuilder:
|
||||
__slots__ = (
|
||||
"_shared_event_gen",
|
||||
"_event_type",
|
||||
"_parent",
|
||||
)
|
||||
|
||||
def __init__(self, event_gen, ty):
|
||||
self._shared_event_gen = event_gen
|
||||
self._event_type = ty
|
||||
self._parent = None
|
||||
|
||||
def __call__(self, count=1):
|
||||
assert count >= 0
|
||||
for _ in range(count):
|
||||
self.tap()
|
||||
return self._shared_event_gen
|
||||
|
||||
def _key_press_release(self, do_press=False, do_release=False, unicode_override=None):
|
||||
assert (do_press or do_release)
|
||||
keys_held = self._shared_event_gen._event_types_held
|
||||
build_keys = []
|
||||
e = self
|
||||
while e is not None:
|
||||
build_keys.append(e._event_type.upper())
|
||||
e = e._parent
|
||||
build_keys.reverse()
|
||||
|
||||
events = [None, None]
|
||||
for i, value in enumerate(('PRESS', 'RELEASE')):
|
||||
if value == 'RELEASE':
|
||||
build_keys.reverse()
|
||||
for event_type in build_keys:
|
||||
if value == 'PRESS':
|
||||
keys_held.add(event_type)
|
||||
else:
|
||||
keys_held.remove(event_type)
|
||||
|
||||
if (not do_press) and value == 'PRESS':
|
||||
continue
|
||||
if (not do_release) and value == 'RELEASE':
|
||||
continue
|
||||
|
||||
shift = 'LEFT_SHIFT' in keys_held or 'RIGHT_SHIFT' in keys_held
|
||||
ctrl = 'LEFT_CTRL' in keys_held or 'RIGHT_CTRL' in keys_held
|
||||
shift = 'LEFT_SHIFT' in keys_held or 'RIGHT_SHIFT' in keys_held
|
||||
alt = 'LEFT_ALT' in keys_held or 'RIGHT_ALT' in keys_held
|
||||
oskey = 'OSKEY' in keys_held
|
||||
hyper = 'HYPER' in keys_held
|
||||
|
||||
unicode = None
|
||||
if value == 'PRESS':
|
||||
if ctrl is False and alt is False and oskey is False:
|
||||
if unicode_override is not None:
|
||||
unicode = unicode_override
|
||||
else:
|
||||
unicode = event_types_text_from_event.get((event_type, shift))
|
||||
if unicode is None and shift:
|
||||
# Some keys don't care about shift
|
||||
unicode = event_types_text_from_event.get((event_type, False))
|
||||
|
||||
event = self._shared_event_gen.window.event_simulate(
|
||||
type=event_type,
|
||||
value=value,
|
||||
unicode=unicode,
|
||||
shift=shift,
|
||||
ctrl=ctrl,
|
||||
alt=alt,
|
||||
oskey=oskey,
|
||||
hyper=hyper,
|
||||
x=self._shared_event_gen._mouse_co[0],
|
||||
y=self._shared_event_gen._mouse_co[1],
|
||||
)
|
||||
events[i] = event
|
||||
return tuple(events)
|
||||
|
||||
def tap(self):
|
||||
return self._key_press_release(do_press=True, do_release=True)
|
||||
|
||||
def press(self):
|
||||
return self._key_press_release(do_press=True)[0]
|
||||
|
||||
def release(self):
|
||||
return self._key_press_release(do_release=True)[1]
|
||||
|
||||
def cursor_motion(self, coords):
|
||||
coords = list(coords)
|
||||
self._shared_event_gen.cursor_position_set(*coords[0], move=True)
|
||||
yield
|
||||
|
||||
event = self.press()
|
||||
shift = event.shift
|
||||
ctrl = event.ctrl
|
||||
shift = event.shift
|
||||
alt = event.alt
|
||||
oskey = event.oskey
|
||||
hyper = event.hyper
|
||||
yield
|
||||
|
||||
for x, y in coords:
|
||||
self._shared_event_gen.window.event_simulate(
|
||||
type='MOUSEMOVE',
|
||||
value='NOTHING',
|
||||
unicode=None,
|
||||
shift=shift,
|
||||
ctrl=ctrl,
|
||||
alt=alt,
|
||||
oskey=oskey,
|
||||
hyper=hyper,
|
||||
x=x,
|
||||
y=y
|
||||
)
|
||||
yield
|
||||
self._shared_event_gen.cursor_position_set(x, y, move=False)
|
||||
self.release()
|
||||
yield
|
||||
|
||||
def __getattr__(self, attr):
|
||||
attr = event_types_alias.get(attr, attr)
|
||||
if attr in event_types:
|
||||
e = _EventBuilder(self._shared_event_gen, attr)
|
||||
e._parent = self
|
||||
return e
|
||||
raise Exception(f"{attr!r} not found in {event_types!r}")
|
||||
|
||||
|
||||
class EventGenerate:
|
||||
__slots__ = (
|
||||
"window",
|
||||
|
||||
"_mouse_co",
|
||||
"_event_types_held",
|
||||
)
|
||||
|
||||
def __init__(self, window):
|
||||
self.window = window
|
||||
self._mouse_co = [0, 0]
|
||||
self._event_types_held = set()
|
||||
|
||||
self.cursor_position_set(window.width // 2, window.height // 2)
|
||||
|
||||
def cursor_position_set(self, x, y, move=False):
|
||||
self._mouse_co[:] = x, y
|
||||
if move:
|
||||
self.window.event_simulate(
|
||||
type='MOUSEMOVE',
|
||||
value='NOTHING',
|
||||
x=x,
|
||||
y=y,
|
||||
)
|
||||
|
||||
def text(self, text):
|
||||
""" Type in entire phrases. """
|
||||
for ch in text:
|
||||
ty, shift = event_types_text_from_char[ch]
|
||||
ty = ty.lower()
|
||||
if shift:
|
||||
eb = getattr(_EventBuilder(self, 'left_shift'), ty)
|
||||
else:
|
||||
eb = _EventBuilder(self, ty)
|
||||
eb.tap()
|
||||
return self
|
||||
|
||||
def text_unicode(self, text):
|
||||
# Since the only purpose of this key-press is to enter text
|
||||
# the key can be almost anything, use a key which isn't likely to be assigned to any other action.
|
||||
#
|
||||
# If it were possible `EVT_UNKNOWNKEY` would be most correct
|
||||
# as dead keys map to this and still enter text.
|
||||
ty_dummy = 'F24'
|
||||
for ch in text:
|
||||
eb = _EventBuilder(self, ty_dummy)
|
||||
eb._key_press_release(do_press=True, do_release=True, unicode_override=ch)
|
||||
return self
|
||||
|
||||
def __getattr__(self, attr):
|
||||
attr = event_types_alias.get(attr, attr)
|
||||
if attr in event_types:
|
||||
return _EventBuilder(self, attr)
|
||||
raise Exception(f"{attr!r} not found in {event_types!r}")
|
||||
|
||||
def __del__(self):
|
||||
if self._event_types_held:
|
||||
print("'__del__' with keys held:", repr(self._event_types_held))
|
||||
|
||||
|
||||
def run(
|
||||
event_iter, *,
|
||||
on_error=None,
|
||||
on_exit=None,
|
||||
on_step_command_pre=None,
|
||||
on_step_command_post=None,
|
||||
):
|
||||
import bpy
|
||||
|
||||
TICKS = 4 # 3 works, 4 to be on the safe side.
|
||||
|
||||
# If we try to handle events this many times
|
||||
TICKS_HANDLING_BREAK_MAX = 256
|
||||
|
||||
def event_step():
|
||||
|
||||
# Handle `is_event_handling_break` here so we don't incorrectly detect
|
||||
# consecutive `is_event_handling_break` based on other functions exiting early.
|
||||
is_event_handling_break = bpy.context.window_manager.is_event_handling_break
|
||||
if is_event_handling_break:
|
||||
if event_step._ticks_handling_break_consecutive > TICKS_HANDLING_BREAK_MAX:
|
||||
raise RuntimeError(
|
||||
"window_manager.is_event_handling_break set {:d} times, may be an event handling bug!".format(
|
||||
TICKS_HANDLING_BREAK_MAX,
|
||||
)
|
||||
)
|
||||
event_step._ticks_handling_break_consecutive += 1
|
||||
else:
|
||||
event_step._ticks_handling_break_consecutive = 0
|
||||
|
||||
# Run once 'TICKS' is reached.
|
||||
if event_step._ticks < TICKS:
|
||||
event_step._ticks += 1
|
||||
return 0.0
|
||||
event_step._ticks = 0
|
||||
|
||||
# Wait for any pending event queue break to be cleared before advancing,
|
||||
# otherwise events injected by the next step may be deferred unexpectedly.
|
||||
if is_event_handling_break:
|
||||
return 0.0
|
||||
|
||||
if on_step_command_pre:
|
||||
if event_step.run_events.gi_frame is not None:
|
||||
import shlex
|
||||
import subprocess
|
||||
subprocess.call(
|
||||
shlex.split(
|
||||
on_step_command_pre.replace(
|
||||
"{file}", event_step.run_events.gi_frame.f_code.co_filename,
|
||||
).replace(
|
||||
"{line}", str(event_step.run_events.gi_frame.f_lineno),
|
||||
)
|
||||
)
|
||||
)
|
||||
try:
|
||||
val = next(event_step.run_events, Ellipsis)
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
if on_error is not None:
|
||||
on_error()
|
||||
if on_exit is not None:
|
||||
on_exit()
|
||||
return None
|
||||
|
||||
if on_step_command_post:
|
||||
if event_step.run_events.gi_frame is not None:
|
||||
import shlex
|
||||
import subprocess
|
||||
subprocess.call(
|
||||
shlex.split(
|
||||
on_step_command_post.replace(
|
||||
"{file}", event_step.run_events.gi_frame.f_code.co_filename,
|
||||
).replace(
|
||||
"{line}", str(event_step.run_events.gi_frame.f_lineno),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(val, EventGenerate) or val is None:
|
||||
return 0.0
|
||||
elif isinstance(val, datetime.timedelta):
|
||||
return val.total_seconds()
|
||||
elif val is Ellipsis:
|
||||
if on_exit is not None:
|
||||
on_exit()
|
||||
return None
|
||||
else:
|
||||
raise Exception(f"{val!r} of type {type(val)!r} not supported")
|
||||
|
||||
event_step.run_events = iter(event_iter)
|
||||
event_step._ticks = 0
|
||||
event_step._ticks_handling_break_consecutive = 0
|
||||
|
||||
# Persistent so this keeps working when tests load a blend file.
|
||||
bpy.app.timers.register(event_step, first_interval=0.0, persistent=True)
|
||||
|
||||
|
||||
def setup_default_preferences(preferences):
|
||||
""" Set preferences useful for automation.
|
||||
"""
|
||||
preferences.view.show_splash = False
|
||||
preferences.view.smooth_view = 0
|
||||
preferences.view.use_save_prompt = False
|
||||
preferences.filepaths.use_auto_save_temporary_files = False
|
||||
196
blender-5.2.0/tests/python/ui_simulate/modules/ui_test_utils.py
Normal file
196
blender-5.2.0/tests/python/ui_simulate/modules/ui_test_utils.py
Normal file
@@ -0,0 +1,196 @@
|
||||
# SPDX-FileCopyrightText: 2019-2026 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
__all__ = (
|
||||
"call_menu",
|
||||
"call_operator",
|
||||
"cursor_motion_data_x",
|
||||
"cursor_motion_data_y",
|
||||
"cursor_motion_data_xy",
|
||||
"cursor_motion_data_circle",
|
||||
"get_area_center",
|
||||
"get_area_center_from_spacetype",
|
||||
"get_window_area_by_type",
|
||||
"get_window_size_in_pixels",
|
||||
"idle_until",
|
||||
"keep_open",
|
||||
"test_window",
|
||||
)
|
||||
|
||||
|
||||
def test_window(window_index=0):
|
||||
"""
|
||||
Get window with associated event simulator and tests.
|
||||
"""
|
||||
import unittest
|
||||
from .easy_keys import EventGenerate
|
||||
import bpy
|
||||
|
||||
window = bpy.data.window_managers[0].windows[window_index]
|
||||
|
||||
return (
|
||||
EventGenerate(window),
|
||||
unittest.TestCase(),
|
||||
window,
|
||||
)
|
||||
|
||||
|
||||
def call_operator(e, text: str):
|
||||
"""
|
||||
Call operator by name.
|
||||
"""
|
||||
yield e.f3()
|
||||
yield e.text(text)
|
||||
yield e.ret()
|
||||
|
||||
|
||||
def call_menu(e, text: str):
|
||||
"""
|
||||
Call operator through menu.
|
||||
"""
|
||||
yield e.f3()
|
||||
yield e.text_unicode(text.replace(" -> ", " \u25b8 "))
|
||||
yield e.ret()
|
||||
|
||||
|
||||
def get_window_area_by_type(window, space_type):
|
||||
"""
|
||||
Get first area of the specified space type in a window.
|
||||
"""
|
||||
for area in window.screen.areas:
|
||||
if area.type == space_type:
|
||||
return area
|
||||
|
||||
|
||||
def get_area_center(area):
|
||||
"""
|
||||
Get coordinate in the center of an area, e.g. for placing the cursor.
|
||||
"""
|
||||
return (
|
||||
area.x + area.width // 2,
|
||||
area.y + area.height // 2,
|
||||
)
|
||||
|
||||
|
||||
def get_area_center_from_spacetype(window, space_type):
|
||||
"""
|
||||
Get coordinate in the center of the first area with the given spacetype.
|
||||
"""
|
||||
area = get_window_area_by_type(window, space_type)
|
||||
if area is None:
|
||||
raise Exception("Space Type {!r} not found".format(space_type))
|
||||
return get_area_center(area)
|
||||
|
||||
|
||||
def get_window_size_in_pixels(window):
|
||||
"""
|
||||
Get window size that can be used for positioning a cursor.
|
||||
"""
|
||||
import sys
|
||||
size = window.width, window.height
|
||||
# macOS window size is a multiple of the pixel_size.
|
||||
if sys.platform == "darwin":
|
||||
from bpy import context
|
||||
# The value is always rounded to an int, so converting to an int is safe here.
|
||||
pixel_size = int(context.preferences.system.pixel_size)
|
||||
size = size[0] * pixel_size, size[1] * pixel_size
|
||||
return size
|
||||
|
||||
|
||||
def idle_until(until, idle=1 / 60, timeout=1.0):
|
||||
"""
|
||||
Idle while the internal event loop runs until a specified condition is true.
|
||||
|
||||
This should be used sparingly as it may represent some other failure
|
||||
condition inside Blender. Currently, it is used to:
|
||||
- Test completion and cancellation of operators that use jobs.
|
||||
- Multi window undo tests that need separate view layer (see #148903).
|
||||
|
||||
Note: In practice, the timeout value of 1.0 seconds should be more than enough
|
||||
for all cases. In testing with a fixed, constant delay, the tests succeeded
|
||||
with a timeout of 1/6th of a second.
|
||||
:param until: lambda to check the condition of after each sleep
|
||||
:param idle: how long to idle between checks of the `until` lambda.
|
||||
Defaults to 60Hz due to common refresh rates.
|
||||
:param timeout: the max time in seconds that this busy wait will execute.
|
||||
:return:
|
||||
"""
|
||||
import datetime
|
||||
import time
|
||||
start_time = time.time()
|
||||
current_time = time.time()
|
||||
while current_time - start_time < timeout and not until():
|
||||
yield datetime.timedelta(seconds=idle)
|
||||
current_time = time.time()
|
||||
|
||||
|
||||
def keep_open():
|
||||
"""
|
||||
Only for development, handy so we can quickly keep the window open while testing.
|
||||
"""
|
||||
import bpy
|
||||
bpy.app.use_event_simulate = False
|
||||
|
||||
|
||||
def cursor_motion_data_x(window, margin=0.2):
|
||||
"""
|
||||
Generate a range of (x,y) positions in screen space, centered vertically in the window
|
||||
from left to right.
|
||||
|
||||
:param margin: Percentage of left and right window space to leave unused
|
||||
"""
|
||||
size = get_window_size_in_pixels(window)
|
||||
return [
|
||||
(x, size[1] // 2) for x in
|
||||
range(int(size[0] * margin), int(size[0] * (1.0 - margin)), 80)
|
||||
]
|
||||
|
||||
|
||||
def cursor_motion_data_y(window, margin=0.2):
|
||||
"""
|
||||
Generate a range of (x,y) positions in screen space, centered horizontally in the window
|
||||
from bottom to top.
|
||||
|
||||
:param margin: Percentage of top and bottom window space to leave unused
|
||||
"""
|
||||
size = get_window_size_in_pixels(window)
|
||||
return [
|
||||
(size[0] // 2, y) for y in
|
||||
range(int(size[1] * margin), int(size[1] * (1.0 - margin)), 80)
|
||||
]
|
||||
|
||||
|
||||
def cursor_motion_data_xy(window, margin=0.2):
|
||||
"""
|
||||
Generate a range of (x,y) positions in screen space from bottom left to top right
|
||||
|
||||
:param margin: Percentage of window space to leave unused
|
||||
"""
|
||||
size = get_window_size_in_pixels(window)
|
||||
return [
|
||||
(p, p) for p in
|
||||
range(int(size[0] * margin), int(size[0] * (1.0 - margin)), 80)
|
||||
]
|
||||
|
||||
|
||||
def cursor_motion_data_circle(center, radius):
|
||||
"""
|
||||
Generate a range of (x,y) positions in screen space as a circle.
|
||||
:param center: The center of the circle
|
||||
:param radius: The radius of the circle
|
||||
"""
|
||||
import sys
|
||||
from math import sin, cos, pi
|
||||
if sys.platform == "darwin":
|
||||
from bpy import context
|
||||
# The value is always rounded to an int, so converting to an int is safe here.
|
||||
radius = radius * int(context.preferences.system.pixel_size)
|
||||
|
||||
steps = 20
|
||||
angles = [(i / steps) * 2.0 * pi for i in range(steps)]
|
||||
angles.append(0.0)
|
||||
|
||||
return [
|
||||
(int(center[0] + -radius * sin(phi)), int(center[1] + radius * cos(phi))) for phi in angles
|
||||
]
|
||||
Reference in New Issue
Block a user