win_gui 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/HISTORY CHANGED
@@ -5,3 +5,7 @@
5
5
  == 0.2.0 / 2010-05-15
6
6
 
7
7
  * Window class extracted into a separate lib
8
+
9
+ == 0.2.1 / 2010-05-31
10
+
11
+ * Complex convenience methods (dialog
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.0
1
+ 0.2.1
@@ -0,0 +1,67 @@
1
+ require 'win/gui'
2
+
3
+ module WinGui
4
+ include Win::Gui
5
+ # extend Win::Gui
6
+
7
+ # Delay between key commands/events (in sec)
8
+ KEY_DELAY = 0.00001
9
+ # Delay when sleeping (in sec)
10
+ SLEEP_DELAY = 0.001
11
+ # Timeout waiting for Window to be closed (in sec)
12
+ CLOSE_TIMEOUT = 1
13
+ # Default timeout for dialog operations (in sec)
14
+ LOOKUP_TIMEOUT = 3
15
+ DIALOG_WINDOW_CLASS = '#32770'
16
+
17
+ # Module defines convenience methods on top of straightforward Win32 API functions.
18
+
19
+ # Finds top-level dialog window by title and yields found dialog window to attached block.
20
+ # We work with dialog window in a block, and then we wait for it to close before proceeding.
21
+ # That is, unless your block returns nil, in which case dialog is ignored and method immediately returns nil
22
+ # If no block is given, method just returns found dialog window or nil
23
+ def dialog(title, timeout=LOOKUP_TIMEOUT)
24
+ dialog = Window.top_level(class: DIALOG_WINDOW_CLASS, title: title, timeout: timeout)
25
+ #set_foreground_window dialog.handle if dialog # TODO: Should be converted to d_w.s_f_g call!
26
+ wait = block_given? ? yield(dialog) : false
27
+ dialog.wait_for_close if dialog && wait
28
+ dialog
29
+ end
30
+
31
+ # Emulates combinations of (any amount of) keys pressed one after another (Ctrl+Alt+P) and then released
32
+ # *keys should be a sequence of a virtual-key codes. The codes must be a value in the range 1 to 254.
33
+ # For a complete list, see msdn:Virtual Key Codes.
34
+ def keystroke(*keys)
35
+ return if keys.empty?
36
+ keybd_event keys.first, 0, KEYEVENTF_KEYDOWN, 0
37
+ sleep KEY_DELAY
38
+ keystroke *keys[1..-1]
39
+ sleep KEY_DELAY
40
+ keybd_event keys.first, 0, KEYEVENTF_KEYUP, 0
41
+ end
42
+
43
+ # types text message into window holding the focus
44
+ def type_in(message)
45
+ message.scan(/./m) do |char|
46
+ keystroke(*char.to_vkeys)
47
+ end
48
+ end
49
+
50
+
51
+ # DialogWndClass = '#32770'
52
+ # def dialog(title, seconds=5)
53
+ # close, dlg = begin
54
+ # sleep 0.25
55
+ # w = Gui.top_level(title, seconds, DialogWndClass)
56
+ # Gui.set_foreground_window w.handle
57
+ # sleep 0.25
58
+ #
59
+ # [yield(w), w]
60
+ # rescue TimeoutError
61
+ # end
62
+ #
63
+ # dlg.wait_for_close if dlg && close
64
+ # return dlg
65
+ # end
66
+
67
+ end
@@ -1,79 +1,108 @@
1
- require 'win/gui'
2
-
3
1
  module WinGui
4
- # Delay between key commands (events)
5
- WG_KEY_DELAY = 0.00001
6
- # Wait delay quant
7
- WG_SLEEP_DELAY = 0.001
8
- # Timeout waiting for Window to be closed
9
- WG_CLOSE_TIMEOUT = 1
10
2
 
3
+ # This class is a wrapper around window handle
11
4
  class Window
12
- include Win::Gui
13
- extend Win::Gui
14
-
5
+ # Make convenience methods from both WinGui and Win::Gui available as both class and instance methods
6
+ # Looks a bit circular though...
7
+ include WinGui
8
+ extend WinGui
9
+
15
10
  def initialize(handle)
16
11
  @handle = handle
17
12
  end
18
13
 
19
14
  attr_reader :handle
20
-
21
- # find top level window by title, return wrapped Window object
22
- def self.top_level(title, seconds=3)
23
- @handle = timeout(seconds) do
24
- sleep WG_SLEEP_DELAY while (h = find_window nil, title) == nil; h
15
+
16
+ # Finds top level window by title/class, returns wrapped Window object or nil.
17
+ # If timeout option given, waits for window to appear within timeout, returns nil if it didn't
18
+ # Options:
19
+ # :title:: window title
20
+ # :class:: window class
21
+ # :timeout:: timeout (seconds)
22
+ def self.top_level(opts={})
23
+ window_title = opts[:title]
24
+ window_class = opts[:class]
25
+ timeout = opts[:timeout] # || LOOKUP_TIMEOUT
26
+
27
+ if timeout
28
+ begin
29
+ timeout(timeout) do
30
+ sleep SLEEP_DELAY while (@handle = find_window window_class, window_title) == nil
31
+ end
32
+ rescue TimeoutError
33
+ nil
34
+ end
35
+ else
36
+ @handle = find_window window_class, window_title
25
37
  end
26
- Window.new @handle
27
- end
28
-
38
+ Window.new(@handle) if @handle
39
+ end
40
+
41
+ # def self.top_level(title, seconds=10, wnd_class = nil)
42
+ # @handle = timeout(seconds) do
43
+ # loop do
44
+ # h = find_window wnd_class, title
45
+ # break h if h > 0
46
+ # sleep 0.3
47
+ # end
48
+ # end
49
+ #
50
+ # Gui.new @handle
51
+ # end
52
+ # end
53
+
29
54
  # find child window (control) by title, window class, or control ID:
30
55
  def child(id)
31
56
  result = case id
32
57
  when String
33
- by_title = find_window_ex @handle, 0, nil, id.gsub('_' , '&' )
34
- by_class = find_window_ex @handle, 0, id, nil
35
- by_title ? by_title : by_class
58
+ by_title = find_window_ex @handle, 0, nil, id.gsub('_', '&' )
59
+ by_class = find_window_ex @handle, 0, id, nil
60
+ by_title ? by_title : by_class
36
61
  when Fixnum
37
- get_dlg_item @handle, id
62
+ get_dlg_item @handle, id
38
63
  when nil
39
- find_window_ex @handle, 0, nil, nil
40
- else
41
- nil
64
+ find_window_ex @handle, 0, nil, nil
65
+ else
66
+ nil
42
67
  end
43
68
  raise "Control '#{id}' not found" unless result
44
69
  Window.new result
45
70
  end
46
71
 
72
+ # returns array of Windows that are descendants (not only DIRECTchildren) of a given Window
47
73
  def children
48
74
  enum_child_windows(@handle).map{|child_handle| Window.new child_handle}
49
75
  end
50
76
 
51
77
  # emulate click of the control identified by id
52
78
  def click(id)
53
- h = child(id).handle
54
- rectangle = [0, 0, 0, 0].pack 'LLLL'
55
- get_window_rect h, rectangle
56
- left, top, right, bottom = rectangle.unpack 'LLLL'
79
+ left, top, right, bottom = get_window_rect child(id).handle
57
80
  center = [(left + right) / 2, (top + bottom) / 2]
58
81
  set_cursor_pos *center
59
82
  mouse_event MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0
60
83
  mouse_event MOUSEEVENTF_LEFTUP, 0, 0, 0, 0
61
84
  end
62
-
85
+
63
86
  def close
64
87
  post_message @handle, WM_SYSCOMMAND, SC_CLOSE, nil
65
88
  end
66
-
89
+
67
90
  def wait_for_close
68
- timeout(WG_CLOSE_TIMEOUT) do
69
- sleep WG_SLEEP_DELAY while window_visible?(@handle)
91
+ timeout(CLOSE_TIMEOUT) do
92
+ sleep SLEEP_DELAY while window_visible?(@handle)
70
93
  end
71
94
  end
72
-
95
+
96
+ # Window class name property - static (not changing)
97
+ def class_name
98
+ @class_name ||= get_class_name @handle
99
+ end
100
+
101
+ # Window text/title property - dynamic (changing)
73
102
  def text
74
- buffer = "\x0" * 2048
75
- length = send_message @handle, WM_GETTEXT, buffer.length, buffer
76
- length == 0 ? '' : buffer[0..length - 1]
103
+ buffer = FFI::MemoryPointer.from_string("\x0" * 2048)
104
+ num_chars = send_message @handle, WM_GETTEXT, buffer.size, buffer # length?
105
+ num_chars == 0 ? '' : buffer.read_string
77
106
  end
78
107
  end
79
108
  end
data/spec/spec_helper.rb CHANGED
@@ -22,7 +22,8 @@ end
22
22
  Spec::Runner.configure { |config| config.extend(SpecMacros) }
23
23
 
24
24
  module WinGuiTest
25
- include Win::Gui
25
+ include Win::Gui # This is a namespace from win gem.
26
+ include WinGui # This is our own main namespace. TODO: looks confusing... better names?
26
27
 
27
28
  # Test related Constants:
28
29
  TIMEOUT = 0.001
@@ -30,9 +31,8 @@ module WinGuiTest
30
31
  SLEEP_DELAY = 0.01
31
32
  APP_PATH = File.join(File.dirname(__FILE__), "../misc/locknote/LockNote.exe" )
32
33
  APP_START = RUBY_PLATFORM =~ /cygwin/ ? "cmd /c start `cygpath -w #{APP_PATH}`" : "start #{APP_PATH}"
33
- # end
34
- #
35
- # 'start "" "' + APP_PATH + '"'
34
+ # 'start "" "' + APP_PATH + '"'
35
+ DIALOG_TITLE = "Save As"
36
36
  WIN_TITLE = 'LockNote - Steganos LockNote'
37
37
  WIN_CLASS = 'ATL:00434098'
38
38
  WIN_RECT = [710, 400, 1210, 800]
@@ -49,7 +49,6 @@ module WinGuiTest
49
49
  end
50
50
 
51
51
  def any_handle
52
- WinGui.def_api 'FindWindow', 'PP', 'L' unless respond_to? :find_window
53
52
  find_window(nil, nil)
54
53
  end
55
54
 
@@ -65,6 +64,12 @@ module WinGuiTest
65
64
  system APP_START
66
65
  sleep SLEEP_DELAY until (handle = find_window(nil, WIN_TITLE))
67
66
  @launched_test_app = Window.new handle
67
+
68
+ def @launched_test_app.textarea #define singleton method retrieving app's text area
69
+ Window.new find_window_ex(self.handle, 0, TEXTAREA_CLASS, nil)
70
+ end
71
+
72
+ @launched_test_app
68
73
  end
69
74
 
70
75
  def close_test_app(app = @launched_test_app)
@@ -79,10 +84,6 @@ module WinGuiTest
79
84
  def test_app
80
85
  app = launch_test_app
81
86
 
82
- def app.textarea #define singleton method retrieving app's text area
83
- Window.new find_window_ex(self.handle, 0, TEXTAREA_CLASS, nil)
84
- end
85
-
86
87
  yield app
87
88
  close_test_app
88
89
  end
@@ -0,0 +1,68 @@
1
+ require File.join(File.dirname(__FILE__), "..", "spec_helper" )
2
+
3
+ module WinGuiTest
4
+
5
+ describe 'Convenience methods' do
6
+ before(:each){ @app = launch_test_app }
7
+ after(:each) { close_test_app }
8
+
9
+ describe '#dialog' do
10
+ before(:each){ keystroke(VK_ALT, 'F'.ord, 'A'.ord) }
11
+ after(:each) { keystroke(VK_ESCAPE) }
12
+
13
+ it 'returns top-level dialog window with given title if no block attached' do
14
+ dialog_window = dialog(DIALOG_TITLE, 0.1)
15
+ dialog_window.should_not == nil
16
+ dialog_window.should be_a Window
17
+ dialog_window.text.should == DIALOG_TITLE
18
+ end
19
+
20
+ it 'yields found dialog window to block if block is attached' do
21
+ dialog(DIALOG_TITLE, 0.1) do |dialog_window|
22
+ dialog_window.should_not == nil
23
+ dialog_window.should be_a Window
24
+ dialog_window.text.should == DIALOG_TITLE
25
+ end
26
+ end
27
+
28
+ it 'returns nil if there is no dialog with given title' do
29
+ dialog(IMPOSSIBLE, 0.1).should == nil
30
+ end
31
+
32
+ it 'yields nil to attached block if no dialog found' do
33
+ dialog(IMPOSSIBLE, 0.1) do |dialog_window|
34
+ dialog_window.should == nil
35
+ end
36
+ end
37
+
38
+ it 'considers timeout argument optional' do
39
+ dialog_window = dialog(DIALOG_TITLE)
40
+ dialog_window.text.should == DIALOG_TITLE
41
+ end
42
+ end # describe dialog
43
+
44
+ describe 'convenience input methods on top of Windows API' do
45
+ describe '#keystroke' do
46
+ spec{ use{ keystroke( vkey = 30, vkey = 30) }}
47
+
48
+ it 'emulates combinations of keys pressed (Ctrl+Alt+P+M, etc)' do
49
+ keystroke(VK_CONTROL, 'A'.ord)
50
+ keystroke(VK_SPACE)
51
+ @app.textarea.text.should.should == ' '
52
+ 2.times {keystroke(VK_CONTROL, 'Z'.ord)} # rolling back changes to allow window closing without dialog!
53
+ end
54
+ end # describe '#keystroke'
55
+
56
+ describe '#type_in' do
57
+ it 'types text message into the window holding the focus' do
58
+ text = '12 34'
59
+ type_in(text)
60
+ @app.textarea.text.should =~ Regexp.new(text)
61
+ 5.times {keystroke(VK_CONTROL, 'Z'.ord)} # rolling back changes to allow window closing without dialog!
62
+ end
63
+ end # describe '#type_in'
64
+
65
+ end # Input methods
66
+
67
+ end
68
+ end
@@ -1,27 +1,29 @@
1
- require File.join(File.dirname(__FILE__), ".." , "spec_helper" )
1
+ require File.join(File.dirname(__FILE__), "..", "spec_helper" )
2
2
 
3
3
  module WinGuiTest
4
- include WinGui
5
4
 
6
5
  describe Window do
7
6
  before(:each) { @app = launch_test_app }
8
7
  after(:each){ close_test_app }
9
-
8
+
10
9
  context 'initializing' do
11
10
  it 'can be wrapped around any existing window' do
12
11
  any_handle = find_window(nil, nil)
13
12
  use{ Window.new any_handle }
14
13
  end
15
14
  end
16
-
15
+
17
16
  context 'manipulating' do
18
-
17
+
19
18
  it 'has handle property equal to underlying window handle' do
20
- any_handle = find_window(nil, nil)
21
19
  any = Window.new any_handle
22
20
  any.handle.should == any_handle
23
21
  end
24
-
22
+
23
+ it 'has class_name property' do
24
+ @app.class_name.should == WIN_CLASS
25
+ end
26
+
25
27
  it 'has text property equal to underlying window text(title)' do
26
28
  @app.text.should == WIN_TITLE
27
29
  end
@@ -31,39 +33,57 @@ module WinGuiTest
31
33
  sleep SLEEP_DELAY # needed to ensure window had enough time to close down
32
34
  find_window(nil, WIN_TITLE).should == nil
33
35
  end
34
-
35
- it 'waits f0r window to disappear (NB: this happens before handle is released!)' do
36
- start = Time.now
36
+
37
+ it 'waits for window to disappear (NB: this happens before handle is released!)' do
38
+ start = Time.now
37
39
  @app.close
38
40
  @app.wait_for_close
39
- (Time.now - start).should be <= WG_CLOSE_TIMEOUT
41
+ (Time.now - start).should be <= CLOSE_TIMEOUT
40
42
  window_visible?(@app.handle).should be false
43
+ window?(@app.handle).should be false
41
44
  end
42
45
  end
43
-
44
- context '.top_level class method' do
45
- it 'finds any top-level window (title = nil) and wraps it in a Window object' do
46
- use { @win = Window.top_level(title = nil, timeout_sec = 3) }
47
- Window.should === @win
48
- end
49
-
46
+
47
+ describe '.top_level' do
50
48
  it 'finds top-level window by title and wraps it in a Window object' do
51
- win = Window.top_level( WIN_TITLE, 1)
49
+ win = Window.top_level( title: WIN_TITLE, timeout: 1)
52
50
  win.handle.should == @app.handle
53
51
  end
52
+
53
+ it 'finds top-level window by class and wraps it in a Window object' do
54
+ win = Window.top_level( class: WIN_CLASS, timeout: 1)
55
+ win.handle.should == @app.handle
56
+ end
57
+
58
+ it 'finds ANY top-level window without args and wraps it in a Window object' do
59
+ use { @win = Window.top_level() }
60
+ Window.should === @win
61
+ end
62
+
63
+ it 'returns nil immediately if top-level window with given title not found' do
64
+ start = Time.now
65
+ Window.top_level( title: IMPOSSIBLE).should == nil
66
+ (Time.now - start).should be_close 0, 0.02
67
+ end
68
+
69
+ it 'returns nil after timeout if top-level window with given title not found' do
70
+ start = Time.now
71
+ Window.top_level( title: IMPOSSIBLE, timeout: 1).should == nil
72
+ (Time.now - start).should be_close 1, 0.02
73
+ end
54
74
  end
55
-
56
- context '#child' do
57
- spec { use { @control = @app.child(title_class_id = nil) }}
75
+
76
+ describe '#child' do
77
+ spec { use { @control = @app.child(title_class_id = nil) }}
58
78
 
59
79
  it 'finds any child window(control) if given nil' do
60
80
  @app.child(nil).should_not == nil
61
81
  end
62
-
82
+
63
83
  it 'finds child window(control) by class' do
64
84
  @app.child(TEXTAREA_CLASS).should_not == nil
65
85
  end
66
-
86
+
67
87
  it 'finds child window(control) by name' do
68
88
  pending 'Need to find control with short name'
69
89
  @app.child(TEXTAREA_TEXT).should_not == nil
@@ -73,22 +93,18 @@ module WinGuiTest
73
93
  pending 'Need to find some control ID'
74
94
  @app.child(TEXTAREA_ID).should_not == nil
75
95
  end
76
-
96
+
77
97
  it 'raises error if wrong control is given' do
78
98
  expect { @app.child('Impossible Control')}.to raise_error "Control 'Impossible Control' not found"
79
99
  end
80
- it 'substitutes & for _ when searching by title ("&Yes" type controls)'
81
-
100
+
101
+ it 'substitutes & for _ when searching by title ("&Yes" type controls)' # Why?
82
102
  end
83
-
84
- context '#children' do
85
- spec { use { children = @app.children }}
86
103
 
87
- it 'returns an array' do
88
- @app.children.should be_a_kind_of(Array)
89
- end
104
+ describe '#children' do
105
+ spec { use { children = @app.children }}
90
106
 
91
- it 'returns an array of all child windows to the given window ' do
107
+ it 'returns an array of Windows that are descendants (not only DIRECTchildren) of a given Window' do
92
108
  children = @app.children
93
109
  children.should be_a_kind_of Array
94
110
  children.should_not be_empty
@@ -111,7 +127,7 @@ module WinGuiTest
111
127
  # expect { @app.child('Impossible Control')}.to raise_error "Control 'Impossible Control' not found"
112
128
  # end
113
129
  # it 'substitutes & for _ when searching by title ("&Yes" type controls)'
114
-
130
+
115
131
  end
116
132
 
117
133
  context '#click' do
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 2
8
- - 0
9
- version: 0.2.0
8
+ - 1
9
+ version: 0.2.1
10
10
  platform: ruby
11
11
  authors:
12
12
  - arvicco
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-05-15 00:00:00 +04:00
17
+ date: 2010-05-31 00:00:00 +04:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -69,10 +69,12 @@ extra_rdoc_files:
69
69
  - README.rdoc
70
70
  files:
71
71
  - lib/version.rb
72
+ - lib/win_gui/convenience.rb
72
73
  - lib/win_gui/window.rb
73
74
  - lib/win_gui.rb
74
75
  - spec/spec.opts
75
76
  - spec/spec_helper.rb
77
+ - spec/win_gui/convenience_spec.rb
76
78
  - spec/win_gui/window_spec.rb
77
79
  - features/step_definitions/win_gui_steps.rb
78
80
  - features/support/env.rb
@@ -127,4 +129,5 @@ summary: Abstractions/wrappers around GUI-related Win32 API functions
127
129
  test_files:
128
130
  - spec/spec.opts
129
131
  - spec/spec_helper.rb
132
+ - spec/win_gui/convenience_spec.rb
130
133
  - spec/win_gui/window_spec.rb