win_gui 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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