nobbie-wx-preview 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/KNOWN ISSUES +27 -0
- data/README +149 -0
- data/lib/nobbie/wx/acceptance_test.rb +19 -0
- data/lib/nobbie/wx/command.rb +63 -0
- data/lib/nobbie/wx/command/choose.rb +36 -0
- data/lib/nobbie/wx/command/click_on.rb +33 -0
- data/lib/nobbie/wx/command/get_component.rb +17 -0
- data/lib/nobbie/wx/command/get_selected_values.rb +25 -0
- data/lib/nobbie/wx/command/is_chosen.rb +21 -0
- data/lib/nobbie/wx/command/is_enabled.rb +18 -0
- data/lib/nobbie/wx/command/select.rb +89 -0
- data/lib/nobbie/wx/command/type_into.rb +79 -0
- data/lib/nobbie/wx/command_executor.rb +25 -0
- data/lib/nobbie/wx/command_factory.rb +48 -0
- data/lib/nobbie/wx/driven.rb +7 -0
- data/lib/nobbie/wx/impl/element/element_path_builder.rb +41 -0
- data/lib/nobbie/wx/impl/operation/choosable.rb +31 -0
- data/lib/nobbie/wx/impl/operation/select.rb +31 -0
- data/lib/nobbie/wx/launcher.rb +75 -0
- data/lib/nobbie/wx/operations.rb +85 -0
- data/lib/nobbie/wx/platform.rb +31 -0
- data/test/all_tests.rb +6 -0
- data/test/suite/app.rb +144 -0
- data/test/suite/nobbie_test_case.rb +46 -0
- data/test/suite/test_choose.rb +52 -0
- data/test/suite/test_click.rb +28 -0
- data/test/suite/test_enabled.rb +13 -0
- data/test/suite/test_launcher.rb +22 -0
- data/test/suite/test_operations.rb +27 -0
- data/test/suite/test_selection.rb +107 -0
- data/test/suite/test_type.rb +59 -0
- metadata +102 -0
data/KNOWN ISSUES
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
= KNOWN ISSUES
|
2
|
+
|
3
|
+
On Windows there seems to be a strange delay between launching the application and it yielding control to the test
|
4
|
+
runner. It seems that the smaller the application, the worse the problem is (i.e. the longer it takes).
|
5
|
+
|
6
|
+
When typing into a TextCtrl, if the value being typed is the same as the current value it seems that on OSX the
|
7
|
+
text changed event is not fired, but on Windows it is. I'm not sure which is correct, but the result should be
|
8
|
+
consistent - so this looks like a WxRuby issue.
|
9
|
+
|
10
|
+
When typing into a readonly ComboBox, on Windows the value changes which is not consistent with the WxRuby
|
11
|
+
documentation for 'set_value'.
|
12
|
+
|
13
|
+
There doesn't seem to be API in Wx for determining if a ComboBox is readonly, so Nobbie's typing depends on a correct
|
14
|
+
implementation of set_value. See previous issue.
|
15
|
+
|
16
|
+
OSX seems to have some problems with regard to the UI repainting and the timing/order of events. Further
|
17
|
+
investigation is required but there are two problems highlighted in the tests. (1) when a value is set for a TextCtrl
|
18
|
+
during initialisation it is not painted to the screen untill you focus on it. (2) when typing into a ComboBox the
|
19
|
+
events value seems to be behind what is actually typed, e.g. if you type 'ab', two events are raised, but the values
|
20
|
+
are '' and 'a'.
|
21
|
+
|
22
|
+
Element highlighting seems to be a bit too quick in Windows.
|
23
|
+
|
24
|
+
Currently there is no way to interact with the UI if an operation results in a blocking dialog. For example,
|
25
|
+
if 'File -> New' results in a FileDialog being displayed, test execution stops until the blocking dialog
|
26
|
+
has been closed manually. This is annoying and I welcome suggestions of how to overcome this problem, in a platform
|
27
|
+
neutral way.
|
data/README
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
= Nobbie-Wx -- An implementation of the Nobbie GUI Driving API for WxRuby2
|
2
|
+
|
3
|
+
Nobbie is a simple generic api for driving GUI's (both web and rich-client interfaces). This
|
4
|
+
particular implementation is for driving WxRuby2 applications. There are other implementations available for
|
5
|
+
driving Java Swing and Web Applications - but they all use the same generic API.
|
6
|
+
|
7
|
+
Nobbie attempts to make the driving of UI's as simple as possible. Whereas most UI drivers tend to give you multiple
|
8
|
+
ways to find components and subsequently multiple ways to interact with them, Nobbie's philosophy is:
|
9
|
+
|
10
|
+
* finding components by name is the simplest, least brittle approach.
|
11
|
+
* *most* UI interactions boil down to: *typing*, *choosing* and *clicking*
|
12
|
+
|
13
|
+
(Note: if you can't sleep without the ability to find things using xpath or whatever, there are hooks to specify your
|
14
|
+
own component finding strategies).
|
15
|
+
|
16
|
+
With this in mind, Nobbie provides a simple method of {finding components}[link:classes/Nobbie/Wx/ElementPathBuilder.html]
|
17
|
+
and a simple set of {Operations}[link:classes/Nobbie/Wx/Operations.html] to perform on them.
|
18
|
+
|
19
|
+
A quick overview with examples:
|
20
|
+
|
21
|
+
* Paths ...
|
22
|
+
|
23
|
+
In order to perform operations[link:classes/Nobbie/Wx/Operations.html] on a component, you need to first find it.
|
24
|
+
Only very simple 'named' paths are supported by default (but you can implement your own by providing an object that
|
25
|
+
responds to 'find_component'). When specifying a path, if you do not provide an object that responds to
|
26
|
+
'find_component', Nobbie will attempt to coerce the path into the default path type: Nobbie::Wx::ElementPathBuilder.
|
27
|
+
|
28
|
+
That may sound complicated but its really just sugar. The following paths will all find the component
|
29
|
+
named 'text_ctrl':
|
30
|
+
|
31
|
+
'text_ctrl'
|
32
|
+
:in => 'text_ctrl'
|
33
|
+
in_('text_ctrl')
|
34
|
+
Nobbie::Wx::ElementPathBuilder.new('text_ctrl')
|
35
|
+
:text_ctrl
|
36
|
+
|
37
|
+
|
38
|
+
* Typing[link:classes/Nobbie/Wx/Operations.html#type] ...
|
39
|
+
|
40
|
+
type(value, path)
|
41
|
+
|
42
|
+
e.g.
|
43
|
+
|
44
|
+
type('fred', in_('first_name'))
|
45
|
+
|
46
|
+
...or if you're one of those DSL/anglification people
|
47
|
+
|
48
|
+
type 'fred', :in => 'first_name'
|
49
|
+
type 'fred', 'first_name'
|
50
|
+
type 'fred', :first_name
|
51
|
+
|
52
|
+
This will find the component named 'first_name' and type 'fred' into it.
|
53
|
+
...this works for anything you can type into, see:
|
54
|
+
{supported components}[link:classes/Nobbie/Wx/Operations.html#type].
|
55
|
+
|
56
|
+
|
57
|
+
* Selecting[link:classes/Nobbie/Wx/Operations.html#selection] ...
|
58
|
+
|
59
|
+
selection(path).choose(value)
|
60
|
+
|
61
|
+
e.g.
|
62
|
+
|
63
|
+
selection(in_('title')).choose('Mr')
|
64
|
+
|
65
|
+
...or
|
66
|
+
|
67
|
+
selection(:in => 'title').choose 'Mr'
|
68
|
+
selection('title').choose 'Mr'
|
69
|
+
selection(:title).choose 'Mr'
|
70
|
+
|
71
|
+
This will find the component named 'title' and select 'Mr'.
|
72
|
+
...this works for anything where you can make a selection from a number of options
|
73
|
+
(which is quite a lot of things), see:
|
74
|
+
{supported components}[link:classes/Nobbie/Wx/SelectOperations.html#selection].
|
75
|
+
|
76
|
+
You can get the value of the current selection using:
|
77
|
+
|
78
|
+
selection(path).selected_value
|
79
|
+
|
80
|
+
|
81
|
+
* Clicking[link:classes/Nobbie/Wx/Operations.html#click] ...
|
82
|
+
|
83
|
+
click(path) (for buttons you can also use the label instead of a path)
|
84
|
+
|
85
|
+
e.g.
|
86
|
+
|
87
|
+
click('save')
|
88
|
+
|
89
|
+
...or
|
90
|
+
|
91
|
+
click 'save'
|
92
|
+
click :save
|
93
|
+
etc
|
94
|
+
|
95
|
+
This will find the component named (or labelled) 'save' and click it.
|
96
|
+
...this works for anything you can click, see:
|
97
|
+
{supported components}[link:classes/Nobbie/Wx/Operations.html#click].
|
98
|
+
|
99
|
+
|
100
|
+
* Choosing[link:classes/Nobbie/Wx/Operations.html#choosable] ...
|
101
|
+
|
102
|
+
choosable(path).choose
|
103
|
+
|
104
|
+
e.g.
|
105
|
+
|
106
|
+
choosable(in_('female')).choose
|
107
|
+
|
108
|
+
...or
|
109
|
+
|
110
|
+
choosable(:in => 'female').choose
|
111
|
+
choosable('female').choose
|
112
|
+
choosable(:female).choose
|
113
|
+
|
114
|
+
This will find the component named 'female' and choose it
|
115
|
+
...this works for anything that has a chosen/non-chosen state, see:
|
116
|
+
{supported components}[link:classes/Nobbie/Wx/ChoosableOperations.html#choose].
|
117
|
+
|
118
|
+
You can determine if a component is currently chosen using:
|
119
|
+
|
120
|
+
choosable(path).chosen?
|
121
|
+
|
122
|
+
Hopefully that makes some degree of sense, there are a few other operations available, that should be self
|
123
|
+
explanatory from the documentation[link:classes/Nobbie/Wx/Operations.html].
|
124
|
+
|
125
|
+
* Writing that first test ...
|
126
|
+
|
127
|
+
By default Nobbie hooks into test/unit. However, due to a Wx limitation its not possible to launch a new
|
128
|
+
instance of your application for every test run (this would also be very slow anyway). So, instead Nobbie
|
129
|
+
will launch your application, run all the tests in your suite and then close down the application. (Note: If
|
130
|
+
anyone has a better solution to this ... please let me know).
|
131
|
+
|
132
|
+
In order for this to happen, you must advise Nobbie which application you wish to test by setting the constant
|
133
|
+
APPLICATION_UNDER_TEST to an instance of your application, i.e.:
|
134
|
+
|
135
|
+
require 'rubygems'
|
136
|
+
require_gem 'nobbie-wx-preview'
|
137
|
+
|
138
|
+
require 'your_application'
|
139
|
+
require 'your_test_suite'
|
140
|
+
|
141
|
+
APPLICATION_UNDER_TEST = YourApp.new
|
142
|
+
|
143
|
+
|
144
|
+
* More examples ...
|
145
|
+
|
146
|
+
Nobbie has a suite of tests which are installed as part of the gem. Take a look at the 'test' directory.
|
147
|
+
|
148
|
+
|
149
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'nobbie/wx/operations'
|
3
|
+
|
4
|
+
def require_all_in_directory(dir)
|
5
|
+
Dir.glob("#{dir}/**/*.rb") {|f| require "#{f}" }
|
6
|
+
end
|
7
|
+
|
8
|
+
class Test::Unit::TestCase
|
9
|
+
include Nobbie::Wx::Operations
|
10
|
+
end
|
11
|
+
|
12
|
+
Test::Unit.run = false
|
13
|
+
|
14
|
+
#todo: how the hell does this work(/not work as expected) on OSX?
|
15
|
+
at_exit do
|
16
|
+
unless $! || Test::Unit.run?
|
17
|
+
exit = Nobbie::Wx::ApplicationLauncher.new.with_application { Thread.pass }
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Nobbie
|
2
|
+
module Wx
|
3
|
+
module Command
|
4
|
+
|
5
|
+
class ComponentAwareCommand #:nodoc:
|
6
|
+
def initialize(path)
|
7
|
+
@path = path
|
8
|
+
end
|
9
|
+
|
10
|
+
def component
|
11
|
+
@component ||= @path.find_component
|
12
|
+
end
|
13
|
+
|
14
|
+
def handle_unsupported_operation_for_component
|
15
|
+
Kernel.raise(UnsupportedOperationForComponentException,
|
16
|
+
"cannot: #{describe} because component #{component.class} does not support it")
|
17
|
+
end
|
18
|
+
|
19
|
+
def handle_value_not_found
|
20
|
+
Kernel.raise(ValueNotFoundException,
|
21
|
+
"cannot: #{describe} because value #{@value} not found")
|
22
|
+
end
|
23
|
+
|
24
|
+
def ensure_enabled(id = nil)
|
25
|
+
#is_enabled takes an id for menu's
|
26
|
+
enabled = id.nil? ? component.is_enabled : component.is_enabled(id)
|
27
|
+
|
28
|
+
Kernel.raise(ComponentDisabledException,
|
29
|
+
"cannot: #{describe} because component is disabled") unless enabled
|
30
|
+
end
|
31
|
+
|
32
|
+
def highlight(component = component)
|
33
|
+
Kernel.raise "highlight requires a block" unless block_given?
|
34
|
+
|
35
|
+
begin
|
36
|
+
unless [Menu, Panel].include?(component.class)
|
37
|
+
#puts "highlight on: #{component.class} - #{component.name}"
|
38
|
+
original_colour = component.background_colour
|
39
|
+
component.background_colour = Colour.from_hex('#FFFF00')
|
40
|
+
|
41
|
+
#todo: these were previously disabled
|
42
|
+
#component.refresh
|
43
|
+
component.update
|
44
|
+
end
|
45
|
+
result = yield component
|
46
|
+
unless component.is_a?(Menu)
|
47
|
+
component.update
|
48
|
+
end
|
49
|
+
return result
|
50
|
+
ensure
|
51
|
+
unless [Menu, Panel].include?(component.class)
|
52
|
+
#puts "highlight off: #{component.class} - #{component.name}"
|
53
|
+
component.background_colour = original_colour
|
54
|
+
component.refresh
|
55
|
+
#component.update
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Nobbie
|
2
|
+
module Wx
|
3
|
+
module Command
|
4
|
+
|
5
|
+
class ChooseCommand < ComponentAwareCommand #:nodoc:
|
6
|
+
def execute
|
7
|
+
highlight {
|
8
|
+
ensure_enabled
|
9
|
+
|
10
|
+
if component.is_a?(RadioButton) || component.is_a?(CheckBox)
|
11
|
+
handle_radio_button_or_check_box
|
12
|
+
else
|
13
|
+
handle_unsupported_operation_for_component
|
14
|
+
end
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def describe
|
19
|
+
"Choose #{@path}"
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def handle_radio_button_or_check_box
|
25
|
+
event_type = (component.is_a?(RadioButton) ? EVT_COMMAND_RADIOBUTTON_SELECTED : EVT_COMMAND_CHECKBOX_CLICKED)
|
26
|
+
|
27
|
+
event = CommandEvent.new(event_type, component.get_id)
|
28
|
+
event.int = 1 #no idea why this works .. but it is needed
|
29
|
+
event.event_object = component
|
30
|
+
component.command(event)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Nobbie
|
2
|
+
module Wx
|
3
|
+
module Command
|
4
|
+
|
5
|
+
class ClickOnCommand < ComponentAwareCommand #:nodoc:
|
6
|
+
def execute
|
7
|
+
highlight {
|
8
|
+
ensure_enabled
|
9
|
+
|
10
|
+
if component.is_a?(Button)
|
11
|
+
handle_button
|
12
|
+
else
|
13
|
+
handle_unsupported_operation_for_component
|
14
|
+
end
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def describe
|
19
|
+
"Click on #{@path}"
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def handle_button
|
25
|
+
event = CommandEvent.new(EVT_COMMAND_BUTTON_CLICKED, component.get_id)
|
26
|
+
event.event_object = component
|
27
|
+
component.command(event)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Nobbie
|
2
|
+
module Wx
|
3
|
+
module Command
|
4
|
+
|
5
|
+
class GetSelectedValuesCommand < ComponentAwareCommand #:nodoc:
|
6
|
+
def execute
|
7
|
+
if component.is_a?(Notebook)
|
8
|
+
component.page(component.get_selection).name
|
9
|
+
elsif component.is_a?(ComboBox)
|
10
|
+
component.value
|
11
|
+
elsif component.is_a?(ListBox) || component.is_a?(Choice)
|
12
|
+
component.string_selection
|
13
|
+
else
|
14
|
+
handle_unsupported_operation_for_component
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def describe
|
19
|
+
"Get selected values #{@path}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Nobbie
|
2
|
+
module Wx
|
3
|
+
module Command
|
4
|
+
|
5
|
+
class IsChosenCommand < ComponentAwareCommand #:nodoc:
|
6
|
+
def execute
|
7
|
+
if component.is_a?(RadioButton) || component.is_a?(CheckBox)
|
8
|
+
return component.value
|
9
|
+
else
|
10
|
+
handle_unsupported_operation_for_component
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def describe
|
15
|
+
"Is chosen #{@path}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Nobbie
|
2
|
+
module Wx
|
3
|
+
module Command
|
4
|
+
|
5
|
+
class IsEnabledCommand < ComponentAwareCommand #:nodoc:
|
6
|
+
def execute
|
7
|
+
#todo: are there any components that do not respond to is_enabled?
|
8
|
+
component.is_enabled
|
9
|
+
end
|
10
|
+
|
11
|
+
def describe
|
12
|
+
"Is enabled #{@path}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Nobbie
|
2
|
+
module Wx
|
3
|
+
module Command
|
4
|
+
|
5
|
+
class SelectCommand < ComponentAwareCommand #:nodoc:
|
6
|
+
def initialize(path, value)
|
7
|
+
super(path)
|
8
|
+
@value = value
|
9
|
+
end
|
10
|
+
|
11
|
+
def execute
|
12
|
+
if component.is_a?(Menu)
|
13
|
+
return handle_menu
|
14
|
+
end
|
15
|
+
|
16
|
+
ensure_enabled
|
17
|
+
|
18
|
+
if component.is_a?(Notebook)
|
19
|
+
handle_notebook
|
20
|
+
elsif component.is_a?(ComboBox)
|
21
|
+
handle_combo_box
|
22
|
+
elsif component.is_a?(ListBox) || component.is_a?(Choice)
|
23
|
+
handle_list_box_or_choice
|
24
|
+
else
|
25
|
+
handle_unsupported_operation_for_component
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def describe
|
30
|
+
"Select #{@value} in #{@path}"
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def handle_menu
|
36
|
+
id = component.find_item(@value)
|
37
|
+
handle_value_not_found unless id > -1
|
38
|
+
ensure_enabled(id)
|
39
|
+
|
40
|
+
#todo: should this be a MenuEvent?
|
41
|
+
#todo: find a way to avoid using APPLICATION_UNDER_TEST
|
42
|
+
APPLICATION_UNDER_TEST.get_top_window.process_event(CommandEvent.new(EVT_COMMAND_MENU_SELECTED, id))
|
43
|
+
|
44
|
+
return ''
|
45
|
+
end
|
46
|
+
|
47
|
+
def handle_notebook
|
48
|
+
component.page_count.times {|i|
|
49
|
+
if component.page(i).name == @value
|
50
|
+
highlight(component.page(i)) {
|
51
|
+
component.selection = i
|
52
|
+
}
|
53
|
+
return ''
|
54
|
+
end
|
55
|
+
}
|
56
|
+
|
57
|
+
handle_value_not_found
|
58
|
+
end
|
59
|
+
|
60
|
+
#todo: remove duplication for event raising
|
61
|
+
def handle_combo_box
|
62
|
+
index = component.find_string(@value)
|
63
|
+
handle_value_not_found unless index > -1
|
64
|
+
|
65
|
+
event = CommandEvent.new(EVT_COMMAND_COMBOBOX_SELECTED, component.get_id)
|
66
|
+
component.selection = index
|
67
|
+
event.event_object = component
|
68
|
+
component.command(event)
|
69
|
+
end
|
70
|
+
|
71
|
+
#todo: remove duplication for event raising
|
72
|
+
def handle_list_box_or_choice
|
73
|
+
index = component.find_string(@value)
|
74
|
+
handle_value_not_found unless index > -1
|
75
|
+
|
76
|
+
event_type = (component.is_a?(ListBox) ? EVT_COMMAND_LISTBOX_SELECTED : EVT_COMMAND_CHOICE_SELECTED)
|
77
|
+
|
78
|
+
event = CommandEvent.new(event_type, component.get_id)
|
79
|
+
event.int = 1 #no idea why this works .. but it is needed
|
80
|
+
event.string = @value
|
81
|
+
component.selection = index
|
82
|
+
event.event_object = component
|
83
|
+
component.command(event)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|