syrma 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +85 -11
- data/lib/syrma/instrumentation.rb +13 -4
- data/lib/syrma/internals.rb +9 -3
- data/lib/syrma/minitest.rb +36 -0
- data/lib/syrma/rspec.rb +27 -0
- data/lib/syrma/session.rb +1 -0
- data/lib/syrma/ui_assertions.rb +51 -0
- data/lib/syrma/version.rb +1 -1
- data/lib/syrma/window_driver.rb +14 -4
- data/lib/syrma.rb +1 -0
- data/sig/syrma.rbs +6 -0
- metadata +20 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0125f382e153f0c3485107cf3cbe4719733da7b250f3e2e7aeac507c7f3beab7
|
|
4
|
+
data.tar.gz: 53f0fd7a4f089b5a8b42e3ea36a2e0edef6c9aec39c6b1c8d006cd1850dcfa8a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2a124116d448b9c7d85cacb595e57a155b11cbf1f1f4a7ec2b97904d9b99aff2f00c35751c435fd2563a9efaa31ab8a53c098f192954c270fe95ad719d789a66
|
|
7
|
+
data.tar.gz: 299ed17ddf94e4c81db7f80517e3402640a55af4768386e5e0ac40668f40d015127cc738e79f0e1a9bb5da639ad8a32ce9b59e191a413b5dc700ae55ce770772
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.2.0 - 2026-09-15
|
|
6
|
+
|
|
7
|
+
- Add Minitest assertions and RSpec matchers for instrumented panels and editor decorations.
|
|
8
|
+
- Support Zaniah 0.2 through 0.5 in headless instrumentation.
|
|
9
|
+
|
|
3
10
|
## 0.1.0 - 2026-09-11
|
|
4
11
|
|
|
5
12
|
- Initial release.
|
data/README.md
CHANGED
|
@@ -1,10 +1,41 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">Syrma</h1>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center">
|
|
4
|
+
<strong>Drive and assert <a href="https://github.com/noxdea/zaniah">Zaniah</a> GUI and TUI applications from Ruby tests.</strong>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://rubygems.org/gems/syrma"><img src="https://img.shields.io/gem/v/syrma.svg" alt="Gem version"></a>
|
|
9
|
+
<a href="https://github.com/noxdea/syrma/actions/workflows/main.yml"><img src="https://github.com/noxdea/syrma/actions/workflows/main.yml/badge.svg" alt="CI"></a>
|
|
10
|
+
<a href="https://www.ruby-lang.org/"><img src="https://img.shields.io/badge/ruby-3.1%2B-CC342D.svg" alt="Ruby 3.1+"></a>
|
|
11
|
+
<a href="LICENSE.txt"><img src="https://img.shields.io/github/license/noxdea/syrma.svg" alt="MIT License"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://noxdea.github.io/syrma/">Website</a> ·
|
|
16
|
+
<a href="#features">Features</a> ·
|
|
17
|
+
<a href="#installation">Installation</a> ·
|
|
18
|
+
<a href="#quick-start">Quick Start</a> ·
|
|
19
|
+
<a href="#snapshots-and-diagnostics">Snapshots</a> ·
|
|
20
|
+
<a href="#documentation">Documentation</a>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
Syrma locates rendered elements, sends input through Zaniah's real event path, waits for redraws, and compares the observable result. Tests can inspect text, element trees, terminal output, pixels, screenshots, menus, and tooltips without bypassing application input handling.
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- Drive GUI and TUI sessions with pointer, keyboard, clipboard, composition, file-drop, resize, and terminal input
|
|
30
|
+
- Locate elements by text, test ID, actionability, or nested queries resolved against the latest frame
|
|
31
|
+
- Assert semantic output first, with tree, terminal, pixel, and screenshot snapshots when needed
|
|
32
|
+
- Keep timing deterministic with a session-scoped virtual clock and explicit frame settling
|
|
33
|
+
- Test with Minitest assertions or RSpec matchers
|
|
34
|
+
- Capture screenshots, hit regions, text runs, and recent events when a test fails
|
|
4
35
|
|
|
5
36
|
## Installation
|
|
6
37
|
|
|
7
|
-
Add Syrma to the test group in your `Gemfile`:
|
|
38
|
+
Add Syrma and your test framework to the test group in your `Gemfile`:
|
|
8
39
|
|
|
9
40
|
```ruby
|
|
10
41
|
group :test do
|
|
@@ -13,11 +44,18 @@ group :test do
|
|
|
13
44
|
end
|
|
14
45
|
```
|
|
15
46
|
|
|
16
|
-
Then
|
|
47
|
+
Then install the bundle and verify the test environment:
|
|
17
48
|
|
|
18
|
-
|
|
49
|
+
```sh
|
|
50
|
+
bundle install
|
|
51
|
+
bundle exec syrma doctor
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Syrma requires Ruby 3.1 or later and supports Zaniah `>= 0.2, < 0.6`. RSpec users can replace Minitest with RSpec in the test group.
|
|
55
|
+
|
|
56
|
+
## Quick start
|
|
19
57
|
|
|
20
|
-
Keep window creation separate from the code that mounts
|
|
58
|
+
Keep window creation separate from the code that mounts the interface:
|
|
21
59
|
|
|
22
60
|
```ruby
|
|
23
61
|
module Counter
|
|
@@ -32,6 +70,8 @@ module Counter
|
|
|
32
70
|
end
|
|
33
71
|
```
|
|
34
72
|
|
|
73
|
+
### Minitest
|
|
74
|
+
|
|
35
75
|
```ruby
|
|
36
76
|
require "syrma/minitest"
|
|
37
77
|
|
|
@@ -43,12 +83,27 @@ class CounterTest < Minitest::Test
|
|
|
43
83
|
end
|
|
44
84
|
|
|
45
85
|
def test_increment
|
|
46
|
-
ui.
|
|
86
|
+
ui.test_id("increment").click
|
|
47
87
|
assert_ui_text "Count: 1"
|
|
48
88
|
end
|
|
49
89
|
end
|
|
50
90
|
```
|
|
51
91
|
|
|
92
|
+
### RSpec
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
require "syrma/rspec"
|
|
96
|
+
|
|
97
|
+
RSpec.describe "Counter", type: :zaniah do
|
|
98
|
+
before { zaniah_session(width: 320, height: 200) { |window| Counter.mount(window) } }
|
|
99
|
+
|
|
100
|
+
it "increments the count" do
|
|
101
|
+
ui.test_id("increment").click
|
|
102
|
+
expect(ui).to have_ui_text("Count: 1")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
52
107
|
`test_id` is provided by Zaniah 0.2 and is safe to use in application code.
|
|
53
108
|
|
|
54
109
|
## API at a glance
|
|
@@ -61,10 +116,22 @@ end
|
|
|
61
116
|
| Window/input | `resize`, `close`, `drop_files`, `feed_terminal` |
|
|
62
117
|
| Synchronize | `settle`, `wait_for`, `advance` |
|
|
63
118
|
| Inspect | `tree`, `texts`, `at`, `pixel`, `screenshot`, `terminal_lines`, `menu`, `tooltip` |
|
|
64
|
-
| Assert |
|
|
119
|
+
| Assert | Text, element, visibility, panels, decorations, background, pixel, tooltip, menu, tree/terminal/image snapshots |
|
|
65
120
|
|
|
66
121
|
Locators are lazy: every operation resolves them against the latest rendered frame. Actions wait for visibility and an unobscured matching event handler, then send events through `Window#input`.
|
|
67
122
|
|
|
123
|
+
Panel and decoration assertions use rendered `test_id` instrumentation, so application code does not need a Syrma or Canopus runtime dependency:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
assert_panel_visible :problems
|
|
127
|
+
assert_panel_badge :problems, 3
|
|
128
|
+
assert_inline_overlay line: 10, text: ": String"
|
|
129
|
+
assert_gutter_marker line: 5, kind: :breakpoint
|
|
130
|
+
assert_line_highlight line: 12, kind: :debug_position
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The corresponding IDs are `syrma:panel:problems`, `syrma:panel:problems:badge`, `syrma:decoration:inline:10`, `syrma:decoration:gutter:5:breakpoint`, and `syrma:decoration:line:12:debug_position`. Lines are zero-based. Instrument the element that was actually laid out and painted; assertions require positive visible bounds. RSpec provides the same names with `have_` in place of `assert_`.
|
|
134
|
+
|
|
68
135
|
## Snapshots and diagnostics
|
|
69
136
|
|
|
70
137
|
```ruby
|
|
@@ -81,24 +148,31 @@ bundle exec syrma snapshots prune --dry-run
|
|
|
81
148
|
bundle exec syrma report
|
|
82
149
|
```
|
|
83
150
|
|
|
84
|
-
## Determinism and
|
|
151
|
+
## Determinism and performance
|
|
85
152
|
|
|
86
153
|
The default text renderer uses only Zaniah's bundled Abel font plus files passed in `fonts:`. Add a repository-owned font for non-Latin screenshot tests. `text: :none` is faster but does not draw glyphs.
|
|
87
154
|
|
|
88
155
|
On Ruby 4.0 arm64 macOS, the included 101-element benchmark measured `event_frames: :gesture` at 3.1 ms per click/check with `text: :none` and 7.9 ms with deterministic text; Syrma's tree build plus locator resolution was about 0.28 ms. Run `bundle exec ruby -Ilib bench/click_bench.rb gesture` on the target CI host for relevant numbers.
|
|
89
156
|
|
|
90
|
-
|
|
157
|
+
## Documentation
|
|
158
|
+
|
|
159
|
+
- [Guide](docs/guide.md): application structure, sessions, CI, configuration, and troubleshooting
|
|
160
|
+
- [Recipes](docs/recipes.md): interactions, screenshots, TUI sessions, and multiple windows
|
|
161
|
+
- [Changelog](CHANGELOG.md): release history
|
|
91
162
|
|
|
92
163
|
## Development
|
|
93
164
|
|
|
94
165
|
```sh
|
|
166
|
+
bundle install
|
|
95
167
|
bundle exec rake
|
|
96
168
|
bundle exec ruby -Ilib:test script/test_gesture.rb
|
|
97
169
|
bundle exec rbs -I sig validate
|
|
98
170
|
gem build --strict syrma.gemspec
|
|
99
171
|
```
|
|
100
172
|
|
|
101
|
-
|
|
173
|
+
## Contributing
|
|
174
|
+
|
|
175
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/noxdea/syrma).
|
|
102
176
|
|
|
103
177
|
## License
|
|
104
178
|
|
|
@@ -6,7 +6,7 @@ module Syrma
|
|
|
6
6
|
|
|
7
7
|
module WindowCapture
|
|
8
8
|
attr_reader :testing_root, :testing_clear, :testing_frame
|
|
9
|
-
attr_accessor :testing_lazy_raster
|
|
9
|
+
attr_accessor :testing_lazy_raster, :testing_reduce_motion
|
|
10
10
|
|
|
11
11
|
def on_frame(&callback)
|
|
12
12
|
@testing_on_frame = callback
|
|
@@ -29,8 +29,15 @@ module Syrma
|
|
|
29
29
|
@testing_recorder = recorder
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
def render(element, present: true, **options)
|
|
33
|
-
|
|
32
|
+
def render(element, present: true, **options) = super(element, present: present && !testing_lazy_raster, **options)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module FrameContextCapture
|
|
36
|
+
def theme
|
|
37
|
+
current = super
|
|
38
|
+
return current unless window.respond_to?(:testing_reduce_motion) && window.testing_reduce_motion
|
|
39
|
+
|
|
40
|
+
current.with(motion: current.motion.with(reduced: true))
|
|
34
41
|
end
|
|
35
42
|
end
|
|
36
43
|
|
|
@@ -56,6 +63,7 @@ module Syrma
|
|
|
56
63
|
return if @installed
|
|
57
64
|
|
|
58
65
|
Zaniah::Platform.singleton_class.prepend(BackendOverride)
|
|
66
|
+
Zaniah::FrameContext.prepend(FrameContextCapture) if Zaniah::FrameContext.method_defined?(:theme)
|
|
59
67
|
@installed = true
|
|
60
68
|
end
|
|
61
69
|
|
|
@@ -69,9 +77,10 @@ module Syrma
|
|
|
69
77
|
Thread.current[:syrma_window_capture] = previous
|
|
70
78
|
end
|
|
71
79
|
|
|
72
|
-
def capture(window, lazy_raster:)
|
|
80
|
+
def capture(window, lazy_raster:, reduce_motion:)
|
|
73
81
|
window.extend(WindowCapture) unless window.is_a?(WindowCapture)
|
|
74
82
|
window.testing_lazy_raster = lazy_raster
|
|
83
|
+
window.testing_reduce_motion = reduce_motion
|
|
75
84
|
window.on_frame
|
|
76
85
|
window
|
|
77
86
|
end
|
data/lib/syrma/internals.rb
CHANGED
|
@@ -5,9 +5,9 @@ module Syrma
|
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
7
|
def style(element) = element.instance_variable_get(:@style) || {}
|
|
8
|
-
def background(element) = element
|
|
9
|
-
def border_color(element) = element
|
|
10
|
-
def radius(element) = element
|
|
8
|
+
def background(element) = paint_value(element, :background, :@background)
|
|
9
|
+
def border_color(element) = paint_value(element, :border_color, :@border_color)
|
|
10
|
+
def radius(element) = paint_value(element, :corner_radii, :@radius)
|
|
11
11
|
def tooltip(element) = element.instance_variable_get(:@tooltip)
|
|
12
12
|
def context_menu(element) = element.instance_variable_get(:@context_menu)
|
|
13
13
|
def key(element) = element.instance_variable_get(:@key)
|
|
@@ -22,5 +22,11 @@ module Syrma
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def executor_idle?(executor) = executor.instance_variable_get(:@foreground).empty?
|
|
25
|
+
|
|
26
|
+
def paint_value(element, property, legacy)
|
|
27
|
+
resolved = element.respond_to?(:resolved_style) ? element.resolved_style : nil
|
|
28
|
+
resolved&.[](property) || element.instance_variable_get(legacy) || style(element)[property]
|
|
29
|
+
end
|
|
30
|
+
private_class_method :paint_value
|
|
25
31
|
end
|
|
26
32
|
end
|
data/lib/syrma/minitest.rb
CHANGED
|
@@ -94,6 +94,35 @@ module Syrma
|
|
|
94
94
|
end
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
+
def assert_panel_visible(id, session: ui, timeout: session.timeout)
|
|
98
|
+
test_id = UIAssertions.panel_id(id)
|
|
99
|
+
assert_ui_instrumentation(session, timeout, test_id, "panel #{id.inspect} to be visible", "#{UIAssertions::PREFIX}:panel:")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def assert_panel_badge(id, value, session: ui, timeout: session.timeout)
|
|
103
|
+
test_id = UIAssertions.panel_id(id, badge: true)
|
|
104
|
+
assert_ui_instrumentation(session, timeout, test_id, "panel #{id.inspect} badge to be #{value.inspect}",
|
|
105
|
+
"#{UIAssertions::PREFIX}:panel:", text: value)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def assert_inline_overlay(line:, text:, session: ui, timeout: session.timeout)
|
|
109
|
+
test_id = UIAssertions.decoration_id(:inline, line)
|
|
110
|
+
assert_ui_instrumentation(session, timeout, test_id, "inline overlay on line #{line} with text #{text.inspect}",
|
|
111
|
+
"#{UIAssertions::PREFIX}:decoration:inline:", text: text)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def assert_gutter_marker(line:, kind:, session: ui, timeout: session.timeout)
|
|
115
|
+
test_id = UIAssertions.decoration_id(:gutter, line, kind)
|
|
116
|
+
assert_ui_instrumentation(session, timeout, test_id, "#{kind.inspect} gutter marker on line #{line}",
|
|
117
|
+
"#{UIAssertions::PREFIX}:decoration:gutter:")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def assert_line_highlight(line:, kind:, session: ui, timeout: session.timeout)
|
|
121
|
+
test_id = UIAssertions.decoration_id(:line, line, kind)
|
|
122
|
+
assert_ui_instrumentation(session, timeout, test_id, "#{kind.inspect} line highlight on line #{line}",
|
|
123
|
+
"#{UIAssertions::PREFIX}:decoration:line:")
|
|
124
|
+
end
|
|
125
|
+
|
|
97
126
|
def assert_screenshot(name, session: ui, **options)
|
|
98
127
|
warn "syrma: text: :none does not render text in images" if session.text_mode == :none
|
|
99
128
|
store = Snapshots::Store.new
|
|
@@ -154,6 +183,13 @@ module Syrma
|
|
|
154
183
|
flunk error.message
|
|
155
184
|
end
|
|
156
185
|
|
|
186
|
+
def assert_ui_instrumentation(session, timeout, test_id, expectation, prefix, text: UIAssertions::ANY_TEXT)
|
|
187
|
+
ui_eventually(session, timeout,
|
|
188
|
+
-> { "Expected #{expectation}; observed #{UIAssertions.describe(session, prefix)}" }) do
|
|
189
|
+
UIAssertions.visible?(session, test_id, text: text)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
157
193
|
def snapshot_path(store, name, extension)
|
|
158
194
|
test_file = self.class.instance_method(self.name.to_sym).source_location&.first || "unknown"
|
|
159
195
|
store.path(test_file: test_file, test_class: self.class.name, name: name, ext: extension)
|
data/lib/syrma/rspec.rb
CHANGED
|
@@ -102,6 +102,33 @@ module Syrma
|
|
|
102
102
|
failure_message { |session| "Expected menu items #{labels.inspect} (current: #{session.menu.items.inspect})" }
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
+
{
|
|
106
|
+
have_panel_visible: ->(id) { [UIAssertions.panel_id(id), UIAssertions::ANY_TEXT, "panel #{id.inspect} to be visible", "#{UIAssertions::PREFIX}:panel:"] },
|
|
107
|
+
have_panel_badge: ->(id, value) { [UIAssertions.panel_id(id, badge: true), value, "panel #{id.inspect} badge to be #{value.inspect}", "#{UIAssertions::PREFIX}:panel:"] },
|
|
108
|
+
have_inline_overlay: ->(line:, text:) { [UIAssertions.decoration_id(:inline, line), text, "inline overlay on line #{line} with text #{text.inspect}", "#{UIAssertions::PREFIX}:decoration:inline:"] },
|
|
109
|
+
have_gutter_marker: ->(line:, kind:) { [UIAssertions.decoration_id(:gutter, line, kind), UIAssertions::ANY_TEXT, "#{kind.inspect} gutter marker on line #{line}", "#{UIAssertions::PREFIX}:decoration:gutter:"] },
|
|
110
|
+
have_line_highlight: ->(line:, kind:) { [UIAssertions.decoration_id(:line, line, kind), UIAssertions::ANY_TEXT, "#{kind.inspect} line highlight on line #{line}", "#{UIAssertions::PREFIX}:decoration:line:"] }
|
|
111
|
+
}.each do |name, query|
|
|
112
|
+
::RSpec::Matchers.define name do |*args, **keywords|
|
|
113
|
+
match do |session|
|
|
114
|
+
@test_id, @text, @expectation, @prefix = query.call(*args, **keywords)
|
|
115
|
+
@session = session
|
|
116
|
+
RSpec.eventually(session) { UIAssertions.visible?(session, @test_id, text: @text) }
|
|
117
|
+
end
|
|
118
|
+
match_when_negated do |session|
|
|
119
|
+
@test_id, @text, @expectation, @prefix = query.call(*args, **keywords)
|
|
120
|
+
@session = session
|
|
121
|
+
RSpec.eventually(session) { !UIAssertions.visible?(session, @test_id, text: @text) }
|
|
122
|
+
end
|
|
123
|
+
failure_message do
|
|
124
|
+
"Expected #{@expectation}; observed #{UIAssertions.describe(@session, @prefix)}"
|
|
125
|
+
end
|
|
126
|
+
failure_message_when_negated do
|
|
127
|
+
"Expected #{@expectation} not to match; observed #{UIAssertions.describe(@session, @prefix)}"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
105
132
|
::RSpec::Matchers.define :match_screenshot do |name, **options|
|
|
106
133
|
match do |session|
|
|
107
134
|
store = Snapshots::Store.new
|
data/lib/syrma/session.rb
CHANGED
|
@@ -68,6 +68,7 @@ module Syrma
|
|
|
68
68
|
|
|
69
69
|
def driver = @drivers[@current]
|
|
70
70
|
def session = self
|
|
71
|
+
def virtual_clock? = @clock.is_a?(Clock)
|
|
71
72
|
def method_missing(name, ...) = driver.respond_to?(name) ? driver.public_send(name, ...) : super
|
|
72
73
|
def respond_to_missing?(name, include_all = false) = driver.respond_to?(name, include_all) || super
|
|
73
74
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Syrma
|
|
4
|
+
module UIAssertions
|
|
5
|
+
PREFIX = "syrma"
|
|
6
|
+
ANY_TEXT = Object.new.freeze
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def panel_id(id, badge: false) = [PREFIX, "panel", component(id, "panel id"), badge ? "badge" : nil].compact.join(":")
|
|
11
|
+
|
|
12
|
+
def decoration_id(kind, line, type = nil)
|
|
13
|
+
raise ArgumentError, "line must be a nonnegative integer" unless line.is_a?(Integer) && line >= 0
|
|
14
|
+
|
|
15
|
+
kind = component(kind, "decoration kind")
|
|
16
|
+
type = component(type, "decoration type") unless type.nil?
|
|
17
|
+
[PREFIX, "decoration", kind, line, type].compact.join(":")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def visible?(session, test_id, text: ANY_TEXT)
|
|
21
|
+
nodes(session, test_id).any? do |node|
|
|
22
|
+
node.visible? && (text.equal?(ANY_TEXT) || text_match?(node.content_text, text))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def describe(session, prefix)
|
|
27
|
+
found = session.tree.nodes.select { |node| node.test_id&.start_with?(prefix) }
|
|
28
|
+
return "none" if found.empty?
|
|
29
|
+
|
|
30
|
+
found.first(10).map do |node|
|
|
31
|
+
"#{node.test_id}=#{node.content_text.inspect} (#{node.visible? ? 'visible' : 'hidden'})"
|
|
32
|
+
end.join(", ")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def nodes(session, test_id) = session.tree.nodes.select { |node| node.test_id == test_id }
|
|
36
|
+
|
|
37
|
+
def text_match?(actual, expected)
|
|
38
|
+
expected.is_a?(Regexp) ? expected.match?(actual) : actual == expected.to_s
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def component(value, label)
|
|
42
|
+
valid = value.is_a?(String) || value.is_a?(Symbol)
|
|
43
|
+
value = value.to_s
|
|
44
|
+
valid &&= !value.empty? && !value.include?(":") && !value.match?(/[\x00-\x1f\x7f]/)
|
|
45
|
+
raise ArgumentError, "#{label} must be a nonempty string or symbol without colons or control characters" unless valid
|
|
46
|
+
|
|
47
|
+
value
|
|
48
|
+
end
|
|
49
|
+
private_class_method :component
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/syrma/version.rb
CHANGED
data/lib/syrma/window_driver.rb
CHANGED
|
@@ -9,7 +9,7 @@ module Syrma
|
|
|
9
9
|
def initialize(window, session)
|
|
10
10
|
@window = window
|
|
11
11
|
@session = session
|
|
12
|
-
Instrumentation.capture(window, lazy_raster: session.raster == :lazy)
|
|
12
|
+
Instrumentation.capture(window, lazy_raster: session.raster == :lazy, reduce_motion: session.virtual_clock?)
|
|
13
13
|
@builder = SnapshotBuilder.new
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -162,9 +162,19 @@ module Syrma
|
|
|
162
162
|
def scroll_until(list, found:, step: 100, max: 50)
|
|
163
163
|
target = find(**found)
|
|
164
164
|
max.times do
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
candidates = target.resolve_all
|
|
166
|
+
return target if candidates.any?(&:visible?)
|
|
167
|
+
|
|
168
|
+
container = resolve_node(list)
|
|
169
|
+
candidate = candidates.first
|
|
170
|
+
dy = if candidate && candidate.bounds.y >= container.visible_bounds.y + container.visible_bounds.height
|
|
171
|
+
candidate.bounds.y - container.visible_bounds.y - container.visible_bounds.height + 1
|
|
172
|
+
elsif candidate && candidate.bounds.y + candidate.bounds.height <= container.visible_bounds.y
|
|
173
|
+
candidate.bounds.y + candidate.bounds.height - container.visible_bounds.y - 1
|
|
174
|
+
else
|
|
175
|
+
step
|
|
176
|
+
end
|
|
177
|
+
scroll(list, dy: dy)
|
|
168
178
|
end
|
|
169
179
|
raise ElementNotFound.new(target, tree)
|
|
170
180
|
end
|
data/lib/syrma.rb
CHANGED
|
@@ -10,6 +10,7 @@ require_relative "syrma/clock"
|
|
|
10
10
|
require_relative "syrma/text_systems"
|
|
11
11
|
require_relative "syrma/snapshot"
|
|
12
12
|
require_relative "syrma/query"
|
|
13
|
+
require_relative "syrma/ui_assertions"
|
|
13
14
|
require_relative "syrma/event_log"
|
|
14
15
|
require_relative "syrma/event_codec"
|
|
15
16
|
require_relative "syrma/recorder"
|
data/sig/syrma.rbs
CHANGED
|
@@ -145,6 +145,7 @@ module Syrma
|
|
|
145
145
|
def window: (?title: String?, ?index: Integer?) -> untyped
|
|
146
146
|
def driver: () -> WindowDriver
|
|
147
147
|
def session: () -> Session
|
|
148
|
+
def virtual_clock?: () -> bool
|
|
148
149
|
def settle: () -> Session
|
|
149
150
|
def advance: (Numeric seconds) -> Session
|
|
150
151
|
def wait_for: (?untyped message, ?timeout: Numeric) { () -> untyped } -> untyped
|
|
@@ -258,6 +259,11 @@ module Syrma
|
|
|
258
259
|
def assert_pixel: (Integer x, Integer y, untyped color, **untyped) -> bool
|
|
259
260
|
def assert_tooltip: (String | Regexp expected, **untyped) -> bool
|
|
260
261
|
def assert_menu_items: (Array[String] labels, **untyped) -> bool
|
|
262
|
+
def assert_panel_visible: (String | Symbol id, **untyped) -> bool
|
|
263
|
+
def assert_panel_badge: (String | Symbol id, untyped value, **untyped) -> bool
|
|
264
|
+
def assert_inline_overlay: (line: Integer, text: String | Regexp, **untyped) -> bool
|
|
265
|
+
def assert_gutter_marker: (line: Integer, kind: String | Symbol, **untyped) -> bool
|
|
266
|
+
def assert_line_highlight: (line: Integer, kind: String | Symbol, **untyped) -> bool
|
|
261
267
|
def assert_screenshot: (String name, **untyped) -> bool
|
|
262
268
|
def assert_tree_snapshot: (String name, **untyped) -> bool
|
|
263
269
|
def assert_terminal_snapshot: (String name, **untyped) -> bool
|
metadata
CHANGED
|
@@ -1,28 +1,35 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: syrma
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yudai Takada
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-09-15 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: zaniah
|
|
14
15
|
requirement: !ruby/object:Gem::Requirement
|
|
15
16
|
requirements:
|
|
16
|
-
- - "
|
|
17
|
+
- - ">="
|
|
17
18
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.2
|
|
19
|
+
version: '0.2'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '0.6'
|
|
19
23
|
type: :runtime
|
|
20
24
|
prerelease: false
|
|
21
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
26
|
requirements:
|
|
23
|
-
- - "
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '0.2'
|
|
30
|
+
- - "<"
|
|
24
31
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
32
|
+
version: '0.6'
|
|
26
33
|
description: 'Drive Zaniah windows headlessly: locate elements, send real input events,
|
|
27
34
|
and assert text, layout, pixels, and snapshots.'
|
|
28
35
|
email:
|
|
@@ -61,18 +68,20 @@ files:
|
|
|
61
68
|
- lib/syrma/snapshots/terminal_format.rb
|
|
62
69
|
- lib/syrma/snapshots/tree_format.rb
|
|
63
70
|
- lib/syrma/text_systems.rb
|
|
71
|
+
- lib/syrma/ui_assertions.rb
|
|
64
72
|
- lib/syrma/version.rb
|
|
65
73
|
- lib/syrma/visual/comparator.rb
|
|
66
74
|
- lib/syrma/visual/image.rb
|
|
67
75
|
- lib/syrma/window_driver.rb
|
|
68
76
|
- sig/syrma.rbs
|
|
69
|
-
homepage: https://github.com/
|
|
77
|
+
homepage: https://github.com/noxdea/syrma
|
|
70
78
|
licenses:
|
|
71
79
|
- MIT
|
|
72
80
|
metadata:
|
|
73
|
-
source_code_uri: https://github.com/
|
|
74
|
-
changelog_uri: https://github.com/
|
|
81
|
+
source_code_uri: https://github.com/noxdea/syrma
|
|
82
|
+
changelog_uri: https://github.com/noxdea/syrma/blob/main/CHANGELOG.md
|
|
75
83
|
rubygems_mfa_required: 'true'
|
|
84
|
+
post_install_message:
|
|
76
85
|
rdoc_options: []
|
|
77
86
|
require_paths:
|
|
78
87
|
- lib
|
|
@@ -87,7 +96,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
87
96
|
- !ruby/object:Gem::Version
|
|
88
97
|
version: '0'
|
|
89
98
|
requirements: []
|
|
90
|
-
rubygems_version: 4.
|
|
99
|
+
rubygems_version: 3.4.19
|
|
100
|
+
signing_key:
|
|
91
101
|
specification_version: 4
|
|
92
102
|
summary: UI testing toolkit for applications built with Zaniah
|
|
93
103
|
test_files: []
|