Commit f5a466b7 authored by anonym's avatar anonym
Browse files

Redesign the Dogtail class hierarchy.

The ScriptProxy part was a remnant from the time when I was still
concidering supporting Dogtail's procedural API, but it's now clear
that it will be too hard and not offer anything beyond the Tree
API. Also, the `interact` + block idea was from this time, and also
from a part of rewritten Git history where each use of the Dogtail
class instance inside the? `interact` block built up a single script,
that then was executed. However, since you can mix-in other calls
(e.g. to Sikuli) that may expect that a change already has happened,
that seemed like a bad approach.

So, let's KISS and simply settle on the classes Application and Node,
roughly reflecting those classes in the Tree API. The diff might look
scary, but it just moves code around, and removes some confusing
parts.
parent 5d19b080
......@@ -639,10 +639,9 @@ Then /^persistence for "([^"]+)" is (|not )enabled$/ do |app, enabled|
end
Given /^I start "([^"]+)" via the GNOME "([^"]+)" applications menu$/ do |app_name, submenu|
Dogtail::Application.interact('gnome-shell') do |app|
for element in ['Applications', submenu, app_name] do
app.child(element, roleName: 'label').click
end
app = Dogtail::Application.new('gnome-shell')
for element in ['Applications', submenu, app_name] do
app.child(element, roleName: 'label').click
end
end
......
......@@ -58,38 +58,33 @@ def maybe_deal_with_pinentry
end
def gedit_copy_all_text
@gedit.interact do |app|
app.child(roleName: 'text').click(button: Dogtail::Mouse::RIGHT_CLICK)
app.menuItem('Select All').click
end
@gedit.child(roleName: 'text').click(button: Dogtail::Mouse::RIGHT_CLICK)
@gedit.menuItem('Select All').click
end
def gedit_paste_into_a_new_tab
@gedit.interact do |app|
app.button('New').click()
app.child(roleName: 'text').click(button: Dogtail::Mouse::RIGHT_CLICK)
app.menuItem('Paste').click
end
@gedit.button('New').click()
@gedit.child(roleName: 'text').click(button: Dogtail::Mouse::RIGHT_CLICK)
@gedit.menuItem('Paste').click
end
def encrypt_sign_helper(encrypt, sign)
gedit_copy_all_text
seahorse_menu_click_helper('GpgAppletIconNormal.png', 'GpgAppletSignEncrypt.png')
Dogtail::Application.interact('gpgApplet') do |app|
dialog = app.dialog('Choose keys')
if encrypt
dialog.child(roleName: "table").child(@gpgApplet_key_desc).doubleClick
end
if sign
combobox = dialog.child(roleName: 'combo box')
combobox.click
combobox.child(@gpgApplet_key_desc, roleName: 'menu item').click
# Often the cursor stays hovering over an element that opens a
# pop-up blocking the OK button.
@screen.hide_cursor
end
dialog.button('OK').click
gpgApplet = Dogtail::Application.new('gpgApplet')
dialog = gpgApplet.dialog('Choose keys')
if encrypt
dialog.child(roleName: "table").child(@gpgApplet_key_desc).doubleClick
end
if sign
combobox = dialog.child(roleName: 'combo box')
combobox.click
combobox.child(@gpgApplet_key_desc, roleName: 'menu item').click
# Often the cursor stays hovering over an element that opens a
# pop-up blocking the OK button.
@screen.hide_cursor
end
dialog.button('OK').click
maybe_deal_with_pinentry
gedit_paste_into_a_new_tab
end
......@@ -103,23 +98,22 @@ def decrypt_verify_helper(encrypted, signed)
end
seahorse_menu_click_helper(icon, 'GpgAppletDecryptVerify.png')
maybe_deal_with_pinentry
Dogtail::Application.interact('gpgApplet') do |app|
dialog = app.child('Information', roleName: 'alert')
stdout_text_area, stderr_text_area = app.children(roleName: 'text')
# Given some inconsistency in either gpg or gpgApplet, we can get
# either one or two trailing newlines here.
stdout = stdout_text_area.get_field('text').chomp.chomp
assert_equal(@message, stdout,
"The expected message could not be found in the GnuPG output")
stderr = stderr_text_area.get_field('text').chomp.chomp
if encrypted
assert(stderr['gpg: encrypted with '], 'Message was not encrypted')
end
if signed
assert(stderr['gpg: Good signature from '], 'Message was not signed')
end
dialog.button('OK').click
gpgApplet = Dogtail::Application.new('gpgApplet')
dialog = gpgApplet.child('Information', roleName: 'alert')
stdout_text_area, stderr_text_area = dialog.children(roleName: 'text')
# Given some inconsistency in either gpg or gpgApplet, we can get
# either one or two trailing newlines here.
stdout = stdout_text_area.get_field('text').chomp.chomp
assert_equal(@message, stdout,
"The expected message could not be found in the GnuPG output")
stderr = stderr_text_area.get_field('text').chomp.chomp
if encrypted
assert(stderr['gpg: encrypted with '], 'Message was not encrypted')
end
if signed
assert(stderr['gpg: Good signature from '], 'Message was not signed')
end
dialog.button('OK').click
end
When /^I encrypt the message using my OpenPGP key$/ do
......
......@@ -5,16 +5,41 @@ module Dogtail
RIGHT_CLICK = 3
end
TREE_API_NODE_SEARCHES = [
:button,
:child,
:childLabelled,
:childNamed,
:menu,
:menuItem,
:tab,
:text,
:textentry,
]
TREE_API_NODE_ACTIONS = [
:click,
:doubleClick,
:grabFocus,
:keyCombo,
:point,
:typeText,
]
TREE_API_APP_SEARCHES = TREE_API_NODE_SEARCHES + [
:dialog,
:window,
]
# We want to keep this class immutable so that handles always are
# left intact when doing new (proxied) method calls. This was we
# can support stuff like:
#
# Dogtail::Application.interact('gedit') do |app|
# menu = app.menu('Menu')
# menu.click()
# menu.something_else()
# menu.click()
# end
# app = Dogtail::Application.new('gedit')
# menu = app.menu('Menu')
# menu.click()
# menu.something_else()
# menu.click()
#
# i.e. the object referenced by `menu` is never modified by method
# calls and can be used as expected. This explains why
......@@ -22,19 +47,23 @@ module Dogtail
# appending the new component the proxied method call would result
# in.
class ScriptProxy
class Application
def initialize(init_lines = [], init_components = [], opts = {})
@module_import_lines = []
@init_lines = init_lines
def initialize(app_name, opts = {})
@app_name = app_name
@opts = opts
@components = init_components
@init_lines = @opts[:init_lines] || [
"from dogtail import tree",
"from dogtail.config import config",
"config.searchShowingOnly = True",
"application = tree.root.application('#{@app_name}')",
]
@components = @opts[:components] || ['application']
end
def build_script(lines)
(
["#!/usr/bin/python"] +
@module_import_lines +
@init_lines +
lines
).join("\n")
......@@ -73,7 +102,7 @@ module Dogtail
elsif v == false
'False'
elsif v.class == String
"\"#{v}\""
"'#{v}'"
elsif [Fixnum, Float].include?(v.class)
v.to_s
else
......@@ -86,66 +115,6 @@ module Dogtail
).join(', ')
end
def proxy_call(method, args)
args_str = self.class.args_to_s(args)
component = "#{method.to_s}(#{args_str})"
final_components = @components + [component]
self.class.new(@init_lines, final_components, @opts)
end
def get_field(key)
run("print(#{build_line}.#{key})").stdout
end
end
TREE_API_NODE_ACTIONS = [
:click,
:doubleClick,
:grabFocus,
:keyCombo,
:point,
:typeText,
]
TREE_API_NODE_SEARCHES = [
:button,
:child,
:childLabelled,
:childNamed,
:dialog,
:menu,
:menuItem,
:tab,
:text,
:textentry,
:window,
]
TREE_API_NODE_METHODS = (TREE_API_NODE_ACTIONS + TREE_API_NODE_SEARCHES)
class TreeAPIScriptProxy < ScriptProxy
def initialize(*args)
super(*args)
@module_import_lines += [
'from dogtail import tree',
'from dogtail.config import config',
]
@module_import_lines.uniq!
@init_lines = ['config.searchShowingOnly = True'] + @init_lines
@init_lines.uniq!
end
TREE_API_NODE_METHODS.each do |method|
define_method(method) do |*args|
proxy = proxy_call(method, args)
# If it's not an action we are calling, we just want a
# "reference" for use later, so we just return ScriptProxy
# object representing the current state of the call.
TREE_API_NODE_ACTIONS.include?(method) ? proxy.run : proxy
end
end
def wait(timeout = nil)
if timeout
try_for(timeout) { run }
......@@ -162,7 +131,7 @@ module Dogtail
# have to port looping, conditionals and much more into our
# script generation, which is insane.
# However, since references are lost between script runs (=
# ScriptProxy.run()) we need to be a bit tricky here. We use the
# Application.run()) we need to be a bit tricky here. We use the
# internal a11y AT-SPI "path" to uniquely identify a Dogtail
# node, so we can give handles to each of them that can be used
# later to re-find them.
......@@ -180,49 +149,53 @@ module Dogtail
" if str(n.path) == '#{path}':",
" node = n",
" break",
"assert(node)",
]
self.class.new(@init_lines + more_init_lines, ['node'], @opts)
Node.new(
@app_name,
@opts.merge(
init_lines: @init_lines + more_init_lines,
components: ['node']
)
)
end
end
end
LOCAL_TREE_API_NODE_METHODS = [
:wait,
:get_field,
]
ALL_TREE_API_NODE_METHODS = TREE_API_NODE_METHODS + LOCAL_TREE_API_NODE_METHODS
class Application
def initialize(app)
@app = app
def get_field(key)
run("print(#{build_line}.#{key})").stdout
end
def self.init_lines(app)
["application = tree.root.application(\"#{app}\")"]
def proxy_call(method, args)
args_str = self.class.args_to_s(args)
method_call = "#{method.to_s}(#{args_str})"
Node.new(
@app_name,
@opts.merge(
init_lines: @init_lines,
components: @components + [method_call]
)
)
end
def self.init_components
['application']
TREE_API_APP_SEARCHES.each do |method|
define_method(method) do |*args|
proxy_call(method, args)
end
end
def self.interact(app, opts = {}, &block)
yield TreeAPIScriptProxy.new(self.init_lines(app),
self.init_components,
opts)
end
end
class Node < Application
def interact(*args, &block)
self.class.interact(@app, *args, &block)
TREE_API_NODE_SEARCHES.each do |method|
define_method(method) do |*args|
proxy_call(method, args)
end
end
ALL_TREE_API_NODE_METHODS.each do |method|
TREE_API_NODE_ACTIONS.each do |method|
define_method(method) do |*args|
interact do |app|
app.method(method).call(*args)
end
proxy_call(method, args).run
end
end
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment