diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..08b58f454 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "pywinauto/windows/injected"] + path = pywinauto/windows/injected + url = https://github.com/pywinauto/injected.git + branch = master diff --git a/appveyor.yml b/appveyor.yml index b74b28562..3e91c16c9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,7 +6,7 @@ # # fetch repository as a zip archive -shallow_clone: true # default is "false" +shallow_clone: false # default is "false" environment: @@ -67,6 +67,10 @@ install: # as well as pywin32, pillow and coverage - "powershell ./ci/install.ps1" - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" + - cd %APPVEYOR_BUILD_FOLDER% + - git submodule init + - git submodule update + # Install the build dependencies of the project. If some dependencies contain # compiled extensions and are not provided as pre-built wheel packages, @@ -76,7 +80,6 @@ install: # Enable desktop (for correct screenshots). #- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-desktop.ps1')) - build: false # Not a C# project, build stuff at the test step instead. test_script: diff --git a/pywinauto/base_application.py b/pywinauto/base_application.py index af738ea1e..336f50ce0 100644 --- a/pywinauto/base_application.py +++ b/pywinauto/base_application.py @@ -223,6 +223,8 @@ def __find_base(self, criteria_, timeout, retry_interval): if 'backend' not in ctrl_criteria: ctrl_criteria['backend'] = self.backend.name + if self.app is not None: + ctrl_criteria['pid'] = self.app.process ctrl = self.backend.generic_wrapper_class(findwindows.find_element(**ctrl_criteria)) previous_parent = ctrl.element_info ctrls.append(ctrl) @@ -479,7 +481,6 @@ def by(self, **criteria): new_item = WindowSpecification(self.criteria[0], allow_magic_lookup=self.allow_magic_lookup) new_item.criteria.extend(self.criteria[1:]) new_item.criteria.append(criteria) - return new_item def __getitem__(self, key): @@ -574,7 +575,16 @@ def __getattribute__(self, attr_name): # FIXME - I don't get this part at all, why is it win32-specific and why not keep the same logic as above? # if we have been asked for an attribute of the dialog # then resolve the window and return the attribute - desktop_wrapper = self.backend.generic_wrapper_class(self.backend.element_info_class()) + if self.backend.name in ('wpf'): + if self.app is None and len(self.criteria) > 0 and 'pid' in self.criteria[0]: + pid = self.criteria[0]['pid'] + else: + pid = self.app.process + desktop_wrapper = self.backend.generic_wrapper_class( + self.backend.element_info_class(pid=pid) + ) + else: + desktop_wrapper = self.backend.generic_wrapper_class(self.backend.element_info_class()) need_to_resolve = (len(self.criteria) == 1 and hasattr(desktop_wrapper, attr_name)) if hasattr(self.backend, 'dialog_class'): need_to_resolve = need_to_resolve and hasattr(self.backend.dialog_class, attr_name) diff --git a/pywinauto/base_wrapper.py b/pywinauto/base_wrapper.py index 8bb20e8f1..478b34506 100644 --- a/pywinauto/base_wrapper.py +++ b/pywinauto/base_wrapper.py @@ -170,6 +170,8 @@ def by(self, **criteria): criteria['backend'] = self.backend.name criteria['parent'] = self.element_info + if self.backend.name in ('wpf'): + criteria['pid'] = self.element_info.pid child_specification = WindowSpecification(criteria) return child_specification diff --git a/pywinauto/controls/__init__.py b/pywinauto/controls/__init__.py index 9bafa46a0..d0f92ce56 100644 --- a/pywinauto/controls/__init__.py +++ b/pywinauto/controls/__init__.py @@ -48,5 +48,8 @@ from . import common_controls from . import win32_controls + from . import wpfwrapper + from . import wpf_controls + from ..base_wrapper import InvalidElement diff --git a/pywinauto/controls/uia_controls.py b/pywinauto/controls/uia_controls.py index 9b511ec2e..16a834754 100644 --- a/pywinauto/controls/uia_controls.py +++ b/pywinauto/controls/uia_controls.py @@ -1126,7 +1126,7 @@ def item_by_path(self, path, exact=False): menu_items = [p.strip() for p in path.split("->")] items_cnt = len(menu_items) if items_cnt == 0: - raise IndexError() + raise IndexError("Menu path has incorrect format, no item identifiers found") for item in menu_items: if not item: raise IndexError("Empty item name between '->' separators") @@ -1141,8 +1141,9 @@ def next_level_menu(parent_menu, item_name, is_last): # a new Menu control is created and placed on the dialog. It can be # a direct child or a descendant. # Sometimes we need to re-discover Menu again + i = 0 try: - menu = next_level_menu(self, menu_items[0], items_cnt == 1) + menu = next_level_menu(self, menu_items[i], items_cnt == 1) if items_cnt == 1: return menu @@ -1156,8 +1157,8 @@ def next_level_menu(parent_menu, item_name, is_last): for i in range(1, items_cnt): menu = next_level_menu(menu, menu_items[i], items_cnt == i + 1) - except(AttributeError): - raise IndexError() + except (AttributeError, IndexError): + raise IndexError('Incorrect menu path, item "{}" (index {}) not found'.format(menu_items[i], i)) return menu diff --git a/pywinauto/controls/wpf_controls.py b/pywinauto/controls/wpf_controls.py new file mode 100644 index 000000000..ec1a39cac --- /dev/null +++ b/pywinauto/controls/wpf_controls.py @@ -0,0 +1,1377 @@ +# -*- coding: utf-8 -*- +# GUI Application automation and testing library +# Copyright (C) 2006-2017 Mark Mc Mahon and Contributors +# https://github.com/pywinauto/pywinauto/graphs/contributors +# http://pywinauto.readthedocs.io/en/latest/credits.html +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of pywinauto nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Wrap various WPF windows controls. To be used with 'wpf' backend.""" +import locale +import time +import six + +from . import wpfwrapper +from . import common_controls +from .. import findbestmatch +from .. import timings + + +# ==================================================================== +class WindowWrapper(wpfwrapper.WPFWrapper): + + """Wrap a WPF Window control.""" + + _control_types = ['Window'] + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control.""" + super(WindowWrapper, self).__init__(elem) + + # ----------------------------------------------------------- + def move_window(self, x=None, y=None, width=None, height=None): + """Move the window to the new coordinates. + + * **x** Specifies the new left position of the window. + Defaults to the current left position of the window. + * **y** Specifies the new top position of the window. + Defaults to the current top position of the window. + * **width** Specifies the new width of the window. + Defaults to the current width of the window. + * **height** Specifies the new height of the window. + Defaults to the current height of the window. + """ + cur_rect = self.rectangle() + + # if no X is specified - so use current coordinate + if x is None: + x = cur_rect.left + else: + try: + y = x.top + width = x.width() + height = x.height() + x = x.left + except AttributeError: + pass + + # if no Y is specified - so use current coordinate + if y is None: + y = cur_rect.top + + # if no width is specified - so use current width + if width is None: + width = cur_rect.width() + + # if no height is specified - so use current height + if height is None: + height = cur_rect.height() + + # ask for the window to be moved + self.set_native_property('Left', x) + self.set_native_property('Top', y) + self.set_native_property('Width', width) + self.set_native_property('Height', height) + + time.sleep(timings.Timings.after_movewindow_wait) + + # ----------------------------------------------------------- + def is_dialog(self): + """Window is always a dialog so return True.""" + return True + + +class ButtonWrapper(wpfwrapper.WPFWrapper): + + """Wrap a WPF Button, CheckBox or RadioButton control.""" + + _control_types = ['Button', + 'CheckBox', + 'RadioButton', + ] + + UNCHECKED = 0 + CHECKED = 1 + INDETERMINATE = 2 + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control.""" + super(ButtonWrapper, self).__init__(elem) + + # ----------------------------------------------------------- + def toggle(self): + """Switch state of checkable controls (yclically). + + Supported states are CHECKED/UNCHECKED or CHECKED/UNCHECKED/INDETERMINATE (if a control is three-state). + Usually applied for the check box control. + """ + current_state = self.get_native_property('IsChecked') + if self.get_native_property('IsThreeState'): + states = (True, False, None) + else: + states = (True, False) + + if current_state is None and not self.get_native_property('IsThreeState'): + next_state = False + else: + next_state = states[(states.index(current_state)+1) % len(states)] + + self.set_native_property('IsChecked', next_state) + + name = self.element_info.name + control_type = self.element_info.control_type + + if name and control_type: + self.actions.log('Toggled ' + control_type.lower() + ' "' + name + '"') + # Return itself so that action can be chained + return self + + # ----------------------------------------------------------- + def get_toggle_state(self): + """Get a toggle state of a check box control. + + The toggle state is represented by an integer + 0 - unchecked + 1 - checked + 2 - indeterminate + """ + val = self.get_native_property('IsChecked') + if val is None: + return self.INDETERMINATE + return self.CHECKED if val else self.UNCHECKED + + + # ----------------------------------------------------------- + def is_dialog(self): + """Buttons are never dialogs so return False.""" + return False + + # ----------------------------------------------------------- + def click(self): + """Click the Button control by raising the ButtonBase.Click event.""" + self.raise_event('Click') + # Return itself so that action can be chained + return self + + def select(self): + """Select the item. + + Usually applied for controls like: a radio button, a tree view item + or a list item. + """ + self.set_native_property('IsChecked', True) + + name = self.element_info.name + control_type = self.element_info.control_type + if name and control_type: + self.actions.log("Selected " + control_type.lower() + ' "' + name + '"') + + # Return itself so that action can be chained + return self + + # ----------------------------------------------------------- + def is_selected(self): + """Indicate that the item is selected or not. + + Usually applied for controls like: a radio button, a tree view item, + a list item. + """ + return self.get_native_property('IsChecked') + + +class ComboBoxWrapper(wpfwrapper.WPFWrapper): + + """Wrap a WPF ComboBox control.""" + + _control_types = ['ComboBox'] + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control.""" + super(ComboBoxWrapper, self).__init__(elem) + + # ----------------------------------------------------------- + def expand(self): + """Display items of the combobox.""" + self.set_native_property('IsDropDownOpen', True) + return self + + # ----------------------------------------------------------- + def collapse(self): + """Hide items of the combobox.""" + self.set_native_property('IsDropDownOpen', False) + return self + + # ----------------------------------------------------------- + def is_editable(self): + """Return the edit possibility of the element.""" + return self.get_native_property('IsEditable') + + # ----------------------------------------------------------- + def is_expanded(self): + """Test if the control is expanded.""" + return self.get_native_property('IsDropDownOpen') + + # ----------------------------------------------------------- + def is_collapsed(self): + """Test if the control is collapsed.""" + return not self.get_native_property('IsDropDownOpen') + + # ----------------------------------------------------------- + def texts(self): + """Return the text of the items in the combobox.""" + return [child.element_info.rich_text for child in self.iter_children()] + + # ----------------------------------------------------------- + def select(self, item): + """Select the combobox item. + + The item can be either a 0 based index of the item to select + or it can be the string that you want to select + """ + if isinstance(item, six.integer_types): + self.set_native_property('SelectedIndex', item) + else: + index = None + for child in self.iter_children(): + if child.element_info.rich_text == item: + index = 1 + if index is None: + raise IndexError('no such item: {}'.format(item)) + self.set_native_property('SelectedIndex', index) + return self + + # ----------------------------------------------------------- + # TODO: add selected_texts for a combobox with a multi-select support + def selected_text(self): + """Return the selected text or None. + + Notice, that in case of multi-select it will be only the text from + a first selected item + """ + selected_index = self.get_native_property('SelectedIndex') + if selected_index == -1: + return '' + return self.children()[selected_index].element_info.rich_text + + # ----------------------------------------------------------- + # TODO: add selected_indices for a combobox with multi-select support + def selected_index(self): + """Return the selected index.""" + return self.get_native_property('SelectedIndex') + + # ----------------------------------------------------------- + def item_count(self): + """Return the number of items in the ComboBox. + + The interface is kept mostly for a backward compatibility with + the native ComboBox interface + """ + return len(self.children()) + + +class EditWrapper(wpfwrapper.WPFWrapper): + + """Wrap an Edit control.""" + + _control_types = ['Edit'] + has_title = False + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control.""" + super(EditWrapper, self).__init__(elem) + + # ----------------------------------------------------------- + @property + def writable_props(self): + """Extend default properties list.""" + props = super(EditWrapper, self).writable_props + props.extend(['selection_indices']) + return props + + # ----------------------------------------------------------- + def line_count(self): + """Return how many lines there are in the Edit.""" + return self.window_text().count("\n") + 1 + + # ----------------------------------------------------------- + def line_length(self, line_index): + """Return how many characters there are in the line.""" + # need to first get a character index of that line + lines = self.window_text().splitlines() + if line_index < len(lines): + return len(lines[line_index]) + if line_index == self.line_count() - 1: + return 0 + raise IndexError("There are only {0} lines but given index is {1}".format(self.line_count(), line_index)) + + # ----------------------------------------------------------- + def get_line(self, line_index): + """Return the line specified.""" + lines = self.window_text().splitlines() + if line_index < len(lines): + return lines[line_index] + if line_index == self.line_count() - 1: + return "" + else: + raise IndexError("There are only {0} lines but given index is {1}".format(self.line_count(), line_index)) + + # ----------------------------------------------------------- + def get_value(self): + """Return the current value of the element.""" + return self.get_native_property('Text') or '' + + # ----------------------------------------------------------- + def is_editable(self): + """Return the edit possibility of the element.""" + return not self.get_native_property('IsReadOnly') + + # ----------------------------------------------------------- + def texts(self): + """Get the text of the edit control.""" + texts = [ self.get_line(i) for i in range(self.line_count()) ] + + return texts + + # ----------------------------------------------------------- + def text_block(self): + """Get the text of the edit control.""" + return self.window_text() + + # ----------------------------------------------------------- + def selection_indices(self): + """The start and end indices of the current selection.""" + start = self.get_native_property('SelectionStart') + end = start + self.get_native_property('SelectionLength') + + return start, end + + # ----------------------------------------------------------- + def set_window_text(self, text, append=False): + """Override set_window_text for edit controls because it should not be used for Edit controls. + + Edit Controls should either use set_edit_text() or type_keys() to modify + the contents of the edit control. + """ + self.verify_actionable() + + if append: + text = self.window_text() + text + + self.set_native_property('Text', text) + + # ----------------------------------------------------------- + def set_edit_text(self, text, pos_start=None, pos_end=None): + """Set the text of the edit control.""" + self.verify_actionable() + + # allow one or both of pos_start and pos_end to be None + if pos_start is not None or pos_end is not None: + # if only one has been specified - then set the other + # to the current selection start or end + start, end = self.selection_indices() + if pos_start is None: + pos_start = start + if pos_end is None and not isinstance(start, six.string_types): + pos_end = end + else: + pos_start = 0 + pos_end = len(self.window_text()) + + if isinstance(text, six.text_type): + if six.PY3: + aligned_text = text + else: + aligned_text = text.encode(locale.getpreferredencoding()) + elif isinstance(text, six.binary_type): + if six.PY3: + aligned_text = text.decode(locale.getpreferredencoding()) + else: + aligned_text = text + else: + # convert a non-string input + if six.PY3: + aligned_text = six.text_type(text) + else: + aligned_text = six.binary_type(text) + + # Calculate new text value + current_text = self.window_text() + new_text = current_text[:pos_start] + aligned_text + current_text[pos_end:] + + self.set_native_property('Text', new_text) + + # time.sleep(Timings.after_editsetedittext_wait) + + if isinstance(aligned_text, six.text_type): + self.actions.log('Set text to the edit box: ' + aligned_text) + else: + self.actions.log(b'Set text to the edit box: ' + aligned_text) + + # return this control so that actions can be chained. + return self + # set set_text as an alias to set_edit_text + set_text = set_edit_text + + # ----------------------------------------------------------- + def select(self, start=0, end=None): + """Set the edit selection of the edit control.""" + self.verify_actionable() + self.set_focus() + + if isinstance(start, six.integer_types): + if isinstance(end, six.integer_types) and start > end: + start, end = end, start + elif end is None: + end = len(self.window_text()) + else: + # if we have been asked to select a string + if isinstance(start, six.text_type): + string_to_select = start + elif isinstance(start, six.binary_type): + string_to_select = start.decode(locale.getpreferredencoding()) + else: + raise ValueError('start and end should be integer or string') + + start = self.window_text().find(string_to_select) + if start < 0: + raise RuntimeError("Text '{0}' hasn't been found".format(string_to_select)) + end = start + len(string_to_select) + + self.set_native_property('SelectionStart', start) + self.set_native_property('SelectionLength', end - start) + + # return this control so that actions can be chained. + return self + + +class TabControlWrapper(wpfwrapper.WPFWrapper): + + """Wrap an WPF Tab control.""" + + _control_types = ['Tab'] + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control.""" + super(TabControlWrapper, self).__init__(elem) + + # ---------------------------------------------------------------- + def get_selected_tab(self): + """Return an index of a selected tab.""" + return self.get_native_property('SelectedIndex') + + # ---------------------------------------------------------------- + def tab_count(self): + """Return a number of tabs.""" + return len(self.children()) + + # ---------------------------------------------------------------- + def select(self, item): + """Select a tab by index or by name.""" + if isinstance(item, six.integer_types): + self.set_native_property('SelectedIndex', item) + else: + index = None + for child in self.iter_children(): + if child.element_info.rich_text == item: + index = 1 + if index is None: + raise IndexError('no such item: {}'.format(item)) + self.set_native_property('SelectedIndex', index) + return self + + # ---------------------------------------------------------------- + def texts(self): + """Tabs texts.""" + return self.children_texts() + + +class SliderWrapper(wpfwrapper.WPFWrapper): + + """Wrap an WPF Slider control.""" + + _control_types = ['Slider'] + has_title = False + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control.""" + super(SliderWrapper, self).__init__(elem) + + # ----------------------------------------------------------- + def min_value(self): + """Get the minimum value of the Slider.""" + return self.get_native_property('Minimum') + + # ----------------------------------------------------------- + def max_value(self): + """Get the maximum value of the Slider.""" + return self.get_native_property('Maximum') + + # ----------------------------------------------------------- + def small_change(self): + """Get a small change of slider's thumb. + + This change is achieved by pressing left and right arrows + when slider's thumb has keyboard focus. + """ + return self.get_native_property('SmallChange') + + # ----------------------------------------------------------- + def large_change(self): + """Get a large change of slider's thumb. + + This change is achieved by pressing PgUp and PgDown keys + when slider's thumb has keyboard focus. + """ + return self.get_native_property('LargeChange') + + # ----------------------------------------------------------- + def value(self): + """Get a current position of slider's thumb.""" + return self.get_native_property('Value') + + # ----------------------------------------------------------- + def set_value(self, value): + """Set position of slider's thumb.""" + if isinstance(value, float): + value_to_set = value + elif isinstance(value, six.integer_types): + value_to_set = value + elif isinstance(value, six.text_type): + value_to_set = float(value) + else: + raise ValueError("value should be either string or number") + + min_value = self.min_value() + max_value = self.max_value() + if not (min_value <= value_to_set <= max_value): + raise ValueError("value should be bigger than {0} and smaller than {1}".format(min_value, max_value)) + + self.set_native_property('Value', value_to_set) + + +class ToolbarWrapper(wpfwrapper.WPFWrapper): + + """Wrap an WPF ToolBar control. + + The control's children usually are: Buttons, SplitButton, + MenuItems, ThumbControls, TextControls, Separators, CheckBoxes. + Notice that ToolTip controls are children of the top window and + not of the toolbar. + """ + + _control_types = ['ToolBar'] + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control.""" + super(ToolbarWrapper, self).__init__(elem) + self.win32_wrapper = None + if len(self.children()) <= 1 and self.element_info.handle is not None: + self.win32_wrapper = common_controls.ToolbarWrapper(self.element_info.handle) + + @property + def writable_props(self): + """Extend default properties list.""" + props = super(ToolbarWrapper, self).writable_props + props.extend(['button_count']) + return props + + # ---------------------------------------------------------------- + def texts(self): + """Return texts of the Toolbar.""" + return [c.window_text() for c in self.buttons()] + + #---------------------------------------------------------------- + def button_count(self): + """Return a number of buttons on the ToolBar.""" + return len(self.children()) + + # ---------------------------------------------------------------- + def buttons(self): + """Return all available buttons.""" + return self.children() + + # ---------------------------------------------------------------- + def button(self, button_identifier, exact=True): + """Return a button by the specified identifier. + + * **button_identifier** can be either an index of a button or + a string with the text of the button. + * **exact** flag specifies if the exact match for the text look up + has to be applied. + """ + cc = self.buttons() + texts = [c.window_text() for c in cc] + + if isinstance(button_identifier, six.string_types): + self.actions.log('Toolbar buttons: ' + str(texts)) + + if exact: + try: + button_index = texts.index(button_identifier) + except ValueError: + raise findbestmatch.MatchError(items=texts, tofind=button_identifier) + else: + # one of these will be returned for the matching text + indices = list(range(0, len(texts))) + + # find which index best matches that text + button_index = findbestmatch.find_best_match(button_identifier, texts, indices) + else: + button_index = button_identifier + + return cc[button_index] + + # ---------------------------------------------------------------- + def check_button(self, button_identifier, make_checked, exact=True): + """Find where the button is and toggle it. + + * **button_identifier** can be either an index of the button or + a string with the text on the button. + * **make_checked** specifies the required toggled state of the button. + If the button is already in the specified state the state isn't changed. + * **exact** flag specifies if the exact match for the text look up + has to be applied + """ + self.actions.logSectionStart('Checking "' + self.window_text() + + '" toolbar button "' + str(button_identifier) + '"') + button = self.button(button_identifier, exact=exact) + if make_checked: + self.actions.log('Pressing down toolbar button "' + str(button_identifier) + '"') + else: + self.actions.log('Pressing up toolbar button "' + str(button_identifier) + '"') + + if not button.is_enabled(): + self.actions.log('Toolbar button is not enabled!') + raise RuntimeError("Toolbar button is not enabled!") + + res = (button.get_toggle_state() == ButtonWrapper.CHECKED) + if res != make_checked: + button.toggle() + + self.actions.logSectionEnd() + return button + + def collapse(self): + """Collapse overflow area of the ToolBar (IsOverflowOpen property).""" + self.set_native_property('IsOverflowOpen', False) + + def expand(self): + """Expand overflow area of the ToolBar (IsOverflowOpen property).""" + self.set_native_property('IsOverflowOpen', True) + + def is_expanded(self): + """Check if the ToolBar overflow area is currently visible.""" + return not self.get_native_property('HasOverflowItems') or self.get_native_property('IsOverflowOpen') + + def is_collapsed(self): + """Check if the ToolBar overflow area is not visible.""" + return not self.is_expanded() + + +class MenuItemWrapper(wpfwrapper.WPFWrapper): + + """Wrap an WPF MenuItem control.""" + + _control_types = ['MenuItem'] + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control.""" + super(MenuItemWrapper, self).__init__(elem) + + # ----------------------------------------------------------- + def items(self): + """Find all items of the menu item.""" + return self.children(control_type="MenuItem") + + # ----------------------------------------------------------- + def select(self): + """Select Menu item by raising Click event.""" + self.raise_event('Click') + + +class MenuWrapper(wpfwrapper.WPFWrapper): + + """Wrap an WPF MenuBar or Menu control.""" + + _control_types = ['Menu'] + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control.""" + super(MenuWrapper, self).__init__(elem) + + # ----------------------------------------------------------- + def items(self): + """Find all menu items.""" + return self.children(control_type="MenuItem") + + # ----------------------------------------------------------- + def item_by_index(self, idx): + """Find a menu item specified by the index.""" + item = self.items()[idx] + return item + + # ----------------------------------------------------------- + def _activate(self, item): + """Activate the specified item.""" + if not item.is_active(): + item.set_focus() + item.set_native_property('IsSubmenuOpen', True) + + # ----------------------------------------------------------- + def _sub_item_by_text(self, menu, name, exact): + """Find a menu sub-item by the specified text.""" + sub_item = None + items = menu.items() + if items: + if exact: + for i in items: + if name == i.window_text(): + sub_item = i + break + else: + texts = [] + for i in items: + texts.append(i.window_text()) + sub_item = findbestmatch.find_best_match(name, texts, items) + + if sub_item is None: + raise IndexError('Item `{}` not found'.format(name)) + + self._activate(sub_item) + return sub_item + + # ----------------------------------------------------------- + def _sub_item_by_idx(self, menu, idx): + """Find a menu sub-item by the specified index.""" + sub_item = None + items = menu.items() + if items: + sub_item = items[idx] + + if sub_item is None: + raise IndexError('Item with index {} not found'.format(idx)) + + self._activate(sub_item) + return sub_item + + # ----------------------------------------------------------- + def item_by_path(self, path, exact=False): + """Find a menu item specified by the path. + + The full path syntax is specified in: + :py:meth:`.controls.menuwrapper.Menu.get_menu_path` + + Note: $ - specifier is not supported + """ + # Get the path parts + menu_items = [p.strip() for p in path.split("->")] + items_cnt = len(menu_items) + if items_cnt == 0: + raise IndexError("Menu path has incorrect format, no item identifiers found") + for item in menu_items: + if not item: + raise IndexError("Empty item name between '->' separators") + + def next_level_menu(parent_menu, item_name): + if item_name.startswith("#"): + return self._sub_item_by_idx(parent_menu, int(item_name[1:])) + return self._sub_item_by_text(parent_menu, item_name, exact) + + # Find a top level menu item and select it. After selecting this item + # a new Menu control is created and placed on the dialog. It can be + # a direct child or a descendant. + # Sometimes we need to re-discover Menu again + i = 0 + try: + menu = next_level_menu(self, menu_items[i]) + if items_cnt == 1: + return menu + + if not menu.items(): + self._activate(menu) + timings.wait_until( + timings.Timings.window_find_timeout, + timings.Timings.window_find_retry, + lambda: len(self.top_level_parent().descendants(control_type="Menu")) > 0) + menu = self.top_level_parent().descendants(control_type="Menu")[0] + + for i in range(1, items_cnt): + menu = next_level_menu(menu, menu_items[i]) + except (AttributeError, IndexError): + raise IndexError('Incorrect menu path, item "{}" (index {}) not found'.format(menu_items[i], i)) + + return menu + + +class TreeItemWrapper(wpfwrapper.WPFWrapper): + + """Wrap an WPF TreeItem control. + + In addition to the provided methods of the wrapper + additional inherited methods can be especially helpful: + select(), extend(), collapse(), is_extended(), is_collapsed(), + click_input(), rectangle() and many others + """ + + _control_types = ['TreeItem'] + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control.""" + super(TreeItemWrapper, self).__init__(elem) + + # ----------------------------------------------------------- + def ensure_visible(self): + """Make sure that the TreeView item is visible.""" + self.invoke_method('BringIntoView') + + # ----------------------------------------------------------- + def get_child(self, child_spec, exact=False): + """Return the child item of this item. + + Accepts either a string or an index. + If a string is passed then it returns the child item + with the best match for the string. + """ + cc = self.children(control_type='TreeItem') + if isinstance(child_spec, six.string_types): + texts = [c.window_text() for c in cc] + if exact: + if child_spec in texts: + index = texts.index(child_spec) + else: + raise IndexError('There is no child equal to "' + str(child_spec) + '" in ' + str(texts)) + else: + indices = range(0, len(texts)) + index = findbestmatch.find_best_match( + child_spec, texts, indices, limit_ratio=.6) + + else: + index = child_spec + + return cc[index] + + # ----------------------------------------------------------- + def _calc_click_coords(self): + """Override the BaseWrapper helper method. + + Set coordinates close to a left part of the item rectangle + + The returned coordinates are always absolute + """ + # TODO get rectangle of text area + rect = self.rectangle() + coords = (rect.left + int(float(rect.width()) / 4.), + rect.top + int(float(rect.height()) / 2.)) + return coords + + # ----------------------------------------------------------- + def sub_elements(self, depth=None): + """Return a list of all visible sub-items of this control.""" + return self.descendants(control_type="TreeItem", depth=depth) + + def expand(self): + """Display child items of the tree item.""" + self.set_native_property('IsExpanded', True) + return self + + # ----------------------------------------------------------- + def collapse(self): + """Hide child items of the tree item.""" + self.set_native_property('IsExpanded', False) + return self + + # ----------------------------------------------------------- + def is_expanded(self): + """Test if the control is expanded.""" + return self.get_native_property('IsExpanded') + + # ----------------------------------------------------------- + def is_collapsed(self): + """Test if the control is collapsed.""" + return not self.get_native_property('IsExpanded') + + # ----------------------------------------------------------- + def select(self): + self.set_native_property('IsSelected', True) + return self + + # ----------------------------------------------------------- + def is_selected(self): + """Test if the control is expanded.""" + return self.get_native_property('IsSelected') + + +# ==================================================================== +class TreeViewWrapper(wpfwrapper.WPFWrapper): + + """Wrap an WPF Tree control.""" + + _control_types = ['Tree'] + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control.""" + super(TreeViewWrapper, self).__init__(elem) + + @property + def writable_props(self): + """Extend default properties list.""" + props = super(TreeViewWrapper, self).writable_props + props.extend(['item_count']) + return props + + # ----------------------------------------------------------- + def item_count(self, depth=None): + """Return a number of items in TreeView.""" + return len(self.descendants(control_type="TreeItem", depth=depth)) + + # ----------------------------------------------------------- + def roots(self): + """Return root elements of TreeView.""" + return self.children(control_type="TreeItem") + + # ----------------------------------------------------------- + def get_item(self, path, exact=False): + r"""Read a TreeView item. + + * **path** a path to the item to return. This can be one of + the following: + + * A string separated by \\ characters. The first character must + be \\. This string is split on the \\ characters and each of + these is used to find the specific child at each level. The + \\ represents the root item - so you don't need to specify the + root itself. + * A list/tuple of strings - The first item should be the root + element. + * A list/tuple of integers - The first item the index which root + to select. Indexing always starts from zero: get_item((0, 2, 3)) + + * **exact** a flag to request exact match of strings in the path + or apply a fuzzy logic of best_match thus allowing non-exact + path specifiers + """ + if not self.item_count(): + return None + + # Ensure the path is absolute + if isinstance(path, six.string_types): + if not path.startswith("\\"): + raise RuntimeError( + "Only absolute paths allowed - " + "please start the path with \\") + path = path.split("\\")[1:] + + # find the correct root elem + if isinstance(path[0], int): + current_elem = self.roots()[path[0]] + else: + roots = self.roots() + texts = [r.window_text() for r in roots] + if exact: + if path[0] in texts: + current_elem = roots[texts.index(path[0])] + else: + raise IndexError("There is no root element equal to '{0}'".format(path[0])) + else: + try: + current_elem = findbestmatch.find_best_match( + path[0], texts, roots, limit_ratio=.6) + except IndexError: + raise IndexError("There is no root element similar to '{0}'".format(path[0])) + + # now for each of the lower levels + # just index into it's children + for child_spec in path[1:]: + try: + # ensure that the item is expanded as this is sometimes + # required for loading tree view branches + current_elem.expand() + current_elem = current_elem.get_child(child_spec, exact) + except IndexError: + if isinstance(child_spec, six.string_types): + raise IndexError("Item '{0}' does not have a child '{1}'".format( + current_elem.window_text(), child_spec)) + raise IndexError("Item '{0}' does not have {1} children".format( + current_elem.window_text(), child_spec + 1)) + + return current_elem + + # ----------------------------------------------------------- + def print_items(self, max_depth=None): + """Print all items with line indents.""" + self.text = "" + + def _print_one_level(item, ident): + """Get texts for the item and its children.""" + self.text += " " * ident + item.window_text() + "\n" + if max_depth is None or ident <= max_depth: + for child in item.children(control_type="TreeItem"): + _print_one_level(child, ident + 1) + + for root in self.roots(): + _print_one_level(root, 0) + + return self.text + + +class ListItemWrapper(wpfwrapper.WPFWrapper): + + """Wrap an WPF ListViewItem and DataGrid row controls.""" + + _control_types = ['ListItem', 'DataItem'] + + # ----------------------------------------------------------- + def __init__(self, elem, container=None): + """Initialize the control.""" + super(ListItemWrapper, self).__init__(elem) + + # Init a pointer to the item's container wrapper. + # It must be set by a container wrapper producing the item. + # Notice that the self.parent property isn't the same + # because it results in a different instance of a wrapper. + self.container = container + + def texts(self): + """Return a list of item texts.""" + if self.element_info.control_type == 'ListItem': + return [self.window_text()] # ListBoxItem + children = self.children() + if len(children) == 1 and children[0].element_info.control_type == 'Pane': + items_holder = children[0] # ListViewItem + else: + items_holder = self # DataGridRow + return [elem.window_text() for elem in items_holder.children()] + + def select(self): + """Select the item. + + Usually applied for controls like: a radio button, a tree view item + or a list item. + """ + self.set_native_property('IsSelected', True) + + name = self.element_info.name + control_type = self.element_info.control_type + if name and control_type: + self.actions.log("Selected " + control_type.lower() + ' "' + name + '"') + + # Return itself so that action can be chained + return self + + # ----------------------------------------------------------- + def is_selected(self): + """Indicate that the item is selected or not. + + Usually applied for controls like: a radio button, a tree view item, + a list item. + """ + return self.get_native_property('IsSelected') + + +class ListViewWrapper(wpfwrapper.WPFWrapper): + + """Wrap an WPF ListView control.""" + + _control_types = ['List'] + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control.""" + super(ListViewWrapper, self).__init__(elem) + + def __getitem__(self, key): + return self.get_item(key) + + # ----------------------------------------------------------- + def item_count(self): + """A number of items in the List.""" + return len(self.children()) + + # ----------------------------------------------------------- + def cells(self): + """Return list of list of cells for any type of control.""" + return self.children(content_only=True) + + # ----------------------------------------------------------- + def get_item(self, row): + """Return an item of the ListView control. + + * **row** can be either an index of the row or a string + with the text of a cell in the row you want returned. + """ + # Verify arguments + if isinstance(row, six.string_types): + # Get DataGrid row + try: + itm = self.descendants(name=row)[0] + # Applications like explorer.exe usually return ListItem + # directly while other apps can return only a cell. + # In this case we need to take its parent - the whole row. + if not isinstance(itm, ListItemWrapper): + itm = itm.parent() + except IndexError: + raise ValueError("Element '{0}' not found".format(row)) + elif isinstance(row, six.integer_types): + # Get the item by a row index + list_items = self.children(content_only=True) + itm = list_items[row] + else: + raise TypeError("String type or integer is expected") + + # Give to the item a pointer on its container + itm.container = self + return itm + + item = get_item # this is an alias to be consistent with other content elements + + # ----------------------------------------------------------- + def get_items(self): + """Return all items of the ListView control.""" + return self.children(content_only=True) + + items = get_items # this is an alias to be consistent with other content elements + + # ----------------------------------------------------------- + def get_item_rect(self, item_index): + """Return the bounding rectangle of the list view item. + + The method is kept mostly for a backward compatibility + with the native ListViewWrapper interface + """ + itm = self.get_item(item_index) + return itm.rectangle() + + def get_selection(self): + """Get selected items.""" + # TODO get selected items directly from SelectedItems property + return [child for child in self.iter_children() if child.is_selected()] + + # ----------------------------------------------------------- + def get_selected_count(self): + """Return a number of selected items. + + The call can be quite expensive as we retrieve all + the selected items in order to count them + """ + selection = self.get_selection() + if selection: + return len(selection) + return 0 + + # ----------------------------------------------------------- + def texts(self): + """Return a list of item texts.""" + return [elem.texts() for elem in self.children(content_only=True)] + + # ----------------------------------------------------------- + @property + def writable_props(self): + """Extend default properties list.""" + props = super(ListViewWrapper, self).writable_props + props.extend(['item_count', + # 'items', + ]) + return props + + +class HeaderItemWrapper(wpfwrapper.WPFWrapper): + + """Wrap an WPF Header Item control.""" + + _control_types = ['HeaderItem'] + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control.""" + super(HeaderItemWrapper, self).__init__(elem) + + +class DataGridWrapper(wpfwrapper.WPFWrapper): + + """Wrap WPF controls that displays data in a customizable grid. + + It can be DataGrid or ListView control in a GridView mode. + """ + + _control_types = ['DataGrid'] + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control.""" + super(DataGridWrapper, self).__init__(elem) + + def __getitem__(self, key): + return self.get_item(key) + + # ----------------------------------------------------------- + def item_count(self): + """A number of items in the Grid.""" + return len(self.children(control_type='DataItem')) + + # ----------------------------------------------------------- + def column_count(self): + """Return the number of columns.""" + return len(self.children(control_type='HeaderItem')) + + # ----------------------------------------------------------- + def get_header_controls(self): + """Return Header controls associated with the Table.""" + return self.children(control_type='HeaderItem') + + columns = get_header_controls + + # ----------------------------------------------------------- + def get_column(self, col_index): + """Get the information for a column of the grid.""" + col = self.columns()[col_index] + return col + + # ----------------------------------------------------------- + def cells(self): + """Return list of list of cells for any type of control.""" + rows = self.children(control_type='DataItem') + + result = [] + for row in rows: + children = row.children() + if len(children) == 1 and children[0].element_info.control_type == 'Pane': + result.append(children[0].children()) + else: + result.append(children) + return result + + # ----------------------------------------------------------- + def cell(self, row, column): + """Return a cell in the grid. + + * **row** is an index of a row in the list. + * **column** is an index of a column in the specified row. + """ + if not isinstance(row, six.integer_types) or not isinstance(column, six.integer_types): + raise TypeError("row and column must be numbers") + + _row = self.get_item(row).children() + if len(_row) == 1 and _row[0].element_info.control_type == 'Pane': + cell_elem = _row[0].children()[column] + else: + cell_elem = _row[column] + return cell_elem + + # ----------------------------------------------------------- + def get_item(self, row): + """Return an item (elements in the specified row) of the grid control. + + * **row** can be either an index of the row or a string + with the text of a cell in the row you want returned. + """ + # Verify arguments + if isinstance(row, six.string_types): + # Get DataGrid row + try: + itm = self.descendants(name=row)[0] + # Applications like explorer.exe usually return ListItem + # directly while other apps can return only a cell. + # In this case we need to take its parent - the whole row. + while itm is not None and not isinstance(itm, ListItemWrapper): + itm = itm.parent() + except IndexError: + raise ValueError("Element '{0}' not found".format(row)) + elif isinstance(row, six.integer_types): + # Get the item by a row index + list_items = self.children(control_type='DataItem') + itm = list_items[row] + else: + raise TypeError("String type or integer is expected") + + # Give to the item a pointer on its container + if itm is not None: + itm.container = self + return itm + + item = get_item # this is an alias to be consistent with other content elements + + # ----------------------------------------------------------- + def get_items(self): + """Return all items of the grid control.""" + return self.children(control_type='DataItem') + + items = get_items # this is an alias to be consistent with other content elements + + # ----------------------------------------------------------- + def get_item_rect(self, item_index): + """Return the bounding rectangle of the list view item. + + The method is kept mostly for a backward compatibility + with the native ListViewWrapper interface + """ + itm = self.get_item(item_index) + return itm.rectangle() + + def get_selection(self): + # TODO get selected items directly from SelectedItems property + return [child for child in self.iter_children(control_type='DataItem') if child.is_selected()] + + # ----------------------------------------------------------- + def get_selected_count(self): + """Return a number of selected items. + + The call can be quite expensive as we retrieve all + the selected items in order to count them + """ + selection = self.get_selection() + if selection: + return len(selection) + else: + return 0 + + # ----------------------------------------------------------- + def texts(self): + """Return a list of item texts.""" + return [elem.texts() for elem in self.descendants(control_type='DataItem')] + + # ----------------------------------------------------------- + @property + def writable_props(self): + """Extend default properties list.""" + props = super(DataGridWrapper, self).writable_props + props.extend(['column_count', + 'item_count', + 'columns', + # 'items', + ]) + return props diff --git a/pywinauto/controls/wpfwrapper.py b/pywinauto/controls/wpfwrapper.py new file mode 100644 index 000000000..053657bc1 --- /dev/null +++ b/pywinauto/controls/wpfwrapper.py @@ -0,0 +1,287 @@ +# -*- coding: utf-8 -*- +# GUI Application automation and testing library +# Copyright (C) 2006-2017 Mark Mc Mahon and Contributors +# https://github.com/pywinauto/pywinauto/graphs/contributors +# http://pywinauto.readthedocs.io/en/latest/credits.html +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of pywinauto nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Basic wrapping of WPF elements.""" + +from __future__ import unicode_literals +from __future__ import print_function + +import six + +from .. import backend +from .win_base_wrapper import WinBaseWrapper +from ..base_wrapper import BaseMeta +from ..windows.injected.api import ConnectionManager +from ..windows.wpf_element_info import WPFElementInfo + + +class WpfMeta(BaseMeta): + + """Metaclass for WpfWrapper objects.""" + + control_type_to_cls = {} + + def __init__(cls, name, bases, attrs): + """Register the control types.""" + BaseMeta.__init__(cls, name, bases, attrs) + + for t in cls._control_types: + WpfMeta.control_type_to_cls[t] = cls + + @staticmethod + def find_wrapper(element): + """Find the correct wrapper for this WPF element.""" + # Check for a more specific wrapper in the registry + try: + wrapper_match = WpfMeta.control_type_to_cls[element.control_type] + except KeyError: + # Set a general wrapper by default + wrapper_match = WPFWrapper + + return wrapper_match + + +@six.add_metaclass(WpfMeta) +class WPFWrapper(WinBaseWrapper): + + """Default wrapper for WPF control in the specified process.""" + + _control_types = [] + + def __new__(cls, element_info): + """Construct the control wrapper.""" + return super(WPFWrapper, cls)._create_wrapper(cls, element_info, WPFWrapper) + + # ----------------------------------------------------------- + def __init__(self, element_info): + """Initialize the control. + + * **element_info** is either a valid UIAElementInfo or it can be an + instance or subclass of UIAWrapper. + If the handle is not valid then an InvalidWindowHandle error + is raised. + """ + WinBaseWrapper.__init__(self, element_info, backend.registry.backends['wpf']) + + def get_native_property(self, name, error_if_not_exists=False): + """Return value of the specified property of the .NET object corresponding to the element.""" + return self.element_info.get_native_property(name, error_if_not_exists) + + def get_native_properties(self): + """Return a dict with names and types of available properties of + the .NET object corresponding to the element.""" + return self.element_info.get_native_properties() + + def set_native_property(self, name, value, is_enum=False): + """Set value of the specified native property via .NET reflection. + + * **is_enum** set it to True if ``value`` is the name of enum member + """ + ConnectionManager().call_action('SetProperty', self.element_info.pid, + element_id=self.element_info.runtime_id, + name=name, + value=value, is_enum=is_enum) + return self + + def invoke_method(self, name): + """Invoke the specified method of the .NET object corresponding to the element. + + Method arguments are not supported yet. + """ + ConnectionManager().call_action('InvokeMethod', self.element_info.pid, + element_id=self.element_info.runtime_id, + name=name) + return self + + def raise_event(self, name): + """Raise the specified event of the .NET object corresponding to the element.""" + ConnectionManager().call_action('RaiseEvent', self.element_info.pid, + element_id=self.element_info.runtime_id, + name=name) + + def automation_id(self): + """Return the Automation ID of the control.""" + return self.element_info.auto_id + + def is_keyboard_focusable(self): + """Return True if the element can be focused with keyboard.""" + return self.get_native_property('Focusable') or False + + def has_keyboard_focus(self): + """Return True if the element is focused with keyboard.""" + return self.get_native_property('IsKeyboardFocused') or False + + def set_focus(self): + """Set the focus to this element.""" + ConnectionManager().call_action('SetFocus', self.element_info.pid, + element_id=self.element_info.runtime_id) + return self + + def get_active(self): + """Return wrapper object for current active element.""" + element_info = self.backend.element_info_class.get_active(self.element_info.pid) + if element_info is None: + return None + return self.backend.generic_wrapper_class(element_info) + + def is_active(self): + """Whether the window is active or not.""" + focused_wrap = self.get_active() + if focused_wrap is None: + return False + return focused_wrap.top_level_parent() == self.top_level_parent() + + def children_texts(self): + """Get texts of the control's children.""" + return [c.window_text() for c in self.children()] + + # System.Windows.WindowState enum + NORMAL = 0 + MAXIMIZED = 1 + MINIMIZED = 2 + + # ----------------------------------------------------------- + def close(self): + """Close the window.""" + self.invoke_method('Close') + + # ----------------------------------------------------------- + def minimize(self): + """Minimize the window.""" + self.set_native_property('WindowState', 'Minimized', is_enum=True) + return self + + # ----------------------------------------------------------- + def maximize(self): + """Maximize the window. + + Only controls supporting Window pattern should answer + """ + self.set_native_property('WindowState', 'Maximized', is_enum=True) + return self + + # ----------------------------------------------------------- + def restore(self): + """Restore the window to normal size. + + Only controls supporting Window pattern should answer + """ + # it's very strange, but set WindowState to Normal is not enough... + self.set_native_property('WindowState', 'Normal', is_enum=True) + restore_rect = self.get_native_property('RestoreBounds') + self.move_window(restore_rect['left'], + restore_rect['top'], + self.get_native_property('Width'), + self.get_native_property('Height')) + self.set_native_property('WindowState', 'Normal', is_enum=True) + return self + + # ----------------------------------------------------------- + def get_show_state(self): + """Get the show state and Maximized/minimized/restored state. + + Returns values as following + + Normal = 0 + Maximized = 1 + Minimized = 2 + """ + val = self.element_info.get_native_property('WindowState') + if val == 'Normal': + return self.NORMAL + if val == 'Maximized': + return self.MAXIMIZED + if val == 'Minimized': + return self.MINIMIZED + raise ValueError('Unexpected WindowState property value: ' + str(val)) + + # ----------------------------------------------------------- + def is_minimized(self): + """Indicate whether the window is minimized or not.""" + return self.get_show_state() == self.MINIMIZED + + # ----------------------------------------------------------- + def is_maximized(self): + """Indicate whether the window is maximized or not.""" + return self.get_show_state() == self.MAXIMIZED + + # ----------------------------------------------------------- + def is_normal(self): + """Indicate whether the window is normal (i.e. not minimized and not maximized).""" + return self.get_show_state() == self.NORMAL + + def move_window(self, x=None, y=None, width=None, height=None): + """Move the window to the new coordinates + The method should be implemented explicitly by controls that + support this action. The most obvious is the Window control. + Otherwise the method throws AttributeError. + + * **x** Specifies the new left position of the window. + Defaults to the current left position of the window. + * **y** Specifies the new top position of the window. + Defaults to the current top position of the window. + * **width** Specifies the new width of the window. Defaults to the + current width of the window. + * **height** Specifies the new height of the window. Default to the + current height of the window. + """ + raise AttributeError("This method is not supported for {0}".format(self)) + + def menu_select(self, path, exact=False, ): + """Select a menu item specified in the path. + + The full path syntax is specified in: + :py:meth:`pywinauto.menuwrapper.Menu.get_menu_path` + + There are usually at least two menu bars: "System" and "Application" + System menu bar is a standard window menu with items like: + 'Restore', 'Move', 'Size', 'Minimize', e.t.c. + It is not supported by backends based on DLL injection. + Application menu bar is often what we look for. In most cases, + its parent is the dialog itself so it should be found among the direct + children of the dialog. Notice that we don't use "Application" + string as a title criteria because it couldn't work on applications + with a non-english localization. + If there is no menu bar has been found we fall back to look up + for Menu control. We try to find the control through all descendants + of the dialog + """ + self.verify_actionable() + + cc = self.descendants(control_type="Menu") + if not cc: + raise AttributeError + menu = cc[0] + menu.item_by_path(path, exact).select() + + +backend.register('wpf', WPFElementInfo, WPFWrapper) diff --git a/pywinauto/unittests/test_wpf_element_info.py b/pywinauto/unittests/test_wpf_element_info.py new file mode 100644 index 000000000..723eda370 --- /dev/null +++ b/pywinauto/unittests/test_wpf_element_info.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +# GUI Application automation and testing library +# Copyright (C) 2006-2018 Mark Mc Mahon and Contributors +# https://github.com/pywinauto/pywinauto/graphs/contributors +# http://pywinauto.readthedocs.io/en/latest/credits.html +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of pywinauto nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import unittest +import os +import sys + +sys.path.append(".") +from pywinauto.windows.application import Application # noqa: E402 +from pywinauto.handleprops import processid # noqa: E402 +from pywinauto.sysinfo import is_x64_Python # noqa: E402 +from pywinauto.timings import Timings # noqa: E402 + +wpf_samples_folder = os.path.join( + os.path.dirname(__file__), r"..\..\apps\WPF_samples") +if is_x64_Python(): + wpf_samples_folder = os.path.join(wpf_samples_folder, 'x64') +wpf_app_1 = os.path.join(wpf_samples_folder, u"WpfApplication1.exe") + + +class WPFElementInfoTests(unittest.TestCase): + """Unit tests for the WPFElementInfo class""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + Timings.slow() + + self.app = Application(backend="wpf") + self.app = self.app.start(wpf_app_1) + + self.dlg = self.app.WPFSampleApplication + self.handle = self.dlg.handle + self.ctrl = self.dlg.find().element_info + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def testHash(self): + """Test element info hashing""" + d = {self.ctrl: "elem"} + self.assertEqual(d[self.ctrl], "elem") + + def testProcessId(self): + """Test process_id equals""" + self.assertEqual(self.ctrl.process_id, processid(self.handle)) + + def testName(self): + """Test application name equals""" + self.assertEqual(self.ctrl.name, "WPF Sample Application") + + def testHandle(self): + """Test application handle equals""" + self.assertEqual(self.ctrl.handle, self.handle) + + def testEnabled(self): + """Test whether the element is enabled""" + self.assertEqual(self.ctrl.enabled, True) + + def testVisible(self): + """Test whether the element is visible""" + self.assertEqual(self.ctrl.visible, True) + + def testChildren(self): + """Test whether a list of only immediate children of the element is equal""" + self.assertEqual(len(self.ctrl.children()), 1) + self.assertEqual(len(self.ctrl.children()[0].children()[0].children()), 2) + + def test_children_generator(self): + """Test whether children generator iterates over correct elements""" + children = [child for child in self.ctrl.iter_children()] + self.assertSequenceEqual(self.ctrl.children(), children) + + def test_default_depth_descendants(self): + """Test whether a list of descendants with default depth of the element is equal""" + self.assertEqual(len(self.ctrl.descendants(depth=None)), len(self.ctrl.descendants())) + + def test_depth_level_one_descendants(self): + """Test whether a list of descendants with depth=1 of the element is equal to children set""" + self.assertEqual(len(self.ctrl.descendants(depth=1)), len(self.ctrl.children())) + + def test_depth_level_three_descendants(self): + """Test whether a list of descendants with depth=3 of the element is equal""" + descendants = self.ctrl.children() + + level_two_children = [] + for element in descendants: + level_two_children.extend(element.children()) + descendants.extend(level_two_children) + + level_three_children = [] + for element in level_two_children: + level_three_children.extend(element.children()) + descendants.extend(level_three_children) + + self.assertEqual(len(self.ctrl.descendants(depth=3)), len(descendants)) + + def test_invalid_depth_descendants(self): + """Test whether a list of descendants with invalid depth raises exception""" + self.assertRaises(Exception, self.ctrl.descendants, depth='qwerty') + + def test_descendants_generator(self): + """Test whether descendant generator iterates over correct elements""" + descendants = [desc for desc in self.ctrl.iter_descendants(depth=3)] + self.assertSequenceEqual(self.ctrl.descendants(depth=3), descendants) + + +if __name__ == "__main__": + unittest.main() diff --git a/pywinauto/unittests/test_wpfwrapper.py b/pywinauto/unittests/test_wpfwrapper.py new file mode 100644 index 000000000..0dd32c2d0 --- /dev/null +++ b/pywinauto/unittests/test_wpfwrapper.py @@ -0,0 +1,1374 @@ +# -*- coding: utf-8 -*- +# GUI Application automation and testing library +# Copyright (C) 2006-2018 Mark Mc Mahon and Contributors +# https://github.com/pywinauto/pywinauto/graphs/contributors +# http://pywinauto.readthedocs.io/en/latest/credits.html +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of pywinauto nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Tests for WPFWrapper""" +from __future__ import print_function +from __future__ import unicode_literals + +import time +import os +import sys +import collections +import unittest +import mock + +sys.path.append(".") +from pywinauto.windows.application import Application # noqa: E402 +from pywinauto.base_application import WindowSpecification # noqa: E402 +from pywinauto.sysinfo import is_x64_Python # noqa: E402 +from pywinauto.timings import Timings # noqa: E402 +from pywinauto.actionlogger import ActionLogger # noqa: E402 +from pywinauto import mouse # noqa: E402 + +import pywinauto.controls.wpf_controls as wpf_ctls +from pywinauto.controls.wpfwrapper import WPFWrapper +from pywinauto.windows.injected.api import InjectedBaseError +from pywinauto.windows.injected.channel import InjectedBrokenPipeError + +wpf_samples_folder = os.path.join( + os.path.dirname(__file__), r"..\..\apps\WPF_samples") +if is_x64_Python(): + wpf_samples_folder = os.path.join(wpf_samples_folder, 'x64') +wpf_app_1 = os.path.join(wpf_samples_folder, u"WpfApplication1.exe") + + +def _set_timings(): + """Setup timings for WPF related tests""" + Timings.defaults() + Timings.window_find_timeout = 20 + + +class WPFWrapperTests(unittest.TestCase): + + """Unit tests for the WPFWrapper class""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + mouse.move((-500, 500)) # remove the mouse from the screen to avoid side effects + + # start the application + self.app = Application(backend='wpf') + self.app = self.app.start(wpf_app_1) + + self.dlg = self.app.WPFSampleApplication + + def test_get_active_wpf(self): + focused_element = self.dlg.get_active() + self.assertTrue(type(focused_element) is WPFWrapper or issubclass(type(focused_element), WPFWrapper)) + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_issue_278(self): + """Test that statement menu = app.MainWindow.Menu works for 'wpf' backend""" + menu_spec = self.dlg.Menu + self.assertTrue(isinstance(menu_spec, WindowSpecification)) + # Also check the app binding + self.assertTrue(menu_spec.app, self.app) + + def test_find_nontop_ctl_by_class_name_and_title(self): + """Test getting a non-top control by a class name and a title""" + # Look up for a non-top button control with 'Apply' caption + self.dlg.wait('ready') + caption = 'Apply' + wins = self.app.windows(top_level_only=False, + class_name='Button', + name=caption) + + # Verify the number of found wrappers + self.assertEqual(len(wins), 1) + + # Verify the caption of the found wrapper + self.assertEqual(wins[0].texts()[0], caption) + + def test_find_top_win_by_class_name_and_title(self): + """Test getting a top window by a class name and a title""" + # Since the top_level_only is True by default + # we don't specify it as a criteria argument + self.dlg.wait('ready') + caption = 'WPF Sample Application' + wins = self.app.windows(class_name='MainWindow', name=caption) + + # Verify the number of found wrappers + self.assertEqual(len(wins), 1) + + # Verify the caption of the found wrapper + self.assertEqual(wins[0].texts()[0], caption) + + def test_class(self): + """Test getting the classname of the dialog""" + button = self.dlg.by(class_name="Button", + name="OK").find() + self.assertEqual(button.class_name(), "Button") + + def test_window_text(self): + """Test getting the window Text of the dialog""" + label = self.dlg.TestLabel.find() + self.assertEqual(label.window_text(), u"TestLabel") + # TODO + # self.assertEqual(label.can_be_label, True) + + def test_control_id(self): + """Test getting control ID""" + button = self.dlg.by(class_name="Button", + name="OK").find() + self.assertNotEqual(button.control_id(), None) + + def test_automation_id(self): + """Test getting automation ID""" + alpha_toolbar = self.dlg.by(name="Alpha", control_type="ToolBar") + button = alpha_toolbar.by(control_type="Button", + auto_id="toolbar_button1").find() + self.assertEqual(button.automation_id(), "toolbar_button1") + + def test_is_visible(self): + """Test is_visible method of a control""" + button = self.dlg.by(class_name="Button", + name="OK").find() + self.assertEqual(button.is_visible(), True) + + def test_is_enabled(self): + """Test is_enabled method of a control""" + button = self.dlg.by(class_name="Button", + name="OK").find() + self.assertEqual(button.is_enabled(), True) + + def test_process_id(self): + """Test process_id method of a control""" + button = self.dlg.by(class_name="Button", + name="OK").find() + self.assertEqual(button.process_id(), self.dlg.process_id()) + self.assertNotEqual(button.process_id(), 0) + + def test_is_dialog(self): + """Test is_dialog method of a control""" + button = self.dlg.by(class_name="Button", + name="OK").find() + self.assertEqual(button.is_dialog(), False) + self.assertEqual(self.dlg.is_dialog(), True) + + def test_parent(self): + """Test getting a parent of a control""" + toolbar = self.dlg.Alpha.find() + self.assertEqual(toolbar.parent(), self.dlg.ToolBarTray.find()) + + def test_top_level_parent(self): + """Test getting a top-level parent of a control""" + button = self.dlg.by(class_name="Button", + name="OK").find() + self.assertEqual(button.top_level_parent(), self.dlg.find()) + + def test_texts(self): + """Test getting texts of a control""" + self.assertEqual(self.dlg.texts(), ['WPF Sample Application']) + + def test_get_properties(self): + """Test getting list of properties of a control""" + props_dict = self.dlg.get_native_properties() + self.assertTrue(type(props_dict) is dict) + self.assertIn('Name', props_dict) + self.assertEquals(props_dict['Name'], 'String') + self.assertGreater(len(props_dict), 25) + + def test_children(self): + """Test getting children of a control""" + tab_ctrl = self.dlg.by(class_name="TabControl").find() + self.assertEqual(len(tab_ctrl.children()), 3) + self.assertEqual(tab_ctrl.children()[0].class_name(), "TabItem") + + def test_children_generator(self): + """Test iterating children of a control""" + tab_ctrl = self.dlg.by(class_name="TabControl").find() + children = [child for child in tab_ctrl.iter_children()] + self.assertEqual(len(children), 3) + self.assertEqual(children[0].class_name(), "TabItem") + + def test_descendants(self): + """Test iterating descendants of a control""" + toolbar = self.dlg.by(class_name="RichTextBox").find() + descendants = toolbar.descendants() + self.assertEqual(len(descendants), 11) + + def test_descendants_generator(self): + toolbar = self.dlg.by(name="Alpha", control_type="ToolBar").find() + descendants = [desc for desc in toolbar.iter_descendants()] + self.assertSequenceEqual(toolbar.descendants(), descendants) + + def test_is_child(self): + """Test is_child method of a control""" + toolbar = self.dlg.Alpha.find() + self.assertEqual(toolbar.is_child(self.dlg.ToolBarTray.find()), True) + + def test_equals(self): + """Test controls comparisons""" + button = self.dlg.by(class_name="Button", + name="OK").find() + self.assertNotEqual(button, self.dlg.find()) + self.assertEqual(button, button.element_info) + self.assertEqual(button, button) + + def test_is_keyboard_focusable(self): + """Test is_keyboard focusable method of several controls""" + edit = self.dlg.by(auto_id='edit1').find() + label = self.dlg.TestLabel.find() + button = self.dlg.by(class_name="Button", + name="OK").find() + self.assertEqual(button.is_keyboard_focusable(), True) + self.assertEqual(edit.is_keyboard_focusable(), True) + self.assertEqual(label.is_keyboard_focusable(), False) + + def test_set_focus(self): + """Test setting a keyboard focus on a control""" + edit = self.dlg.by(auto_id='edit1').find() + edit.set_focus() + self.assertEqual(edit.has_keyboard_focus(), True) + + def test_type_keys(self): + """Test sending key types to a control""" + edit = self.dlg.by(auto_id='edit1').find() + edit.type_keys("t") + self.assertEqual(edit.window_text(), "t") + edit.type_keys("e") + self.assertEqual(edit.window_text(), "te") + edit.type_keys("s") + self.assertEqual(edit.window_text(), "tes") + edit.type_keys("t") + self.assertEqual(edit.window_text(), "test") + edit.type_keys("T") + self.assertEqual(edit.window_text(), "testT") + edit.type_keys("y") + self.assertEqual(edit.window_text(), "testTy") + + def test_capture_as_image_multi_monitor(self): + with mock.patch('win32api.EnumDisplayMonitors') as mon_device: + mon_device.return_value = (1, 2) + rect = self.dlg.rectangle() + expected = (rect.width(), rect.height()) + result = self.dlg.capture_as_image().size + self.assertEqual(expected, result) + + +class WPFWrapperMouseTests(unittest.TestCase): + + """Unit tests for mouse actions of the WPFWrapper class""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + + self.app = Application(backend='wpf') + self.app = self.app.start(wpf_app_1) + + dlg = self.app.WPFSampleApplication + self.button = dlg.by(class_name="Button", + name="OK").find() + + self.label = dlg.by(class_name="Label", name="TestLabel").find() + self.app.wait_cpu_usage_lower(threshold=1.5, timeout=30, usage_interval=1.0) + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_click_input(self): + """Test click_input method of a control""" + self.button.click_input() + self.assertEqual(self.label.window_text(), "LeftClick") + + def test_double_click_input(self): + """Test double_click_input method of a control""" + self.button.double_click_input() + self.assertEqual(self.label.window_text(), "DoubleClick") + + def test_right_click_input(self): + """Test right_click_input method of a control""" + self.button.right_click_input() + self.assertEqual(self.label.window_text(), "RightClick") + + +class WindowWrapperTests(unittest.TestCase): + + """Unit tests for the WPFWrapper class""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + mouse.move((-500, 500)) # remove the mouse from the screen to avoid side effects + + # start the application + self.app = Application(backend='wpf') + self.app = self.app.start(wpf_app_1) + + self.dlg = self.app.WPFSampleApplication + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_close(self): + """Test close method of a control""" + wrp = self.dlg.find() + wrp.close() + + try: + # process can be in the termination state at this moment + self.assertEqual(self.dlg.exists(), False) + except (InjectedBaseError, InjectedBrokenPipeError): + pass + + def test_move_window(self): + """Test move_window without any parameters""" + + # move_window with default parameters + prevRect = self.dlg.rectangle() + self.dlg.move_window() + self.assertEqual(prevRect, self.dlg.rectangle()) + + # move_window call for a not supported control + button = self.dlg.by(class_name="Button", name="OK") + self.assertRaises(AttributeError, button.move_window) + + # Make RECT stub to avoid import win32structures + Rect = collections.namedtuple('Rect', 'left top right bottom') + prev_rect = self.dlg.rectangle() + new_rect = Rect._make([i + 5 for i in prev_rect]) + + self.dlg.move_window( + new_rect.left, + new_rect.top, + new_rect.right - new_rect.left, + new_rect.bottom - new_rect.top + ) + time.sleep(0.1) + logger = ActionLogger() + logger.log("prev_rect = %s", prev_rect) + logger.log("new_rect = %s", new_rect) + logger.log("self.dlg.rectangle() = %s", self.dlg.rectangle()) + self.assertEqual(self.dlg.rectangle(), new_rect) + + self.dlg.move_window(prev_rect) + self.assertEqual(self.dlg.rectangle(), prev_rect) + + def test_minimize_maximize(self): + """Test window minimize/maximize operations""" + wrp = self.dlg.minimize() + self.dlg.wait_not('active') + self.assertEqual(wrp.is_minimized(), True) + wrp.maximize() + self.dlg.wait('active') + self.assertEqual(wrp.is_maximized(), True) + wrp.minimize() + self.dlg.wait_not('active') + wrp.restore() + self.dlg.wait('active') + self.assertEqual(wrp.is_normal(), True) + + +class ButtonWrapperTests(unittest.TestCase): + + """Unit tests for the WPF controls inherited from ButtonBase""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + + # start the application + self.app = Application(backend='wpf') + self.app = self.app.start(wpf_app_1) + + self.dlg = self.app.WPFSampleApplication + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_check_box(self): + """Test 'toggle' and 'toggle_state' for the check box control""" + # Get a current state of the check box control + check_box = self.dlg.CheckBox.find() + cur_state = check_box.get_toggle_state() + self.assertEqual(cur_state, wpf_ctls.ButtonWrapper.INDETERMINATE) + + # Toggle the next state + cur_state = check_box.toggle().get_toggle_state() + + # Get a new state of the check box control + self.assertEqual(cur_state, wpf_ctls.ButtonWrapper.UNCHECKED) + + cur_state = check_box.select().get_toggle_state() + self.assertEqual(cur_state, wpf_ctls.ButtonWrapper.CHECKED) + + def test_toggle_button(self): + """Test 'toggle' and 'toggle_state' for the toggle button control""" + # Get a current state of the check box control + button = self.dlg.ToggleMe.find() + cur_state = button.get_toggle_state() + self.assertEqual(cur_state, button.CHECKED) + + # Toggle the next state + cur_state = button.toggle().get_toggle_state() + + # Get a new state of the check box control + self.assertEqual(cur_state, button.UNCHECKED) + + # Toggle the next state + cur_state = button.toggle().get_toggle_state() + self.assertEqual(cur_state, button.CHECKED) + + def test_button_click(self): + """Test the click method for the Button control""" + label = self.dlg.by(class_name="Label", + name="TestLabel").find() + self.dlg.Apply.click() + self.assertEqual(label.window_text(), "ApplyClick") + + def test_radio_button(self): + """Test 'select' and 'is_selected' for the radio button control""" + yes = self.dlg.Yes.find() + cur_state = yes.is_selected() + self.assertEqual(cur_state, False) + + cur_state = yes.select().is_selected() + self.assertEqual(cur_state, True) + + no = self.dlg.No.find() + cur_state = no.select().is_selected() + self.assertEqual(cur_state, True) + +class ComboBoxTests(unittest.TestCase): + + """Unit tests for the ComboBoxWrapper class with WPF app""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + + # start the application + self.app = Application(backend='wpf') + self.app = self.app.start(wpf_app_1) + + self.dlg = self.app.WPFSampleApplication + self.combo = self.dlg.by(control_type="ComboBox").find() + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_expand_collapse(self): + """Test methods .expand() and .collapse() for WPF combo box""" + self.dlg.set_focus() + + combo = self.combo + combo_name = self.combo.element_info.name + + self.assertFalse(combo.is_expanded(), + msg='{} combo box must be collapsed initially'.format(combo_name)) + + # test that method allows chaining + self.assertEqual(combo.expand(), combo, + msg='Method .expand() for {} combo box must return self'.format(combo_name)) + self.assertTrue(combo.is_expanded(), + msg='{} combo box has not been expanded!'.format(combo_name)) + + # .expand() keeps already expanded state (and still allows chaining) + self.assertEqual(combo.expand(), combo, + msg='Method .expand() for {} combo box must return self, always!'.format(combo_name)) + self.assertTrue(combo.is_expanded(), + msg='{} combo box does NOT keep expanded state!'.format(combo_name)) + + # collapse + self.assertEqual(combo.collapse(), combo, + msg='Method .collapse() for {} combo box must return self'.format(combo_name)) + self.assertFalse(combo.is_expanded(), + msg='{} combo box has not been collapsed!'.format(combo_name)) + + # collapse already collapsed should keep collapsed state + self.assertEqual(combo.collapse(), combo, + msg='Method .collapse() for {} combo box must return self, always!'.format(combo_name)) + self.assertFalse(combo.is_expanded(), + msg='{} combo box does NOT keep collapsed state!'.format(combo_name)) + + def test_texts(self): + """Test method .texts() for WPF combo box""" + self.dlg.set_focus() + texts = [u'Combo Item 1', u'Combo Item 2'] + + self.assertEqual(self.combo.texts(), texts) + self.assertEqual(self.combo.expand().texts(), texts) + self.assertTrue(self.combo.is_expanded()) + + def test_select(self): + """Test method .select() for WPF combo box""" + self.dlg.set_focus() + + self.assertEqual(self.combo.selected_index(), -1) # nothing selected + self.combo.select(u'Combo Item 2') + self.assertEqual(self.combo.selected_text(), u'Combo Item 2') + self.assertEqual(self.combo.selected_index(), 1) + self.combo.select(0) + self.assertEqual(self.combo.selected_text(), u'Combo Item 1') + self.assertEqual(self.combo.selected_index(), 0) + + def test_item_count(self): + """Test method .item_count() for WPF combo box""" + self.dlg.set_focus() + self.assertEqual(self.combo.item_count(), 2) + + +class EditWrapperTests(unittest.TestCase): + + """Unit tests for the EditWrapper class""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + + # start the application + app = Application(backend='wpf') + app = app.start(wpf_app_1) + + self.app = app + self.dlg = app.WPFSampleApplication + + self.edit = self.dlg.by(class_name="TextBox").find() + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_set_window_text(self): + """Test setting text value of control (the text in textbox itself)""" + text_to_set = "This test" + + self.edit.set_window_text(text_to_set) + self.assertEqual(self.edit.text_block(), text_to_set) + + self.edit.set_window_text(" is done", True) + self.assertEqual(self.edit.text_block(), text_to_set + " is done") + + def test_set_text(self): + """Test setting the text of the edit control""" + self.edit.set_edit_text("Some text") + self.assertEqual(self.edit.text_block(), "Some text") + + self.edit.set_edit_text(579) + self.assertEqual(self.edit.text_block(), "579") + + self.edit.set_edit_text(333, pos_start=1, pos_end=2) + self.assertEqual(self.edit.text_block(), "53339") + + def test_line_count(self): + """Test getting the line count of the edit control""" + self.edit.set_edit_text("Here is some text") + + self.assertEqual(self.edit.line_count(), 1) + + def test_get_line(self): + """Test getting each line of the edit control""" + test_data = "Here is some text" + self.edit.set_edit_text(test_data) + + self.assertEqual(self.edit.get_line(0), test_data) + + def test_get_value(self): + """Test getting value of the edit control""" + test_data = "Some value" + self.edit.set_edit_text(test_data) + + self.assertEqual(self.edit.get_value(), test_data) + + def test_text_block(self): + """Test getting the text block of the edit control""" + test_data = "Here is some text" + self.edit.set_edit_text(test_data) + + self.assertEqual(self.edit.text_block(), test_data) + + def test_select(self): + """Test selecting text in the edit control in various ways""" + self.edit.set_edit_text("Some text") + + self.edit.select(0, 0) + self.assertEqual((0, 0), self.edit.selection_indices()) + + self.edit.select() + self.assertEqual((0, 9), self.edit.selection_indices()) + + self.edit.select(1, 7) + self.assertEqual((1, 7), self.edit.selection_indices()) + + self.edit.select(5, 2) + self.assertEqual((2, 5), self.edit.selection_indices()) + + self.edit.select("me t") + self.assertEqual((2, 6), self.edit.selection_indices()) + + self.assertRaises(RuntimeError, self.edit.select, "123") + + +class TabControlWrapperTests(unittest.TestCase): + + """Unit tests for the TabControlWrapper class""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + + # start the application + app = Application(backend='wpf') + app = app.start(wpf_app_1) + dlg = app.WPFSampleApplication + + self.app = app + self.ctrl = dlg.by(class_name="TabControl").find() + self.texts = [u"General", u"Tree and List Views", u"ListBox and Grid"] + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_tab_count(self): + """Test the tab count in the Tab control""" + self.assertEqual(self.ctrl.tab_count(), len(self.texts)) + + def test_get_selected_tab(self): + """Test selecting a tab by index or by name and getting an index of the selected tab""" + # Select a tab by name, use chaining to get the index of the selected tab + idx = self.ctrl.select(u"Tree and List Views").get_selected_tab() + self.assertEqual(idx, 1) + # Select a tab by index + self.ctrl.select(0) + self.assertEqual(self.ctrl.get_selected_tab(), 0) + + def test_texts(self): + """Make sure the tabs captions are read correctly""" + self.assertEqual(self.ctrl.texts(), self.texts) + + +class SliderWrapperTests(unittest.TestCase): + + """Unit tests for the SliderWrapper class""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + + # start the application + app = Application(backend='wpf') + app = app.start(wpf_app_1) + + self.app = app + self.dlg = app.WPFSampleApplication + + self.slider = self.dlg.by(class_name="Slider").find() + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_min_value(self): + """Test getting minimum value of the Slider""" + self.assertEqual(self.slider.min_value(), 0.0) + + def test_max_value(self): + """Test getting maximum value of the Slider""" + self.assertEqual(self.slider.max_value(), 100.0) + + def test_small_change(self): + """Test Getting small change of slider's thumb""" + self.assertEqual(self.slider.small_change(), 0.1) + + def test_large_change(self): + """Test Getting large change of slider's thumb""" + self.assertEqual(self.slider.large_change(), 1.0) + + def test_value(self): + """Test getting current position of slider's thumb""" + self.assertEqual(self.slider.value(), 70.0) + + def test_set_value(self): + """Test setting position of slider's thumb""" + self.slider.set_value(24) + self.assertEqual(self.slider.value(), 24.0) + + self.slider.set_value(33.3) + self.assertEqual(self.slider.value(), 33.3) + + self.slider.set_value("75.4") + self.assertEqual(self.slider.value(), 75.4) + + self.assertRaises(ValueError, self.slider.set_value, -1) + self.assertRaises(ValueError, self.slider.set_value, 102) + + self.assertRaises(ValueError, self.slider.set_value, [50, ]) + + +class ToolbarWpfTests(unittest.TestCase): + + """Unit tests for ToolbarWrapper class on WPF demo""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + + # start the application + self.app = Application(backend='wpf') + self.app = self.app.start(wpf_app_1) + self.dlg = self.app.WPFSampleApplication + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_button_access_wpf(self): + """Test getting access to buttons on Toolbar of WPF demo""" + # Read a second toolbar with buttons: "button1, button2" + tb = self.dlg.Toolbar2.find() + self.assertEqual(tb.button_count(), 2) + self.assertEqual(len(tb.texts()), 2) + + # Test if it's in writable properties + props = set(tb.get_properties().keys()) + self.assertEqual('button_count' in props, True) + + expect_txt = "button 1" + self.assertEqual(tb.button(0).window_text(), expect_txt) + + found_txt = tb.button(expect_txt, exact=True).window_text() + self.assertEqual(found_txt, expect_txt) + + found_txt = tb.button("b 1", exact=False).window_text() + self.assertEqual(found_txt, expect_txt) + + expect_txt = "button 2" + found_txt = tb.button(expect_txt, exact=True).window_text() + self.assertEqual(found_txt, expect_txt) + + # Notice that findbestmatch.MatchError is subclassed from IndexError + self.assertRaises(IndexError, tb.button, "BaD n_$E ", exact=False) + + def test_overflow_area_status(self): + """Check if overflow area visible (note: OverflowButton is inactive in the sample""" + tb = self.dlg.Toolbar2.find() + self.assertTrue(tb.is_expanded()) + self.assertFalse(tb.is_collapsed()) + + +class MenuWrapperWpfTests(unittest.TestCase): + + """Unit tests for the MenuWrapper class on WPF demo""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + + # start the application + self.app = Application(backend='wpf') + self.app = self.app.start(wpf_app_1) + self.dlg = self.app.WPFSampleApplication + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_menu_by_index(self): + """Test selecting a WPF menu item by index""" + path = "#0->#1->#1" # "File->Close->Later" + self.dlg.menu_select(path) + label = self.dlg.MenuLaterClickStatic.find() + self.assertEqual(label.window_text(), u"MenuLaterClick") + + # Non-existing paths + path = "#5->#1" + self.assertRaises(IndexError, self.dlg.menu_select, path) + path = "#0->#1->#1->#2->#3" + self.assertRaises(IndexError, self.dlg.menu_select, path) + + def test_menu_by_exact_text(self): + """Test selecting a WPF menu item by exact text match""" + path = "_File->_Close->_Later" + self.dlg.menu_select(path, True) + label = self.dlg.MenuLaterClickStatic.find() + self.assertEqual(label.window_text(), u"MenuLaterClick") + + # A non-exact menu name + path = "File->About" + self.assertRaises(IndexError, self.dlg.menu_select, path, True) + + def test_menu_by_best_match_text(self): + """Test selecting a WPF menu item by best match text""" + path = "file-> close -> later" + self.dlg.menu_select(path, False) + label = self.dlg.MenuLaterClickStatic.find() + self.assertEqual(label.window_text(), u"MenuLaterClick") + + def test_menu_by_mixed_match(self): + """Test selecting a WPF menu item by a path with mixed specifiers""" + path = "file-> #1 -> later" + self.dlg.menu_select(path, False) + label = self.dlg.MenuLaterClickStatic.find() + self.assertEqual(label.window_text(), u"MenuLaterClick") + + # Bad specifiers + path = "file-> 1 -> later" + self.assertRaises(IndexError, self.dlg.menu_select, path) + path = "#0->#1->1" + self.assertRaises(IndexError, self.dlg.menu_select, path) + path = "0->#1->1" + self.assertRaises(IndexError, self.dlg.menu_select, path) + + +class TreeViewWpfTests(unittest.TestCase): + + """Unit tests for TreeViewWrapper class on WPF demo""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + + # start the application + self.app = Application(backend='wpf') + self.app = self.app.start(wpf_app_1) + self.dlg = self.app.WPFSampleApplication + tab_itm = self.dlg.TreeAndListViews.set_focus() + self.ctrl = tab_itm.descendants(control_type="Tree")[0] + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_tv_item_count_and_roots(self): + """Test getting roots and a total number of items in TreeView""" + self.assertEqual(self.ctrl.item_count(), 27) + + # Test if it's in writable properties + props = set(self.ctrl.get_properties().keys()) + self.assertEqual('item_count' in props, True) + + roots = self.ctrl.roots() + self.assertEqual(len(roots), 1) + self.assertEqual(roots[0].texts()[0], u'Date Elements') + + sub_items = roots[0].sub_elements(depth=1) + self.assertEqual(len(sub_items), 4) + self.assertEqual(sub_items[0].window_text(), u'Empty Date') + self.assertEqual(sub_items[-1].window_text(), u'Years') + + expected_str = "Date Elements\n Empty Date\n Week\n Monday\n Tuesday\n Wednsday\n" + expected_str += " Thursday\n Friday\n Saturday\n Sunday\n " + expected_str += "Months\n January\n February\n March\n April\n June\n July\n August\n Semptember\n " + expected_str += "October\n November\n December\n Years\n 2015\n 2016\n 2017\n 2018\n" + self.assertEqual(self.ctrl.print_items(), expected_str) + + def test_tv_item_select(self): + """Test selecting an item from TreeView""" + # Find by a path with indexes + itm = self.ctrl.get_item((0, 2, 3)) + self.assertEqual(itm.is_selected(), False) + + # Select + itm.select() + self.assertEqual(itm.is_selected(), True) + + # A second call to Select doesn't remove selection + itm.select() + self.assertEqual(itm.is_selected(), True) + + itm = self.ctrl.get_item((0, 3, 2)) + itm.ensure_visible() + self.assertEqual(itm.is_selected(), False) + # coords = itm.children(control_type='Text')[0].rectangle().mid_point() + # itm.click_input(coords=coords, absolute=True) + # self.assertEqual(itm.is_selected(), True) + + def test_tv_get_item(self): + """Test getting an item from TreeView""" + # Find by a path with indexes + itm = self.ctrl.get_item((0, 2, 3)) + self.assertEqual(isinstance(itm, wpf_ctls.TreeItemWrapper), True) + self.assertEqual(itm.window_text(), u'April') + + # Find by a path with strings + itm = self.ctrl.get_item('\\Date Elements\\Months\\April', exact=True) + self.assertEqual(isinstance(itm, wpf_ctls.TreeItemWrapper), True) + self.assertEqual(itm.window_text(), u'April') + + itm = self.ctrl.get_item('\\ Date Elements \\ months \\ april', exact=False) + self.assertEqual(isinstance(itm, wpf_ctls.TreeItemWrapper), True) + self.assertEqual(itm.window_text(), u'April') + + itm = self.ctrl.get_item('\\Date Elements', exact=False) + self.assertEqual(isinstance(itm, wpf_ctls.TreeItemWrapper), True) + self.assertEqual(itm.window_text(), u'Date Elements') + + # Try to find the last item in the tree hierarchy + itm = self.ctrl.get_item('\\Date Elements\\Years\\2018', exact=False) + self.assertEqual(isinstance(itm, wpf_ctls.TreeItemWrapper), True) + self.assertEqual(itm.window_text(), u'2018') + + itm = self.ctrl.get_item((0, 3, 3)) + self.assertEqual(isinstance(itm, wpf_ctls.TreeItemWrapper), True) + self.assertEqual(itm.window_text(), u'2018') + + self.assertRaises(RuntimeError, + self.ctrl.get_item, + 'Date Elements\\months', + exact=False) + + self.assertRaises(IndexError, + self.ctrl.get_item, + '\\_X_- \\months', + exact=False) + + self.assertRaises(IndexError, + self.ctrl.get_item, + '\\_X_- \\ months', + exact=True) + + self.assertRaises(IndexError, + self.ctrl.get_item, + '\\Date Elements\\ months \\ aprel', + exact=False) + + self.assertRaises(IndexError, + self.ctrl.get_item, + '\\Date Elements\\ months \\ april\\', + exact=False) + + self.assertRaises(IndexError, + self.ctrl.get_item, + '\\Date Elements\\ months \\ aprel', + exact=True) + + self.assertRaises(IndexError, self.ctrl.get_item, (0, 200, 1)) + + self.assertRaises(IndexError, self.ctrl.get_item, (130, 2, 1)) + + def test_tv_drag_n_drop(self): + """Test moving an item with mouse over TreeView""" + # Make sure the both nodes are visible + self.ctrl.get_item('\\Date Elements\\weeks').collapse() + itm_from = self.ctrl.get_item('\\Date Elements\\Years') + itm_to = self.ctrl.get_item('\\Date Elements\\Empty Date') + + itm_from.drag_mouse_input(itm_to) + + # Verify that the item and its sub-items are attached to the new node + itm = self.ctrl.get_item('\\Date Elements\\Empty Date\\Years') + self.assertEqual(itm.window_text(), 'Years') + itm = self.ctrl.get_item((0, 0, 0, 0)) + self.assertEqual(itm.window_text(), '2015') + itm = self.ctrl.get_item('\\Date Elements\\Empty Date\\Years') + itm.collapse() + + itm_from = self.ctrl.get_item('\\Date Elements\\Empty Date\\Years') + itm_to = self.ctrl.get_item(r'\Date Elements\Months') + self.ctrl.drag_mouse_input(itm_to, itm_from) + itm = self.ctrl.get_item(r'\Date Elements\Months\Years') + self.assertEqual(itm.window_text(), 'Years') + + # Error handling: drop on itself + self.assertRaises(AttributeError, + self.ctrl.drag_mouse_input, + itm_from, itm_from) + + # Drag-n-drop by manually calculated absolute coordinates + itm_from = self.ctrl.get_item(r'\Date Elements\Months') + itm_from.collapse() + r = itm_from.rectangle() + coords_from = (int(r.left + (r.width() / 4.0)), + int(r.top + (r.height() / 2.0))) + + r = self.ctrl.get_item(r'\Date Elements\Weeks').rectangle() + coords_to = (int(r.left + (r.width() / 4.0)), + int(r.top + (r.height() / 2.0))) + + self.ctrl.drag_mouse_input(coords_to, coords_from) + itm = self.ctrl.get_item(r'\Date Elements\Weeks\Months') + self.assertEqual(itm.window_text(), 'Months') + + +class ListViewWrapperTests(unittest.TestCase): + + """Unit tests for the ListViewWrapper class""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + + # start the application + app = Application(backend='wpf') + app = app.start(wpf_app_1) + dlg = app.WPFSampleApplication + + self.app = app + + self.listbox_datagrid_tab = dlg.ListBox_and_Grid + + self.listbox_texts = [ + [u"TextItem 1", ], + [u"TextItem 2", ], + [u"ButtonItem", ], + [u"CheckItem", ], + [u"TextItem 3", ], + [u"TextItem 4", ], + [u"TextItem 5", ], + [u"TextItem 6", ], + [u"TextItem 7", ], + [u"TextItem 8", ], + ] + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_item_count(self): + """Test the items count in the ListView controls""" + # ListBox + self.listbox_datagrid_tab.set_focus() + listbox = self.listbox_datagrid_tab.descendants(class_name=u"ListBox")[0] + self.assertEqual(listbox.item_count(), len(self.listbox_texts)) + + def test_cells(self): + """Test getting a cells of the ListView controls""" + # ListBox + self.listbox_datagrid_tab.set_focus() + listbox = self.listbox_datagrid_tab.descendants(class_name=u"ListBox")[0] + cells = listbox.cells() + self.assertEqual(cells[listbox.item_count() - 1].window_text(), "TextItem 8") + self.assertEqual(cells[3].window_text(), "CheckItem") + + def test_get_item(self): + """Test getting an item of ListView controls""" + # ListBox + self.listbox_datagrid_tab.set_focus() + listbox = self.listbox_datagrid_tab.descendants(class_name=u"ListBox")[0] + item = listbox.get_item(u"TextItem 2") + self.assertEqual(item.texts(), self.listbox_texts[1]) + + item = listbox.get_item(3) + self.assertEqual(item.texts(), self.listbox_texts[3]) + + item = listbox.get_item(u"TextItem 8") + self.assertEqual(item.texts(), self.listbox_texts[9]) + + def test_get_items(self): + """Test getting all items of ListView controls""" + # ListBox + self.listbox_datagrid_tab.set_focus() + listbox = self.listbox_datagrid_tab.descendants(class_name=u"ListBox")[0] + content = [item.texts() for item in listbox.get_items()] + self.assertEqual(content, self.listbox_texts) + + def test_texts(self): + """Test getting all items of ListView controls""" + # ListBox + self.listbox_datagrid_tab.set_focus() + listbox = self.listbox_datagrid_tab.descendants(class_name=u"ListBox")[0] + self.assertEqual(listbox.texts(), self.listbox_texts) + + def test_select_and_get_item(self): + """Test selecting an item of the ListView control""" + self.listbox_datagrid_tab.set_focus() + self.ctrl = self.listbox_datagrid_tab.descendants(class_name=u"ListBox")[0] + self.assertEqual(self.ctrl.texts(), self.listbox_texts) + # Verify get_selected_count + self.assertEqual(self.ctrl.get_selected_count(), 0) + + # Select by an index + row = 1 + i = self.ctrl.get_item(row) + self.assertEqual(i.is_selected(), False) + i.select() + self.assertEqual(i.is_selected(), True) + cnt = self.ctrl.get_selected_count() + self.assertEqual(cnt, 1) + rect = self.ctrl.get_item_rect(row) + self.assertEqual(rect, i.rectangle()) + + # Select by text + row = 'TextItem 6' + i = self.ctrl.get_item(row) + i.select() + self.assertEqual(i.is_selected(), True) + i = self.ctrl.get_item(7) # re-get the item by a row index + self.assertEqual(i.is_selected(), True) + + row = None + self.assertRaises(TypeError, self.ctrl.get_item, row) + + +class GridListViewWrapperTests(unittest.TestCase): + + """Unit tests for the DataGridWrapper class""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + + # start the application + app = Application(backend='wpf') + app = app.start(wpf_app_1) + dlg = app.WPFSampleApplication + + self.app = app + + self.listview_tab = dlg.Tree_and_List_Views + self.listbox_datagrid_tab = dlg.ListBox_and_Grid + + self.listview_texts = [ + [u"1", u"Tomatoe", u"Red"], + [u"2", u"Cucumber", u"Green", ], + [u"3", u"Reddish", u"Purple", ], + [u"4", u"Cauliflower", u"White", ], + [u"5", u"Cupsicum", u"Yellow", ], + [u"6", u"Cupsicum", u"Red", ], + [u"7", u"Cupsicum", u"Green", ], + ] + + self.datagrid_texts = [ + [u"0", u"A0", u"B0", u"C0", u"D0", u"E0", u"", ], + [u"1", u"A1", u"B1", u"C1", u"D1", u"E1", u"", ], + [u"2", u"A2", u"B2", u"C2", u"D2", u"E2", u"", ], + [u"3", u"A3", u"B3", u"C3", u"D3", u"E3", u"", ], + ] + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_item_count(self): + """Test the items count in grid controls""" + # ListView + self.listview_tab.set_focus() + listview = self.listview_tab.descendants(class_name=u"ListView")[0] + self.assertEqual(listview.item_count(), len(self.listview_texts)) + + # DataGrid + self.listbox_datagrid_tab.set_focus() + datagrid = self.listbox_datagrid_tab.descendants(class_name=u"DataGrid")[0] + self.assertEqual(datagrid.item_count(), len(self.datagrid_texts)) + + def test_column_count(self): + """Test the columns count in grid controls""" + # ListView + self.listview_tab.set_focus() + listview = self.listview_tab.descendants(class_name=u"ListView")[0] + self.assertEqual(listview.column_count(), len(self.listview_texts[0])) + + # DataGrid + self.listbox_datagrid_tab.set_focus() + datagrid = self.listbox_datagrid_tab.descendants(class_name=u"DataGrid")[0] + self.assertEqual(datagrid.column_count(), len(self.datagrid_texts[0]) - 1) + + def test_get_header_control(self): + """Test getting a Header control and Header Item control of grid controls""" + # ListView + self.listview_tab.set_focus() + listview = self.listview_tab.descendants(class_name=u"ListView")[0] + hdr_itm = listview.get_header_controls()[1] + # HeaderItem of ListView + self.assertTrue(isinstance(hdr_itm, wpf_ctls.HeaderItemWrapper)) + self.assertEquals('Name', hdr_itm.element_info.name) + + # DataGrid + self.listbox_datagrid_tab.set_focus() + datagrid = self.listbox_datagrid_tab.descendants(class_name=u"DataGrid")[0] + grid_hdr_item = datagrid.get_header_controls()[2] + self.assertEquals('B', grid_hdr_item.element_info.name) + self.assertTrue(isinstance(grid_hdr_item, wpf_ctls.HeaderItemWrapper)) + + def test_get_column(self): + """Test get_column() method for grid controls""" + # ListView + self.listview_tab.set_focus() + listview = self.listview_tab.descendants(class_name=u"ListView")[0] + listview_col = listview.get_column(1) + self.assertEqual(listview_col.texts()[0], u"Name") + + # DataGrid + self.listbox_datagrid_tab.set_focus() + datagrid = self.listbox_datagrid_tab.descendants(class_name=u"DataGrid")[0] + datagrid_col = datagrid.get_column(2) + self.assertEqual(datagrid_col.texts()[0], u"B") + + self.assertRaises(IndexError, datagrid.get_column, 10) + + def test_cell(self): + """Test getting a cell of grid controls""" + # ListView + self.listview_tab.set_focus() + listview = self.listview_tab.descendants(class_name=u"ListView")[0] + cell = listview.cell(3, 2) + self.assertEqual(cell.window_text(), self.listview_texts[3][2]) + + # DataGrid + self.listbox_datagrid_tab.set_focus() + datagrid = self.listbox_datagrid_tab.descendants(class_name=u"DataGrid")[0] + cell = datagrid.cell(2, 0) + self.assertEqual(cell.window_text(), self.datagrid_texts[2][0]) + + self.assertRaises(TypeError, datagrid.cell, 1.5, 1) + + self.assertRaises(IndexError, datagrid.cell, 10, 10) + + def test_cells(self): + """Test getting a cells of grid controls""" + def compare_cells(cells, control): + for i in range(0, control.item_count()): + for j in range(0, control.column_count()): + self.assertEqual(cells[i][j], control.cell(i, j)) + + # ListView + self.listview_tab.set_focus() + listview = self.listview_tab.descendants(class_name=u"ListView")[0] + compare_cells(listview.cells(), listview) + + # DataGrid + self.listbox_datagrid_tab.set_focus() + datagrid = self.listbox_datagrid_tab.descendants(class_name=u"DataGrid")[0] + compare_cells(datagrid.cells(), datagrid) + + def test_get_item(self): + """Test getting an item of grid controls""" + # ListView + self.listview_tab.set_focus() + listview = self.listview_tab.descendants(class_name=u"ListView")[0] + item = listview.get_item(u"Reddish") + self.assertEqual(item.texts(), self.listview_texts[2]) + + self.assertRaises(ValueError, listview.get_item, u"Apple") + + # DataGrid + self.listbox_datagrid_tab.set_focus() + datagrid = self.listbox_datagrid_tab.descendants(class_name=u"DataGrid")[0] + item = datagrid.get_item(u"B2") + self.assertEqual(item.texts(), self.datagrid_texts[2]) + + item = datagrid.get_item(3) + self.assertEqual(item.texts(), self.datagrid_texts[3]) + + self.assertRaises(TypeError, datagrid.get_item, 12.3) + + def test_get_items(self): + """Test getting all items of grid controls""" + self.listview_tab.set_focus() + listview = self.listview_tab.descendants(class_name=u"ListView")[0] + content = [item.texts() for item in listview.get_items()] + self.assertEqual(content, self.listview_texts) + + # DataGrid + self.listbox_datagrid_tab.set_focus() + datagrid = self.listbox_datagrid_tab.descendants(class_name=u"DataGrid")[0] + content = [item.texts() for item in datagrid.get_items()] + self.assertEqual(content, self.datagrid_texts) + + def test_texts(self): + """Test getting all items of grid controls""" + self.listview_tab.set_focus() + listview = self.listview_tab.descendants(class_name=u"ListView")[0] + self.assertEqual(listview.texts(), self.listview_texts) + + # DataGrid + self.listbox_datagrid_tab.set_focus() + datagrid = self.listbox_datagrid_tab.descendants(class_name=u"DataGrid")[0] + self.assertEqual(datagrid.texts(), self.datagrid_texts) + + def test_select_and_get_item_listview(self): + """Test selecting an item of the ListView control""" + self.listview_tab.set_focus() + self.ctrl = self.listview_tab.descendants(class_name=u"ListView")[0] + # Verify get_selected_count + self.assertEqual(self.ctrl.get_selected_count(), 0) + + # Select by an index + row = 1 + i = self.ctrl.get_item(row) + self.assertEqual(i.is_selected(), False) + i.select() + self.assertEqual(i.is_selected(), True) + cnt = self.ctrl.get_selected_count() + self.assertEqual(cnt, 1) + rect = self.ctrl.get_item_rect(row) + self.assertEqual(rect, i.rectangle()) + + # Select by text + row = '3' + i = self.ctrl.get_item(row) + i.select() + self.assertEqual(i.is_selected(), True) + row = 'White' + i = self.ctrl.get_item(row) + i.select() + i = self.ctrl.get_item(3) # re-get the item by a row index + self.assertEqual(i.is_selected(), True) + + row = None + self.assertRaises(TypeError, self.ctrl.get_item, row) + + def test_select_and_get_item_datagrid(self): + """Test selecting an item of the DataGrid control""" + self.listbox_datagrid_tab.set_focus() + datagrid = self.listbox_datagrid_tab.descendants(class_name=u"DataGrid")[0] + # Verify get_selected_count + self.assertEqual(datagrid.get_selected_count(), 0) + + # Select by an index + row = 1 + i = datagrid.get_item(row) + self.assertEqual(i.is_selected(), False) + i.select() + self.assertEqual(i.is_selected(), True) + cnt = datagrid.get_selected_count() + self.assertEqual(cnt, 1) + rect = datagrid.get_item_rect(row) + self.assertEqual(rect, i.rectangle()) + + # Select by text + row = 'A3' + i = datagrid.get_item(row) + i.select() + self.assertEqual(i.is_selected(), True) + row = 'B0' + i = datagrid.get_item(row) + i.select() + i = datagrid.get_item(3) # re-get the item by a row index + self.assertEqual(i.is_selected(), True) + + row = None + self.assertRaises(TypeError, datagrid.get_item, row) + + +if __name__ == "__main__": + unittest.main() diff --git a/pywinauto/windows/injected b/pywinauto/windows/injected new file mode 160000 index 000000000..4957c5f2d --- /dev/null +++ b/pywinauto/windows/injected @@ -0,0 +1 @@ +Subproject commit 4957c5f2d551615649c02e93b36dacc1dff5a164 diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py new file mode 100644 index 000000000..2373f18c0 --- /dev/null +++ b/pywinauto/windows/wpf_element_info.py @@ -0,0 +1,307 @@ +# GUI Application automation and testing library +# Copyright (C) 2006-2018 Mark Mc Mahon and Contributors +# https://github.com/pywinauto/pywinauto/graphs/contributors +# http://pywinauto.readthedocs.io/en/latest/credits.html +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of pywinauto nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Implementation of the class to deal with an UI element of WPF via injected managed DLL/assembly.""" +from six import integer_types, string_types + +from pywinauto.handleprops import dumpwindow +from pywinauto.element_info import ElementInfo +from .win32structures import RECT +from .injected.api import ConnectionManager, InjectedNotFoundError, InjectedUnsupportedActionError + + +def is_element_satisfying_criteria(element, process=None, class_name=None, name=None, control_type=None, + **kwargs): + """Check if element satisfies filter criteria.""" + is_appropriate_control_type = True + if control_type: + if isinstance(control_type, string_types): + is_appropriate_control_type = element.control_type == control_type + else: + raise TypeError('control_type must be string') + + def is_none_or_equals(criteria, prop): + return criteria is None or prop == criteria + + return is_none_or_equals(process, element.process_id) \ + and is_none_or_equals(class_name, element.class_name) \ + and is_none_or_equals(name, element.name) \ + and is_appropriate_control_type + + +class WPFElementInfo(ElementInfo): + + """Class to get information about WPF UI element in the specified process.""" + + re_props = ["class_name", "name", "auto_id", "control_type", "full_control_type", "value"] + exact_only_props = ["handle", "pid", "control_id", "enabled", "visible", "rectangle", "framework_id", "runtime_id"] + search_order = ["handle", "control_type", "class_name", "pid", "control_id", "visible", "enabled", "name", + "auto_id", "full_control_type", "rectangle", "framework_id", + "runtime_id", "value"] + assert set(re_props + exact_only_props) == set(search_order) + + def __init__(self, elem_id=None, cache_enable=False, pid=None): + """Create an instance of WPFElementInfo from an ID of the element (int or long). + + If elem_id is None create an instance for UI root element. + """ + self._pid = pid + if elem_id is not None: + if isinstance(elem_id, integer_types): + # Create instance of WPFElementInfo from a handle + self._element = elem_id + else: + raise TypeError("WPFElementInfo object can be initialized " + "with integer instance only!") + else: + self._element = 0 + + self.set_cache_strategy(cached=cache_enable) + + def set_cache_strategy(self, cached): + pass + + @property + def handle(self): + """Return handle of the element.""" + if self._element == 0: + return None + reply = ConnectionManager().call_action('GetHandle', self._pid, element_id=self._element) + return reply['value'] + + @property + def auto_id(self): + """Return AutomationId of the element.""" + if self._element == 0: + return '' + return self.get_native_property('Name') or '' + + @property + def name(self): + """Return name of the element.""" + if self._element == 0: + return '--root--' + reply = ConnectionManager().call_action('GetName', self._pid, element_id=self._element) + return reply['value'] + + @property + def rich_text(self): + """Return rich_text of the element.""" + if self.control_type == 'Edit': + return self.get_native_property('Text') or '' + return self.name + + @property + def value(self): + """Return value of the element (in order to search elements by this property). + + Only specified element types (like Edit) are supported. + Use :py:meth:`get_native_property` to get value of the property with the specified name. + """ + if self.control_type == 'Edit': + return self.get_native_property('Text') or self.get_native_property('Password') or '' + return '' + + @property + def control_id(self): + """Return element ID (may be different from run to run).""" + return self._element + + @property + def process_id(self): + """Return process ID of the element.""" + return self._pid + + pid = process_id + + @property + def framework_id(self): + """Return framework ID of the element (always is 'WPF', for compatibility with UIA).""" + return "WPF" + + @property + def runtime_id(self): + """Return element ID (may be different from run to run).""" + return self._element + + @property + def class_name(self): + """Return class name of the element.""" + if self._element == 0: + return '' + reply = ConnectionManager().call_action('GetTypeName', self._pid, element_id=self._element) + return reply['value'] + + @property + def enabled(self): + """Check if the element is enabled.""" + if self._element == 0: + return True + return self.get_native_property('IsEnabled') or False + + @property + def visible(self): + """Check if the element is visible.""" + if self._element == 0: + return True + return self.get_native_property('IsVisible') or False + + @property + def parent(self): + """Return parent of the element.""" + if self._element == 0: + return None + reply = ConnectionManager().call_action('GetParent', self._pid, element_id=self._element) + return WPFElementInfo(reply['value'], pid=self._pid) + + def children(self, **kwargs): + """Return a list of only immediate children of the element. + + * **kwargs** is a criteria to reduce a list by process, + class_name, control_type, content_only and/or title. + """ + return list(self.iter_children(**kwargs)) + + @property + def control_type(self): + """Return control type of element.""" + if self._element == 0: + return None + reply = ConnectionManager().call_action('GetControlType', self._pid, element_id=self._element) + return reply['value'] + + def iter_children(self, **kwargs): + """Return a generator of only immediate children of the element. + + * **kwargs** is a criteria to reduce a list by process, + class_name, control_type, content_only and/or title. + """ + if 'process' in kwargs and self._pid is None: + self._pid = kwargs['process'] + reply = ConnectionManager().call_action('GetChildren', self._pid, element_id=self._element) + for elem_id in reply['elements']: + element = WPFElementInfo(elem_id, pid=self._pid) + if is_element_satisfying_criteria(element, **kwargs): + yield element + + def descendants(self, **kwargs): + """Return a list of all descendant children of the element. + + * **kwargs** is a criteria to reduce a list by process, + class_name, control_type, content_only and/or title. + """ + return list(self.iter_descendants(**kwargs)) + + def iter_descendants(self, **kwargs): + """Iterate over descendants of the element.""" + # TODO implement cache support + # cache_enable = kwargs.pop('cache_enable', False) + depth = kwargs.pop("depth", None) + process = kwargs.pop("process", None) + if not isinstance(depth, (integer_types, type(None))) or isinstance(depth, integer_types) and depth < 0: + raise Exception("Depth must be an integer") + + if depth == 0: + return + for child in self.iter_children(process=process): + if is_element_satisfying_criteria(child, **kwargs): + yield child + if depth is not None: + kwargs["depth"] = depth - 1 + for c in child.iter_descendants(**kwargs): + if is_element_satisfying_criteria(c, **kwargs): + yield c + + @property + def rectangle(self): + """Return rectangle of the element.""" + rect = RECT() + if self._element != 0: + reply = ConnectionManager().call_action('GetRectangle', self._pid, element_id=self._element) + rect.left = reply['value']['left'] + rect.right = reply['value']['right'] + rect.top = reply['value']['top'] + rect.bottom = reply['value']['bottom'] + return rect + + def dump_window(self): + """Dump window to a set of properties.""" + return dumpwindow(self.handle) + + def get_native_property(self, name, error_if_not_exists=False): + """Return value of the specified property of the .NET object corresponding to the element.""" + try: + reply = ConnectionManager().call_action('GetProperty', self._pid, element_id=self._element, name=name) + return reply['value'] + except InjectedNotFoundError as e: + if error_if_not_exists: + raise e + return None + + def get_native_properties(self): + """Return a dict with names and types of available properties of the .NET object related to the element.""" + reply = ConnectionManager().call_action('GetProperties', self._pid, element_id=self._element) + return reply['value'] + + def __hash__(self): + """Return a unique hash value based on the element's ID.""" + return hash(self._element) + + def __eq__(self, other): + """Check if 2 WPFElementInfo objects describe 1 actual element.""" + if not isinstance(other, WPFElementInfo): + return False + return self._element == other._element + + def __ne__(self, other): + """Check if 2 WPFElementInfo objects describe 2 different elements.""" + return not (self == other) + + @classmethod + def get_active(cls, app_or_pid): + """Return current active element.""" + from .application import Application + + if isinstance(app_or_pid, integer_types): + pid = app_or_pid + elif isinstance(app_or_pid, Application): + pid = app_or_pid.process + else: + raise TypeError("WPFElementInfo object can be initialized " + "with integer or Application instance only!") + + try: + reply = ConnectionManager().call_action('GetFocusedElement', pid) + if reply['value'] > 0: + return cls(reply['value'], pid=pid) + return None + except InjectedUnsupportedActionError: + return None