syrma 0.1.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +105 -0
- data/exe/syrma +6 -0
- data/lib/syrma/cli.rb +149 -0
- data/lib/syrma/clock.rb +24 -0
- data/lib/syrma/codegen.rb +91 -0
- data/lib/syrma/configuration.rb +17 -0
- data/lib/syrma/diagnostics.rb +64 -0
- data/lib/syrma/errors.rb +33 -0
- data/lib/syrma/event_codec.rb +63 -0
- data/lib/syrma/event_log.rb +22 -0
- data/lib/syrma/instrumentation.rb +80 -0
- data/lib/syrma/internals.rb +26 -0
- data/lib/syrma/minitest.rb +182 -0
- data/lib/syrma/popup_driver.rb +37 -0
- data/lib/syrma/query.rb +85 -0
- data/lib/syrma/rake_task.rb +28 -0
- data/lib/syrma/recorder.rb +35 -0
- data/lib/syrma/report.rb +57 -0
- data/lib/syrma/rspec.rb +154 -0
- data/lib/syrma/script_launcher.rb +62 -0
- data/lib/syrma/session.rb +172 -0
- data/lib/syrma/snapshot.rb +82 -0
- data/lib/syrma/snapshots/store.rb +153 -0
- data/lib/syrma/snapshots/terminal_format.rb +28 -0
- data/lib/syrma/snapshots/tree_format.rb +30 -0
- data/lib/syrma/text_systems.rb +26 -0
- data/lib/syrma/version.rb +5 -0
- data/lib/syrma/visual/comparator.rb +39 -0
- data/lib/syrma/visual/image.rb +31 -0
- data/lib/syrma/window_driver.rb +263 -0
- data/lib/syrma.rb +37 -0
- data/sig/syrma.rbs +266 -0
- metadata +93 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Syrma
|
|
4
|
+
class WindowDriver
|
|
5
|
+
I = Zaniah::Input
|
|
6
|
+
|
|
7
|
+
attr_reader :window, :session
|
|
8
|
+
|
|
9
|
+
def initialize(window, session)
|
|
10
|
+
@window = window
|
|
11
|
+
@session = session
|
|
12
|
+
Instrumentation.capture(window, lazy_raster: session.raster == :lazy)
|
|
13
|
+
@builder = SnapshotBuilder.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def strict = session.strict
|
|
17
|
+
|
|
18
|
+
def tree
|
|
19
|
+
session.settle
|
|
20
|
+
@tree = nil if @tree && @tree.frame != window.testing_frame
|
|
21
|
+
@tree ||= @builder.build(window)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def find(**conditions, &predicate) = Locator.new(self, Criteria.new(conditions, predicate))
|
|
25
|
+
def all(**conditions, &predicate) = find(**conditions, &predicate).resolve_all
|
|
26
|
+
def test_id(id) = find(test_id: id)
|
|
27
|
+
def text(value) = find(text: value)
|
|
28
|
+
def texts = tree.text_runs.map { |run| run[2] }
|
|
29
|
+
def tooltip = tree.tooltip
|
|
30
|
+
|
|
31
|
+
def button(label)
|
|
32
|
+
find(clickable: true) do |node|
|
|
33
|
+
node.content_text == label &&
|
|
34
|
+
node.descendants.none? { |descendant| descendant.clickable? && descendant.content_text == label }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def at(x, y = nil, event: :mouse_down, button: :left)
|
|
39
|
+
point = x.is_a?(Zaniah::Point) ? x : Zaniah::Point.new(x, y)
|
|
40
|
+
tree.hit_at(point, event: event, button: button)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def pixel(x, y)
|
|
44
|
+
width = window.content_size.width.to_i
|
|
45
|
+
height = window.content_size.height.to_i
|
|
46
|
+
x = Integer(x)
|
|
47
|
+
y = Integer(y)
|
|
48
|
+
raise RangeError, "Pixel coordinates are out of bounds: #{x},#{y}" unless x.between?(0, width - 1) && y.between?(0, height - 1)
|
|
49
|
+
|
|
50
|
+
session.screenshot_pixels(self).byteslice((y * width + x) * 4, 4).bytes
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def screenshot(path = nil)
|
|
54
|
+
width = window.content_size.width.to_i
|
|
55
|
+
height = window.content_size.height.to_i
|
|
56
|
+
pixels = session.screenshot_pixels(self)
|
|
57
|
+
return pixels unless path
|
|
58
|
+
|
|
59
|
+
Zaniah::PNG.write(path, width, height, pixels)
|
|
60
|
+
path
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def terminal_lines(colors: false)
|
|
64
|
+
raise UnsupportedBackend, "terminal_lines is only available for TUI sessions" unless session.tui?(self)
|
|
65
|
+
|
|
66
|
+
session.settle
|
|
67
|
+
TerminalFormat.lines(session.output_for(self).string, colors: colors)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def click(target = nil, button: :left, modifiers: [], count: 1, offset: nil, force: false, **criteria)
|
|
71
|
+
target = criteria unless criteria.empty?
|
|
72
|
+
point = actionable_point(target, :mouse_down, button, force, offset)
|
|
73
|
+
gesture do
|
|
74
|
+
dispatch I::MouseMove.new(point, modifiers)
|
|
75
|
+
dispatch I::MouseDown.new(point, button, modifiers, count)
|
|
76
|
+
dispatch I::MouseUp.new(point, button, modifiers)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def double_click(target, **options)
|
|
81
|
+
click(target, **options, count: 1)
|
|
82
|
+
click(target, **options, count: 2)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def right_click(target, **options) = click(target, **options, button: :right)
|
|
86
|
+
def hover(target) = dispatch(I::MouseMove.new(actionable_point(target, :mouse_move, :left, false, nil), []))
|
|
87
|
+
|
|
88
|
+
def drag(from, to, steps: 8, button: :left, modifiers: [])
|
|
89
|
+
raise ArgumentError, "steps must be a positive integer" unless steps.is_a?(Integer) && steps.positive?
|
|
90
|
+
|
|
91
|
+
start = actionable_point(from, :mouse_down, button, false, nil)
|
|
92
|
+
finish = point_for(to)
|
|
93
|
+
gesture do
|
|
94
|
+
dispatch I::MouseMove.new(start, modifiers)
|
|
95
|
+
dispatch I::MouseDown.new(start, button, modifiers, 1)
|
|
96
|
+
1.upto(steps) do |index|
|
|
97
|
+
ratio = index.fdiv(steps)
|
|
98
|
+
dispatch I::MouseMove.new(
|
|
99
|
+
Zaniah::Point.new(start.x + (finish.x - start.x) * ratio,
|
|
100
|
+
start.y + (finish.y - start.y) * ratio), modifiers
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
dispatch I::MouseUp.new(finish, button, modifiers)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def scroll(target, dx: 0, dy: 0, modifiers: [])
|
|
108
|
+
point = actionable_point(target, :scroll, :left, false, nil)
|
|
109
|
+
dispatch I::ScrollWheel.new(point, Zaniah::Point.new(dx, dy), nil, modifiers)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def mouse_move(point, modifiers: []) = dispatch(I::MouseMove.new(point_for(point), modifiers))
|
|
113
|
+
def mouse_down(point, button: :left, modifiers: [], count: 1) = dispatch(I::MouseDown.new(point_for(point), button, modifiers, count))
|
|
114
|
+
def mouse_up(point, button: :left, modifiers: []) = dispatch(I::MouseUp.new(point_for(point), button, modifiers))
|
|
115
|
+
|
|
116
|
+
def press(*keystrokes)
|
|
117
|
+
keystrokes.each do |stroke|
|
|
118
|
+
normalized = I::Keystroke.normalize(stroke)
|
|
119
|
+
gesture do
|
|
120
|
+
dispatch I::KeyDown.new(normalized, false)
|
|
121
|
+
dispatch I::KeyUp.new(normalized)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def key_down(stroke, held: false) = dispatch(I::KeyDown.new(I::Keystroke.normalize(stroke), held))
|
|
127
|
+
def key_up(stroke) = dispatch(I::KeyUp.new(I::Keystroke.normalize(stroke)))
|
|
128
|
+
|
|
129
|
+
def type(text, key_events: true)
|
|
130
|
+
text.each_grapheme_cluster do |character|
|
|
131
|
+
key = character == " " ? "space" : character.downcase
|
|
132
|
+
gesture do
|
|
133
|
+
dispatch I::KeyDown.new(key, false) if key_events
|
|
134
|
+
dispatch I::TextInput.new(character)
|
|
135
|
+
dispatch I::KeyUp.new(key) if key_events
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def paste(text) = dispatch(I::TextInput.new(text))
|
|
141
|
+
def compose(text, selection: nil) = dispatch(I::Composition.new(text, selection))
|
|
142
|
+
def commit(text) = dispatch(I::TextInput.new(text))
|
|
143
|
+
|
|
144
|
+
def drop_files(paths, at:)
|
|
145
|
+
dispatch I::FileDrop.new(Array(paths).map(&:to_s), point_for(at))
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def resize(width, height)
|
|
149
|
+
window.resize(width, height)
|
|
150
|
+
session.settle
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def close = window.close
|
|
154
|
+
|
|
155
|
+
def feed_terminal(bytes)
|
|
156
|
+
raise UnsupportedBackend, "feed_terminal is only available for TUI sessions" unless window.respond_to?(:feed_input)
|
|
157
|
+
|
|
158
|
+
window.feed_input(bytes)
|
|
159
|
+
session.settle
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def scroll_until(list, found:, step: 100, max: 50)
|
|
163
|
+
target = find(**found)
|
|
164
|
+
max.times do
|
|
165
|
+
return target if target.exists? && target.resolve_all.any?(&:visible?)
|
|
166
|
+
|
|
167
|
+
scroll(list, dy: step)
|
|
168
|
+
end
|
|
169
|
+
raise ElementNotFound.new(target, tree)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def menu = (@menu_driver ||= PopupDriver.new(self))
|
|
173
|
+
|
|
174
|
+
def dispatch(event)
|
|
175
|
+
session.event_log.add(window.testing_frame, window.title, event)
|
|
176
|
+
window.input(event)
|
|
177
|
+
session.settle unless @in_gesture && session.event_frames == :gesture
|
|
178
|
+
event
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def gesture
|
|
182
|
+
outer = @in_gesture
|
|
183
|
+
@in_gesture = true
|
|
184
|
+
yield
|
|
185
|
+
ensure
|
|
186
|
+
@in_gesture = outer
|
|
187
|
+
session.settle unless outer
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
def normalize_target(target)
|
|
193
|
+
return find(**target) if target.is_a?(Hash)
|
|
194
|
+
return Zaniah::Point.new(*target) if target.is_a?(Array) && target.length == 2
|
|
195
|
+
|
|
196
|
+
target
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def resolve_node(target)
|
|
200
|
+
target = normalize_target(target)
|
|
201
|
+
target.is_a?(Locator) ? target.resolve : target
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def point_for(target)
|
|
205
|
+
target = normalize_target(target)
|
|
206
|
+
return target if target.is_a?(Zaniah::Point)
|
|
207
|
+
|
|
208
|
+
resolve_node(target).center
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def actionable_point(target, event, button, force, offset)
|
|
212
|
+
target = normalize_target(target)
|
|
213
|
+
return target if target.is_a?(Zaniah::Point)
|
|
214
|
+
|
|
215
|
+
last_error = nil
|
|
216
|
+
session.wait_for(-> { "Target did not become actionable: #{target}\n Last reason: #{last_error&.message}" }) do
|
|
217
|
+
current_tree = tree
|
|
218
|
+
node = target.is_a?(Locator) ? target.resolve(current_tree) : target
|
|
219
|
+
point = offset ? offset_point(node, offset) : node.center
|
|
220
|
+
force ? point : check_actionable(current_tree, node, event, button, point)
|
|
221
|
+
rescue ElementNotFound, AmbiguousMatch, NotActionable => error
|
|
222
|
+
raise if error.is_a?(AmbiguousMatch)
|
|
223
|
+
|
|
224
|
+
last_error = error
|
|
225
|
+
nil
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def offset_point(node, offset)
|
|
230
|
+
x, y = offset.respond_to?(:x) ? [offset.x, offset.y] : offset
|
|
231
|
+
Zaniah::Point.new(node.visible_bounds.x + x, node.visible_bounds.y + y)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def check_actionable(current_tree, node, event, button, preferred = nil)
|
|
235
|
+
bounds = node.bounds
|
|
236
|
+
unless bounds.width.positive? && bounds.height.positive?
|
|
237
|
+
raise NotActionable.new(:invisible, "#{node.inspect} has zero size")
|
|
238
|
+
end
|
|
239
|
+
unless node.visible?
|
|
240
|
+
raise NotActionable.new(:outside_viewport, "#{node.inspect} is outside the viewport or clipped by an ancestor")
|
|
241
|
+
end
|
|
242
|
+
raise NotActionable.new(:popup, "A context menu has captured input") if current_tree.menu
|
|
243
|
+
|
|
244
|
+
points = preferred ? [preferred] : samples(node.visible_bounds)
|
|
245
|
+
points.each do |point|
|
|
246
|
+
hit = current_tree.hit_at(point, event: event, button: button)
|
|
247
|
+
next if hit.nil?
|
|
248
|
+
|
|
249
|
+
return point if hit.equal?(node) || node.ancestor_of?(hit) || hit.ancestor_of?(node)
|
|
250
|
+
end
|
|
251
|
+
blocker = current_tree.hit_at(preferred || node.center, event: event, button: button)
|
|
252
|
+
reason = blocker ? :obscured : :no_handler
|
|
253
|
+
message = blocker ? "#{node.inspect} is covered by #{blocker.inspect}" : "No element receives events at #{node.inspect}"
|
|
254
|
+
raise NotActionable.new(reason, message)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def samples(bounds)
|
|
258
|
+
[[0.5, 0.5], *[0.2, 0.5, 0.8].product([0.2, 0.5, 0.8])].uniq.map do |x, y|
|
|
259
|
+
Zaniah::Point.new(bounds.x + bounds.width * x, bounds.y + bounds.height * y)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
data/lib/syrma.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zaniah"
|
|
4
|
+
require_relative "syrma/version"
|
|
5
|
+
require_relative "syrma/configuration"
|
|
6
|
+
require_relative "syrma/errors"
|
|
7
|
+
require_relative "syrma/internals"
|
|
8
|
+
require_relative "syrma/instrumentation"
|
|
9
|
+
require_relative "syrma/clock"
|
|
10
|
+
require_relative "syrma/text_systems"
|
|
11
|
+
require_relative "syrma/snapshot"
|
|
12
|
+
require_relative "syrma/query"
|
|
13
|
+
require_relative "syrma/event_log"
|
|
14
|
+
require_relative "syrma/event_codec"
|
|
15
|
+
require_relative "syrma/recorder"
|
|
16
|
+
require_relative "syrma/window_driver"
|
|
17
|
+
require_relative "syrma/popup_driver"
|
|
18
|
+
require_relative "syrma/session"
|
|
19
|
+
require_relative "syrma/visual/comparator"
|
|
20
|
+
require_relative "syrma/visual/image"
|
|
21
|
+
require_relative "syrma/snapshots/store"
|
|
22
|
+
require_relative "syrma/snapshots/tree_format"
|
|
23
|
+
require_relative "syrma/snapshots/terminal_format"
|
|
24
|
+
require_relative "syrma/diagnostics"
|
|
25
|
+
require_relative "syrma/codegen"
|
|
26
|
+
require_relative "syrma/script_launcher"
|
|
27
|
+
require_relative "syrma/report"
|
|
28
|
+
|
|
29
|
+
module Syrma
|
|
30
|
+
class << self
|
|
31
|
+
attr_writer :configuration
|
|
32
|
+
|
|
33
|
+
def configuration = (@configuration ||= Configuration.new)
|
|
34
|
+
def configure = yield(configuration)
|
|
35
|
+
def reset_configuration! = self.configuration = Configuration.new
|
|
36
|
+
end
|
|
37
|
+
end
|
data/sig/syrma.rbs
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
module Syrma
|
|
2
|
+
VERSION: String
|
|
3
|
+
|
|
4
|
+
def self.configuration: () -> Configuration
|
|
5
|
+
def self.configuration=: (Configuration) -> Configuration
|
|
6
|
+
def self.configure: () { (Configuration) -> untyped } -> untyped
|
|
7
|
+
def self.reset_configuration!: () -> Configuration
|
|
8
|
+
|
|
9
|
+
class Configuration
|
|
10
|
+
attr_accessor timeout: Float
|
|
11
|
+
attr_accessor strict: bool
|
|
12
|
+
attr_accessor event_frames: Symbol
|
|
13
|
+
attr_accessor text: untyped
|
|
14
|
+
attr_accessor fonts: Array[String]
|
|
15
|
+
attr_accessor snapshot_dir: String
|
|
16
|
+
attr_accessor artifacts_dir: String
|
|
17
|
+
def initialize: () -> void
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class Clock
|
|
21
|
+
def self.real_now: () -> Float
|
|
22
|
+
def initialize: () -> void
|
|
23
|
+
def call: () -> Float
|
|
24
|
+
def advance: (Numeric seconds) -> Float
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class Error < StandardError
|
|
28
|
+
end
|
|
29
|
+
class UnstableUI < Error
|
|
30
|
+
end
|
|
31
|
+
class WaitTimeout < Error
|
|
32
|
+
end
|
|
33
|
+
class SnapshotMissing < Error
|
|
34
|
+
end
|
|
35
|
+
class SnapshotMismatch < Error
|
|
36
|
+
end
|
|
37
|
+
class UnsupportedBackend < Error
|
|
38
|
+
end
|
|
39
|
+
class ElementNotFound < Error
|
|
40
|
+
def initialize: (untyped locator, Tree tree) -> void
|
|
41
|
+
end
|
|
42
|
+
class AmbiguousMatch < Error
|
|
43
|
+
def initialize: (untyped locator, Array[Node] nodes) -> void
|
|
44
|
+
end
|
|
45
|
+
class NotActionable < Error
|
|
46
|
+
attr_reader reason: Symbol
|
|
47
|
+
def initialize: (Symbol reason, String message) -> void
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class Node
|
|
51
|
+
attr_reader path: Array[Integer]
|
|
52
|
+
attr_reader element: untyped
|
|
53
|
+
attr_reader type: Symbol
|
|
54
|
+
attr_reader test_id: String?
|
|
55
|
+
attr_reader key: untyped
|
|
56
|
+
attr_reader text: String?
|
|
57
|
+
attr_reader color: untyped
|
|
58
|
+
attr_reader font_size: Numeric?
|
|
59
|
+
attr_reader bounds: untyped
|
|
60
|
+
attr_reader visible_bounds: untyped
|
|
61
|
+
attr_reader background: untyped
|
|
62
|
+
attr_reader border_color: untyped
|
|
63
|
+
attr_reader radius: untyped
|
|
64
|
+
attr_reader handlers: Array[Symbol]
|
|
65
|
+
attr_reader tooltip: String?
|
|
66
|
+
attr_reader children: Array[Node]
|
|
67
|
+
def visible?: () -> bool
|
|
68
|
+
def clickable?: () -> bool
|
|
69
|
+
def center: () -> untyped
|
|
70
|
+
def descendants: () -> Array[Node]
|
|
71
|
+
def content_text: () -> String
|
|
72
|
+
def ancestor_of?: (Node other) -> bool
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class Tree
|
|
76
|
+
attr_reader frame: Integer
|
|
77
|
+
attr_reader size: untyped
|
|
78
|
+
attr_reader root: Node?
|
|
79
|
+
attr_reader nodes: Array[Node]
|
|
80
|
+
attr_reader hits: Array[untyped]
|
|
81
|
+
attr_reader text_runs: Array[untyped]
|
|
82
|
+
attr_reader menu: Array[String]?
|
|
83
|
+
attr_reader menu_index: Integer?
|
|
84
|
+
attr_reader tooltip: String?
|
|
85
|
+
def hit_at: (untyped point, ?event: Symbol, ?button: Symbol) -> Node?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
class Criteria
|
|
89
|
+
def initialize: (Hash[Symbol, untyped] conditions, ?^(Node) -> bool predicate) -> void
|
|
90
|
+
def match?: (Node node) -> bool
|
|
91
|
+
def to_s: () -> String
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
class Locator
|
|
95
|
+
attr_reader driver: WindowDriver
|
|
96
|
+
attr_reader criteria: Criteria
|
|
97
|
+
attr_reader scope: Locator?
|
|
98
|
+
attr_reader index: Integer?
|
|
99
|
+
def find: (**untyped) ?{ (Node) -> bool } -> Locator
|
|
100
|
+
def nth: (Integer index) -> Locator
|
|
101
|
+
def first: () -> Locator
|
|
102
|
+
def last: () -> Locator
|
|
103
|
+
def candidates: (?Tree tree) -> Array[Node]
|
|
104
|
+
def resolve_all: (?Tree tree) -> Array[Node]
|
|
105
|
+
def resolve: (?Tree tree, ?strict: bool) -> Node
|
|
106
|
+
def count: () -> Integer
|
|
107
|
+
def exists?: () -> bool
|
|
108
|
+
def visible?: () -> bool
|
|
109
|
+
def text: () -> String
|
|
110
|
+
def click: (**untyped) -> untyped
|
|
111
|
+
def double_click: (**untyped) -> untyped
|
|
112
|
+
def hover: () -> untyped
|
|
113
|
+
def right_click: () -> untyped
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
class Event
|
|
117
|
+
attr_reader frame: Integer
|
|
118
|
+
attr_reader window: String
|
|
119
|
+
attr_reader input: untyped
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
class EventLog
|
|
123
|
+
include Enumerable[Event]
|
|
124
|
+
def initialize: (?limit: Integer) -> void
|
|
125
|
+
def each: () { (Event) -> void } -> untyped
|
|
126
|
+
def last: (?Integer count) -> (Event | Array[Event] | nil)
|
|
127
|
+
def add: (Integer frame, String window, untyped input) -> Event
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
class Session
|
|
131
|
+
attr_reader raster: Symbol
|
|
132
|
+
attr_reader strict: bool
|
|
133
|
+
attr_reader timeout: Float
|
|
134
|
+
attr_reader event_log: EventLog
|
|
135
|
+
attr_reader app: untyped
|
|
136
|
+
attr_reader event_frames: Symbol
|
|
137
|
+
attr_reader font_paths: Array[String]
|
|
138
|
+
attr_reader text_mode: untyped
|
|
139
|
+
attr_reader windows: Array[untyped]
|
|
140
|
+
attr_reader drivers: Array[WindowDriver]
|
|
141
|
+
def self.new: (**untyped) ?{ (untyped) -> untyped } -> Session
|
|
142
|
+
def self.attach: (untyped window, **untyped) -> Session
|
|
143
|
+
def self.for_app: (untyped app, **untyped) { (untyped) -> untyped } -> Session
|
|
144
|
+
def self.launch_script: (String path, ?argv: Array[String], **untyped) -> Session
|
|
145
|
+
def window: (?title: String?, ?index: Integer?) -> untyped
|
|
146
|
+
def driver: () -> WindowDriver
|
|
147
|
+
def session: () -> Session
|
|
148
|
+
def settle: () -> Session
|
|
149
|
+
def advance: (Numeric seconds) -> Session
|
|
150
|
+
def wait_for: (?untyped message, ?timeout: Numeric) { () -> untyped } -> untyped
|
|
151
|
+
def screenshot_pixels: (?WindowDriver target) -> String
|
|
152
|
+
def tui?: (?WindowDriver target) -> bool
|
|
153
|
+
def output_for: (?WindowDriver target) -> untyped
|
|
154
|
+
def exit_status: () -> Integer?
|
|
155
|
+
def close: () -> bool
|
|
156
|
+
def method_missing: (Symbol name, *untyped, **untyped) -> untyped
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
class WindowDriver
|
|
160
|
+
attr_reader window: untyped
|
|
161
|
+
attr_reader session: Session
|
|
162
|
+
def strict: () -> bool
|
|
163
|
+
def tree: () -> Tree
|
|
164
|
+
def find: (**untyped) ?{ (Node) -> bool } -> Locator
|
|
165
|
+
def all: (**untyped) ?{ (Node) -> bool } -> Array[Node]
|
|
166
|
+
def test_id: (String | Symbol id) -> Locator
|
|
167
|
+
def text: (String | Regexp value) -> Locator
|
|
168
|
+
def texts: () -> Array[String]
|
|
169
|
+
def tooltip: () -> String?
|
|
170
|
+
def button: (String label) -> Locator
|
|
171
|
+
def at: (untyped x, ?untyped y, ?event: Symbol, ?button: Symbol) -> Node?
|
|
172
|
+
def pixel: (Integer x, Integer y) -> Array[Integer]
|
|
173
|
+
def screenshot: (?String path) -> String
|
|
174
|
+
def terminal_lines: (?colors: bool) -> Array[String]
|
|
175
|
+
def click: (?untyped target, **untyped) -> untyped
|
|
176
|
+
def double_click: (untyped target, **untyped) -> untyped
|
|
177
|
+
def right_click: (untyped target, **untyped) -> untyped
|
|
178
|
+
def hover: (untyped target) -> untyped
|
|
179
|
+
def drag: (untyped from, untyped to, **untyped) -> untyped
|
|
180
|
+
def scroll: (untyped target, **untyped) -> untyped
|
|
181
|
+
def mouse_move: (untyped point, **untyped) -> untyped
|
|
182
|
+
def mouse_down: (untyped point, **untyped) -> untyped
|
|
183
|
+
def mouse_up: (untyped point, **untyped) -> untyped
|
|
184
|
+
def press: (*String keystrokes) -> untyped
|
|
185
|
+
def key_down: (String stroke, ?held: bool) -> untyped
|
|
186
|
+
def key_up: (String stroke) -> untyped
|
|
187
|
+
def type: (String text, ?key_events: bool) -> untyped
|
|
188
|
+
def paste: (String text) -> untyped
|
|
189
|
+
def compose: (String text, ?selection: untyped) -> untyped
|
|
190
|
+
def commit: (String text) -> untyped
|
|
191
|
+
def drop_files: (Array[String] paths, at: untyped) -> untyped
|
|
192
|
+
def resize: (Numeric width, Numeric height) -> untyped
|
|
193
|
+
def close: () -> untyped
|
|
194
|
+
def feed_terminal: (String bytes) -> untyped
|
|
195
|
+
def scroll_until: (untyped list, found: Hash[Symbol, untyped], ?step: Numeric, ?max: Integer) -> Locator
|
|
196
|
+
def menu: () -> PopupDriver
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
class PopupDriver
|
|
200
|
+
def open?: () -> bool
|
|
201
|
+
def items: () -> Array[String]
|
|
202
|
+
def selected: () -> String?
|
|
203
|
+
def dismiss: () -> untyped
|
|
204
|
+
def select: (String label, ?via: Symbol) -> untyped
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
class Comparator
|
|
208
|
+
class Result
|
|
209
|
+
attr_reader width: Integer
|
|
210
|
+
attr_reader height: Integer
|
|
211
|
+
attr_reader diff_pixels: Integer
|
|
212
|
+
attr_reader diff_image: String
|
|
213
|
+
def ratio: () -> Float
|
|
214
|
+
end
|
|
215
|
+
def initialize: (?threshold: Numeric) -> void
|
|
216
|
+
def compare: (Integer width, Integer height, String expected, String actual) -> Result
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
class Recorder
|
|
220
|
+
def initialize: (WindowDriver driver, IO output) -> void
|
|
221
|
+
def record: (untyped event) -> untyped
|
|
222
|
+
def close: () -> untyped
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
module EventCodec
|
|
226
|
+
def self.dump: (untyped event, t: Numeric, ?target: Hash[String, untyped]?) -> String
|
|
227
|
+
def self.load: (String line) -> untyped
|
|
228
|
+
def self.parse: (String line) -> Hash[String, untyped]
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
module Codegen
|
|
232
|
+
def self.generate: (Array[String] lines, ?framework: Symbol) -> String
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
module Report
|
|
236
|
+
def self.generate: (?root: String?, ?output: String?) -> String
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
class CLI
|
|
240
|
+
def self.run: (?Array[String] argv, ?out: IO, ?err: IO) -> Integer
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
class RakeTask
|
|
244
|
+
def initialize: () -> void
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
module Minitest
|
|
248
|
+
def zaniah_session: (**untyped) { (untyped) -> untyped } -> Session
|
|
249
|
+
def ui: () -> Session
|
|
250
|
+
def assert_ui_text: (String | Regexp expected, **untyped) -> bool
|
|
251
|
+
def refute_ui_text: (String | Regexp unexpected, **untyped) -> bool
|
|
252
|
+
def assert_element: (?untyped target, **untyped) -> bool
|
|
253
|
+
def refute_element: (?untyped target, **untyped) -> bool
|
|
254
|
+
def assert_visible: (?untyped target, **untyped) -> bool
|
|
255
|
+
def assert_hidden: (?untyped target, **untyped) -> bool
|
|
256
|
+
def assert_clickable: (?untyped target, **untyped) -> bool
|
|
257
|
+
def assert_bg: (untyped target, untyped color, **untyped) -> bool
|
|
258
|
+
def assert_pixel: (Integer x, Integer y, untyped color, **untyped) -> bool
|
|
259
|
+
def assert_tooltip: (String | Regexp expected, **untyped) -> bool
|
|
260
|
+
def assert_menu_items: (Array[String] labels, **untyped) -> bool
|
|
261
|
+
def assert_screenshot: (String name, **untyped) -> bool
|
|
262
|
+
def assert_tree_snapshot: (String name, **untyped) -> bool
|
|
263
|
+
def assert_terminal_snapshot: (String name, **untyped) -> bool
|
|
264
|
+
def assert_no_text_overlap: (**untyped) -> bool
|
|
265
|
+
end
|
|
266
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: syrma
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yudai Takada
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: zaniah
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.2.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.2.0
|
|
26
|
+
description: 'Drive Zaniah windows headlessly: locate elements, send real input events,
|
|
27
|
+
and assert text, layout, pixels, and snapshots.'
|
|
28
|
+
email:
|
|
29
|
+
- t.yudai92@gmail.com
|
|
30
|
+
executables:
|
|
31
|
+
- syrma
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- CHANGELOG.md
|
|
36
|
+
- LICENSE.txt
|
|
37
|
+
- README.md
|
|
38
|
+
- exe/syrma
|
|
39
|
+
- lib/syrma.rb
|
|
40
|
+
- lib/syrma/cli.rb
|
|
41
|
+
- lib/syrma/clock.rb
|
|
42
|
+
- lib/syrma/codegen.rb
|
|
43
|
+
- lib/syrma/configuration.rb
|
|
44
|
+
- lib/syrma/diagnostics.rb
|
|
45
|
+
- lib/syrma/errors.rb
|
|
46
|
+
- lib/syrma/event_codec.rb
|
|
47
|
+
- lib/syrma/event_log.rb
|
|
48
|
+
- lib/syrma/instrumentation.rb
|
|
49
|
+
- lib/syrma/internals.rb
|
|
50
|
+
- lib/syrma/minitest.rb
|
|
51
|
+
- lib/syrma/popup_driver.rb
|
|
52
|
+
- lib/syrma/query.rb
|
|
53
|
+
- lib/syrma/rake_task.rb
|
|
54
|
+
- lib/syrma/recorder.rb
|
|
55
|
+
- lib/syrma/report.rb
|
|
56
|
+
- lib/syrma/rspec.rb
|
|
57
|
+
- lib/syrma/script_launcher.rb
|
|
58
|
+
- lib/syrma/session.rb
|
|
59
|
+
- lib/syrma/snapshot.rb
|
|
60
|
+
- lib/syrma/snapshots/store.rb
|
|
61
|
+
- lib/syrma/snapshots/terminal_format.rb
|
|
62
|
+
- lib/syrma/snapshots/tree_format.rb
|
|
63
|
+
- lib/syrma/text_systems.rb
|
|
64
|
+
- lib/syrma/version.rb
|
|
65
|
+
- lib/syrma/visual/comparator.rb
|
|
66
|
+
- lib/syrma/visual/image.rb
|
|
67
|
+
- lib/syrma/window_driver.rb
|
|
68
|
+
- sig/syrma.rbs
|
|
69
|
+
homepage: https://github.com/ydah/syrma
|
|
70
|
+
licenses:
|
|
71
|
+
- MIT
|
|
72
|
+
metadata:
|
|
73
|
+
source_code_uri: https://github.com/ydah/syrma
|
|
74
|
+
changelog_uri: https://github.com/ydah/syrma/blob/main/CHANGELOG.md
|
|
75
|
+
rubygems_mfa_required: 'true'
|
|
76
|
+
rdoc_options: []
|
|
77
|
+
require_paths:
|
|
78
|
+
- lib
|
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '3.1'
|
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
requirements: []
|
|
90
|
+
rubygems_version: 4.0.19
|
|
91
|
+
specification_version: 4
|
|
92
|
+
summary: UI testing toolkit for applications built with Zaniah
|
|
93
|
+
test_files: []
|