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.
@@ -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: []