scarpe 0.2.1 → 0.2.2

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.
Files changed (240) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -0
  3. data/.yardopts +11 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +112 -0
  6. data/README.md +31 -24
  7. data/Rakefile +13 -1
  8. data/docs/yard/catscradle.md +44 -0
  9. data/docs/yard/template/default/fulldoc/html/setup.rb +13 -0
  10. data/docs/yard/template/default/layout/html/setup.rb +9 -0
  11. data/examples/background_with_image.rb +16 -0
  12. data/examples/bloopsaphone/working/bronx_army_knife.rb +66 -0
  13. data/examples/bloopsaphone/working/morning_serenity.rb +21 -0
  14. data/examples/bloopsaphone/working/simpsons_theme_song_by_why.rb +6 -4
  15. data/examples/button_go_away.rb +1 -1
  16. data/examples/check.rb +18 -0
  17. data/examples/clear_and_append.rb +24 -0
  18. data/examples/download_and_show_image.rb +28 -0
  19. data/examples/edit_box.rb +3 -5
  20. data/examples/fonts.rb +2 -2
  21. data/examples/get_headers.rb +10 -0
  22. data/examples/highlander.rb +2 -0
  23. data/examples/link.rb +2 -2
  24. data/examples/local_fonts.rb +4 -0
  25. data/examples/local_images.rb +4 -0
  26. data/examples/motion_events.rb +20 -0
  27. data/examples/parse_xl_funnies.rb +58 -0
  28. data/examples/radio/radio.rb +16 -0
  29. data/examples/radio/radio_groups.rb +18 -0
  30. data/examples/radio/radio_same_slot.rb +6 -0
  31. data/examples/ruby_racer.rb +13 -15
  32. data/examples/selfitude.rb +18 -0
  33. data/examples/shapes/shapes_fill.rb +4 -3
  34. data/examples/shoes_school.rb +2 -4
  35. data/examples/show_hide.rb +6 -0
  36. data/examples/skip_ci/change_my_audio_source.rb +21 -0
  37. data/examples/skip_ci/guitar_fretboard.rb +137 -0
  38. data/examples/video.rb +10 -0
  39. data/exe/scarpe +42 -66
  40. data/fonts/Pacifico.ttf +0 -0
  41. data/lacci/Gemfile +22 -0
  42. data/lacci/Gemfile.lock +72 -0
  43. data/lacci/Rakefile +12 -0
  44. data/lacci/lacci.gemspec +37 -0
  45. data/lacci/lib/lacci/scarpe_cli.rb +70 -0
  46. data/lacci/lib/lacci/scarpe_core.rb +21 -0
  47. data/lacci/lib/lacci/version.rb +13 -0
  48. data/lacci/lib/shoes/app.rb +264 -0
  49. data/{lib/scarpe → lacci/lib/shoes}/background.rb +1 -1
  50. data/{lib/scarpe → lacci/lib/shoes}/border.rb +1 -1
  51. data/{lib/scarpe → lacci/lib/shoes}/colors.rb +1 -1
  52. data/lacci/lib/shoes/constants.rb +29 -0
  53. data/{lib/scarpe → lacci/lib/shoes}/display_service.rb +40 -45
  54. data/lacci/lib/shoes/download.rb +123 -0
  55. data/lacci/lib/shoes/log.rb +71 -0
  56. data/lacci/lib/shoes/spacing.rb +9 -0
  57. data/{lib/scarpe → lacci/lib/shoes}/widget.rb +63 -43
  58. data/{lib/scarpe → lacci/lib/shoes/widgets}/alert.rb +3 -3
  59. data/{lib/scarpe → lacci/lib/shoes/widgets}/arc.rb +7 -5
  60. data/{lib/scarpe → lacci/lib/shoes/widgets}/button.rb +3 -3
  61. data/lacci/lib/shoes/widgets/check.rb +28 -0
  62. data/lacci/lib/shoes/widgets/document_root.rb +20 -0
  63. data/{lib/scarpe → lacci/lib/shoes/widgets}/edit_box.rb +10 -5
  64. data/{lib/scarpe → lacci/lib/shoes/widgets}/edit_line.rb +2 -2
  65. data/lacci/lib/shoes/widgets/flow.rb +22 -0
  66. data/lacci/lib/shoes/widgets/font.rb +14 -0
  67. data/{lib/scarpe → lacci/lib/shoes/widgets}/image.rb +3 -7
  68. data/lacci/lib/shoes/widgets/line.rb +18 -0
  69. data/{lib/scarpe → lacci/lib/shoes/widgets}/link.rb +2 -2
  70. data/{lib/scarpe → lacci/lib/shoes/widgets}/list_box.rb +2 -2
  71. data/{lib/scarpe → lacci/lib/shoes/widgets}/para.rb +4 -26
  72. data/lacci/lib/shoes/widgets/radio.rb +35 -0
  73. data/lacci/lib/shoes/widgets/shape.rb +37 -0
  74. data/lacci/lib/shoes/widgets/slot.rb +75 -0
  75. data/{lib/scarpe → lacci/lib/shoes/widgets}/span.rb +2 -2
  76. data/lacci/lib/shoes/widgets/stack.rb +24 -0
  77. data/{lib/scarpe → lacci/lib/shoes/widgets}/star.rb +6 -9
  78. data/lacci/lib/shoes/widgets/subscription_item.rb +60 -0
  79. data/lacci/lib/shoes/widgets/text_widget.rb +51 -0
  80. data/lacci/lib/shoes/widgets/video.rb +15 -0
  81. data/lacci/lib/shoes/widgets.rb +29 -0
  82. data/lacci/lib/shoes.rb +127 -0
  83. data/lacci/test/test_colors.rb +39 -0
  84. data/lacci/test/test_helper.rb +9 -0
  85. data/lacci/test/test_lacci.rb +9 -0
  86. data/lib/scarpe/cats_cradle.rb +249 -0
  87. data/lib/scarpe/evented_assertions.rb +88 -0
  88. data/lib/scarpe/version.rb +1 -1
  89. data/lib/scarpe/wv/alert.rb +3 -2
  90. data/lib/scarpe/wv/app.rb +30 -8
  91. data/lib/scarpe/wv/arc.rb +5 -6
  92. data/lib/scarpe/wv/background.rb +10 -1
  93. data/lib/scarpe/wv/border.rb +5 -3
  94. data/lib/scarpe/wv/button.rb +11 -9
  95. data/lib/scarpe/wv/check.rb +29 -0
  96. data/lib/scarpe/wv/control_interface.rb +14 -20
  97. data/lib/scarpe/wv/control_interface_test.rb +13 -28
  98. data/lib/scarpe/wv/document_root.rb +3 -45
  99. data/lib/scarpe/wv/edit_box.rb +5 -7
  100. data/lib/scarpe/wv/edit_line.rb +2 -2
  101. data/lib/scarpe/wv/flow.rb +10 -20
  102. data/lib/scarpe/wv/font.rb +36 -0
  103. data/lib/scarpe/wv/html.rb +3 -2
  104. data/lib/scarpe/wv/image.rb +7 -2
  105. data/lib/scarpe/wv/line.rb +4 -7
  106. data/lib/scarpe/wv/link.rb +1 -0
  107. data/lib/scarpe/wv/list_box.rb +3 -3
  108. data/lib/scarpe/wv/para.rb +16 -14
  109. data/lib/scarpe/wv/radio.rb +34 -0
  110. data/lib/scarpe/wv/shape.rb +44 -8
  111. data/lib/scarpe/wv/slot.rb +81 -0
  112. data/lib/scarpe/wv/spacing.rb +1 -1
  113. data/lib/scarpe/wv/span.rb +10 -8
  114. data/lib/scarpe/wv/stack.rb +10 -30
  115. data/lib/scarpe/wv/star.rb +11 -12
  116. data/lib/scarpe/wv/subscription_item.rb +50 -0
  117. data/lib/scarpe/wv/video.rb +34 -0
  118. data/lib/scarpe/wv/web_wrangler.rb +238 -58
  119. data/lib/scarpe/wv/webview_local_display.rb +27 -5
  120. data/lib/scarpe/wv/webview_relay_display.rb +18 -119
  121. data/lib/scarpe/wv/webview_relay_util.rb +143 -0
  122. data/lib/scarpe/wv/widget.rb +80 -11
  123. data/lib/scarpe/wv/wv_display_worker.rb +17 -4
  124. data/lib/scarpe/wv.rb +33 -4
  125. data/lib/scarpe/wv_local.rb +1 -1
  126. data/lib/scarpe/wv_relay.rb +1 -1
  127. data/lib/scarpe.rb +3 -32
  128. data/scarpe-components/.gitignore +1 -0
  129. data/scarpe-components/Gemfile +22 -0
  130. data/scarpe-components/README.md +35 -0
  131. data/scarpe-components/Rakefile +12 -0
  132. data/scarpe-components/lib/scarpe/components/base64.rb +29 -0
  133. data/scarpe-components/lib/scarpe/components/file_helpers.rb +65 -0
  134. data/scarpe-components/lib/scarpe/components/modular_logger.rb +113 -0
  135. data/scarpe-components/lib/scarpe/components/print_logger.rb +43 -0
  136. data/{lib/scarpe → scarpe-components/lib/scarpe/components}/promises.rb +102 -35
  137. data/scarpe-components/lib/scarpe/components/segmented_file_loader.rb +170 -0
  138. data/scarpe-components/lib/scarpe/components/unit_test_helpers.rb +217 -0
  139. data/scarpe-components/lib/scarpe/components/version.rb +7 -0
  140. data/scarpe-components/scarpe-components.gemspec +38 -0
  141. data/scarpe-components/test/test_components.rb +9 -0
  142. data/scarpe-components/test/test_helper.rb +23 -0
  143. data/scarpe-components/test/test_promises.rb +260 -0
  144. data/scarpe-components/test/test_segmented_app_files.rb +182 -0
  145. data/{lib/scarpe → spikes}/glibui/widget.rb +2 -2
  146. data/{lib/scarpe → spikes}/glibui.rb +1 -1
  147. data/templates/basic_class_template.erb +1 -1
  148. data/templates/class_template_with_event_bind.erb +1 -1
  149. data/templates/class_template_with_shapes.erb +1 -1
  150. data/templates/webview_template.erb +0 -3
  151. metadata +151 -118
  152. data/examples/fill.rb +0 -25
  153. data/examples/legacy/not_checked/shoes-contrib/basic/class-book.yaml +0 -387
  154. data/examples/legacy/not_checked/shoes-contrib/good/good-clock.rb +0 -51
  155. data/examples/legacy/not_checked/shoes-contrib/good/good-follow.rb +0 -26
  156. data/examples/legacy/not_checked/shoes-contrib/good/good-reminder.rb +0 -174
  157. data/examples/legacy/not_checked/shoes-contrib/good/good-vjot.rb +0 -56
  158. data/examples/legacy/not_checked/shoes-contrib/simple/simple-timer.rb +0 -13
  159. data/examples/legacy/not_checked/shoes-dep-samples/good-clock.rb +0 -51
  160. data/examples/legacy/not_checked/shoes-dep-samples/good-follow.rb +0 -26
  161. data/examples/legacy/not_checked/shoes-dep-samples/good-reminder.rb +0 -174
  162. data/examples/legacy/not_checked/shoes-dep-samples/good-vjot.rb +0 -56
  163. data/examples/legacy/not_checked/shoes-dep-samples/simple-accordion.rb +0 -75
  164. data/examples/legacy/not_checked/shoes-dep-samples/simple-anim-shapes.rb +0 -17
  165. data/examples/legacy/not_checked/shoes-dep-samples/simple-anim-text.rb +0 -13
  166. data/examples/legacy/not_checked/shoes-dep-samples/simple-arc.rb +0 -23
  167. data/examples/legacy/not_checked/shoes-dep-samples/simple-bounce.rb +0 -24
  168. data/examples/legacy/not_checked/shoes-dep-samples/simple-calc.rb +0 -70
  169. data/examples/legacy/not_checked/shoes-dep-samples/simple-chipmunk.rb +0 -26
  170. data/examples/legacy/not_checked/shoes-dep-samples/simple-control-sizes.rb +0 -24
  171. data/examples/legacy/not_checked/shoes-dep-samples/simple-curve.rb +0 -26
  172. data/examples/legacy/not_checked/shoes-dep-samples/simple-dialogs.rb +0 -29
  173. data/examples/legacy/not_checked/shoes-dep-samples/simple-draw.rb +0 -13
  174. data/examples/legacy/not_checked/shoes-dep-samples/simple-editor.rb +0 -28
  175. data/examples/legacy/not_checked/shoes-dep-samples/simple-form.rb +0 -28
  176. data/examples/legacy/not_checked/shoes-dep-samples/simple-form.shy +0 -0
  177. data/examples/legacy/not_checked/shoes-dep-samples/simple-mask.rb +0 -21
  178. data/examples/legacy/not_checked/shoes-dep-samples/simple-menu.rb +0 -31
  179. data/examples/legacy/not_checked/shoes-dep-samples/simple-menu1.rb +0 -35
  180. data/examples/legacy/not_checked/shoes-dep-samples/simple-rubygems.rb +0 -29
  181. data/examples/legacy/not_checked/shoes-dep-samples/simple-slide.rb +0 -45
  182. data/examples/legacy/not_checked/shoes-dep-samples/simple-sphere.rb +0 -28
  183. data/examples/legacy/not_checked/shoes-dep-samples/simple-sqlite3.rb +0 -13
  184. data/examples/legacy/not_checked/shoes-dep-samples/simple-timer.rb +0 -13
  185. data/examples/legacy/not_checked/shoes-dep-samples/simple-video.rb +0 -13
  186. data/examples/legacy/not_checked/simple/anim-text.rb +0 -13
  187. data/examples/legacy/not_checked/simple/arc.rb +0 -23
  188. data/examples/legacy/not_checked/simple/bounce.rb +0 -24
  189. data/examples/legacy/not_checked/simple/chipmunk.rb +0 -26
  190. data/examples/legacy/not_checked/simple/curve.rb +0 -26
  191. data/examples/legacy/not_checked/simple/dialogs.rb +0 -29
  192. data/examples/legacy/not_checked/simple/downloader.rb +0 -40
  193. data/examples/legacy/not_checked/simple/draw.rb +0 -13
  194. data/examples/legacy/not_checked/simple/mask.rb +0 -21
  195. data/examples/legacy/not_checked/simple/slide.rb +0 -45
  196. data/examples/legacy/not_checked/simple/sphere.rb +0 -28
  197. data/lib/constants.rb +0 -5
  198. data/lib/scarpe/app.rb +0 -78
  199. data/lib/scarpe/document_root.rb +0 -20
  200. data/lib/scarpe/fill.rb +0 -23
  201. data/lib/scarpe/flow.rb +0 -19
  202. data/lib/scarpe/line.rb +0 -25
  203. data/lib/scarpe/logger.rb +0 -155
  204. data/lib/scarpe/shape.rb +0 -19
  205. data/lib/scarpe/spacing.rb +0 -9
  206. data/lib/scarpe/stack.rb +0 -70
  207. data/lib/scarpe/text_widget.rb +0 -42
  208. data/lib/scarpe/unit_test_helpers.rb +0 -163
  209. data/lib/scarpe/widgets.rb +0 -30
  210. data/lib/scarpe/wv/fill.rb +0 -30
  211. data/lib/scarpe/wv/shape_helper.rb +0 -44
  212. data/scarpe-0.2.0.gem +0 -0
  213. /data/{lib/scarpe → spikes}/glibui/README.md +0 -0
  214. /data/{lib/scarpe → spikes}/glibui/alert.rb +0 -0
  215. /data/{lib/scarpe → spikes}/glibui/app.rb +0 -0
  216. /data/{lib/scarpe → spikes}/glibui/background.rb +0 -0
  217. /data/{lib/scarpe → spikes}/glibui/border.rb +0 -0
  218. /data/{lib/scarpe → spikes}/glibui/button.rb +0 -0
  219. /data/{lib/scarpe → spikes}/glibui/dimensions.rb +0 -0
  220. /data/{lib/scarpe → spikes}/glibui/document_root.rb +0 -0
  221. /data/{lib/scarpe → spikes}/glibui/edit_box.rb +0 -0
  222. /data/{lib/scarpe → spikes}/glibui/edit_line.rb +0 -0
  223. /data/{lib/scarpe → spikes}/glibui/flow.rb +0 -0
  224. /data/{lib/scarpe → spikes}/glibui/html.rb +0 -0
  225. /data/{lib/scarpe → spikes}/glibui/image.rb +0 -0
  226. /data/{lib/scarpe → spikes}/glibui/link.rb +0 -0
  227. /data/{lib/scarpe → spikes}/glibui/local_display.rb +0 -0
  228. /data/{lib/scarpe → spikes}/glibui/para.rb +0 -0
  229. /data/{lib/scarpe → spikes}/glibui/spacing.rb +0 -0
  230. /data/{lib/scarpe → spikes}/glibui/stack.rb +0 -0
  231. /data/{lib/scarpe → spikes}/glibui/text_widget.rb +0 -0
  232. /data/{lib/scarpe → spikes}/libui/alert.rb +0 -0
  233. /data/{lib/scarpe → spikes}/libui/button.rb +0 -0
  234. /data/{lib/scarpe → spikes}/libui/colors.rb +0 -0
  235. /data/{lib/scarpe → spikes}/libui/core.rb +0 -0
  236. /data/{lib/scarpe → spikes}/libui/flow.rb +0 -0
  237. /data/{lib/scarpe → spikes}/libui/libui.rb +0 -0
  238. /data/{lib/scarpe → spikes}/libui/notepad.md +0 -0
  239. /data/{lib/scarpe → spikes}/libui/para.rb +0 -0
  240. /data/{lib/scarpe → spikes}/libui/stack.rb +0 -0
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "scarpe/components/unit_test_helpers"
4
+ require "scarpe/evented_assertions"
5
+
6
+ require "fiber"
7
+
8
+ module Scarpe::Test
9
+ # We'd like something we can call Shoes widget methods on, such as para.replace.
10
+ # But we'd also like to be able to grab the corresponding display widget and
11
+ # call some of *those* methods.
12
+ class CCProxy
13
+ attr_reader :display
14
+ attr_reader :obj
15
+
16
+ def initialize(obj)
17
+ @obj = obj
18
+ # TODO: how to do this with Webview relay? Proxy object to send a message, maybe?
19
+ @display = ::Shoes::DisplayService.display_service.query_display_widget_for(obj.linkable_id)
20
+ end
21
+
22
+ def method_missing(method, ...)
23
+ if @obj.respond_to?(method)
24
+ self.singleton_class.define_method(method) do |*args, **kwargs, &block|
25
+ @obj.send(method, *args, **kwargs, &block)
26
+ end
27
+ send(method, ...)
28
+ else
29
+ super # raise an exception
30
+ end
31
+ end
32
+
33
+ def respond_to_missing?(method_name, include_private = false)
34
+ @obj.respond_to_missing?(method_name, include_private)
35
+ end
36
+ end
37
+
38
+ # This class defines the CatsCradle DSL. It also holds a "bag of fibers"
39
+ # with promises for when they should next resume.
40
+ class CCInstance
41
+ include Shoes::Log
42
+ include Scarpe::Test::EventedAssertions
43
+ include Scarpe::Test::Helpers
44
+
45
+ def self.instance
46
+ @instance ||= CCInstance.new
47
+ end
48
+
49
+ def initialize
50
+ log_init("CatsCradle")
51
+
52
+ @assertion_data = []
53
+ @assertions_passed = 0
54
+ @assertions_failed = []
55
+
56
+ @waiting_fibers = []
57
+ @event_promises = {}
58
+
59
+ @manager_fiber = Fiber.new do
60
+ loop do
61
+ # A fiber can run briefly and then exit. It can run and then block on an API call.
62
+ # These fibers return promises to indicate to CatsCradle when they can run again.
63
+ # A fiber that is no longer #alive? is assumed to be successfully finished.
64
+ @waiting_fibers.each do |fiber_data|
65
+ next unless fiber_data[:promise].fulfilled?
66
+
67
+ @log.debug("Resuming fiber with value #{fiber_data[:promise].returned_value.inspect}")
68
+ result = fiber_data[:fiber].transfer fiber_data[:promise].returned_value
69
+
70
+ # Dead fibers will be removed later, just leave it
71
+ next unless fiber_data[:fiber].alive?
72
+
73
+ case result
74
+ when ::Scarpe::Promise
75
+ fiber_data[:promise] = result
76
+ else
77
+ raise "Unexpected object returned from Fiber#transfer for still-living Fiber! #{result.inspect}"
78
+ end
79
+ end
80
+
81
+ # Throw out dead fibers or those that will never wake
82
+ @waiting_fibers.select! do |fiber_data|
83
+ fiber_data[:fiber].alive? && !fiber_data[:promise].rejected?
84
+ end
85
+
86
+ # Done with this iteration
87
+ Fiber.yield
88
+ end
89
+ end
90
+ end
91
+
92
+ # If we add "every" events, that's likely to complicate timing and event_promise handling.
93
+ EVENT_TYPES = [:next_heartbeat, :next_redraw]
94
+
95
+ # This needs to be called after the basic display service objects exist
96
+ # and we can find the control interface.
97
+ def event_init
98
+ return if @cc_init_done
99
+
100
+ @cc_init_done = true
101
+
102
+ @control_interface = ::Shoes::DisplayService.display_service.control_interface
103
+ @wrangler = @control_interface.wrangler
104
+
105
+ cc_instance = self # ControlInterface#on_event does an instance eval. We'll reset self with another.
106
+
107
+ @control_interface.on_event(:every_heartbeat) do
108
+ cc_instance.instance_eval do
109
+ p = @event_promises.delete(:next_heartbeat)
110
+ p&.fulfilled!
111
+
112
+ # Give every ready fiber a chance to run once.
113
+ @manager_fiber.resume
114
+ end
115
+ end
116
+
117
+ @control_interface.on_event(:every_redraw) do
118
+ cc_instance.instance_eval do
119
+ p = @event_promises.delete(:next_redraw)
120
+ p&.fulfilled!
121
+
122
+ # Give every ready fiber a chance to run once.
123
+ @manager_fiber.resume
124
+ end
125
+ end
126
+ end
127
+
128
+ def event_promise(event)
129
+ @event_promises[event] ||= ::Scarpe::Promise.new
130
+ end
131
+
132
+ def on_event(event, &block)
133
+ raise "Unknown event type: #{event.inspect}!" unless EVENT_TYPES.include?(event)
134
+
135
+ f = Fiber.new do
136
+ CCInstance.instance.instance_eval(&block)
137
+ end
138
+ @waiting_fibers << { promise: event_promise(event), fiber: f }
139
+ end
140
+
141
+ # What to do about TextWidgets? Link, code, em, strong?
142
+ # Also, wait, what's up with span? What *is* that?
143
+ Shoes::Widget.widget_classes.each do |widget_class|
144
+ finder_name = widget_class.dsl_name
145
+
146
+ define_method(finder_name) do |*args|
147
+ app = Shoes::App.instance
148
+
149
+ widgets = app.find_widgets_by(widget_class, *args)
150
+ raise "Found more than one #{finder_name} matching #{args.inspect}!" if widgets.size > 1
151
+ raise "Found no #{finder_name} matching #{args.inspect}!" if widgets.empty?
152
+
153
+ CCProxy.new(widgets[0])
154
+ end
155
+ end
156
+
157
+ def proxy_for(shoes_widget)
158
+ CCProxy.new(shoes_widget)
159
+ end
160
+
161
+ def wait(promise)
162
+ raise("Must supply a promise to wait!") unless promise.is_a?(::Scarpe::Promise)
163
+
164
+ # Wait until this promise is complete before running again
165
+ @manager_fiber.transfer(promise)
166
+ end
167
+
168
+ # This returns a promise, which can be waited on using wait()
169
+ def fully_updated
170
+ @wrangler.promise_dom_fully_updated
171
+ end
172
+
173
+ def dom_html(timeout: 1.0)
174
+ query_js_value("document.getElementById('wrapper-wvroot').innerHTML", timeout:)
175
+ end
176
+
177
+ def query_js_value(js_code, timeout: 1.0)
178
+ js_promise = @wrangler.eval_js_async(js_code, timeout:)
179
+
180
+ # This promise will return the string, so we can just pass it to #transfer
181
+ @manager_fiber.transfer(js_promise)
182
+ end
183
+
184
+ def assert(value, msg = nil)
185
+ msg ||= "Assertion #{value ? "succeeded" : "failed"}"
186
+ @assertion_data << [value ? true : false, msg]
187
+ if value
188
+ @assertions_passed += 1
189
+ else
190
+ @assertions_failed << msg
191
+ end
192
+ end
193
+
194
+ def assert_equal(expected, actual, msg = nil)
195
+ msg ||= "Expected #{actual.inspect} to equal #{expected.inspect}!"
196
+ assert actual == expected, msg
197
+ end
198
+
199
+ def assertion_data_as_a_struct
200
+ {
201
+ still_pending: 0,
202
+ succeeded: @assertions_passed,
203
+ failed: @assertions_failed.size,
204
+ failures: @assertions_failed,
205
+ }
206
+ end
207
+
208
+ def test_metadata
209
+ {}
210
+ end
211
+
212
+ def test_finished
213
+ if !@assertions_failed.empty?
214
+ return_results(false, "Assertions failed", assertion_data_as_a_struct)
215
+ elsif @assertions_passed > 0
216
+ return_results(true, "All assertions passed", assertion_data_as_a_struct)
217
+ else
218
+ return_results(true, "Test finished successfully")
219
+ end
220
+ ::Shoes::DisplayService.dispatch_event("destroy", nil)
221
+ end
222
+ end
223
+
224
+ # "Cat's Cradle" is a children's game where they interlace string between
225
+ # their fingers to make beautiful complicated shapes. The interlacing
226
+ # of fibers made it a good name for a prototype.
227
+
228
+ # An attempt at an experimental Fiber-based testing system to deal with
229
+ # Shoes, Display and JS all at the same time.
230
+ #
231
+ # In general, we'll use Fiber.transfer to bounce control back and forth
232
+ # between the evented implementations (e.g. waiting for redraw) that
233
+ # need to return control to Webview, and the procedural test flows
234
+ # that look far better if we don't do that explicitly.
235
+ #
236
+ # Ruby Fiber basic docs: https://ruby-doc.org/core-3.0.0/Fiber.html
237
+ #
238
+ # This module is mixed into Shoes::App if we're running CatsCradle-based tests
239
+ module CatsCradle
240
+ def event_init
241
+ @cc_instance = CCInstance.instance
242
+ @cc_instance.event_init
243
+ end
244
+
245
+ def on_heartbeat(&block)
246
+ @cc_instance.on_event(:next_heartbeat, &block)
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tempfile"
4
+ require "json"
5
+ require "fileutils"
6
+
7
+ module Scarpe::Test; end
8
+
9
+ # We need a separate assertion system for the kind of Scarpe
10
+ # testing where we start a subprocess, perform the assertion
11
+ # logic in the subprocess, and then run Minitest in the parent.
12
+ # It's an unusual setup.
13
+ module Scarpe::Test::EventedAssertions
14
+ # Assert that `text` includes `subtext`.
15
+ #
16
+ # @param text [String] the longer text
17
+ # @param subtext [String] the text that is asserted to be included
18
+ # @param msg [String,nil] if supplied, the failure message for the assertion
19
+ # @return [void]
20
+ def assert_include(text, subtext, msg = nil)
21
+ msg ||= "Expected #{text.inspect} to include #{subtext.inspect}"
22
+ assert text.include?(subtext), msg
23
+ end
24
+
25
+ # Assert that `text` does not include `subtext`.
26
+ #
27
+ # @param text [String] the longer text
28
+ # @param subtext [String] the text that is asserted to not be included
29
+ # @param msg [String,nil] if supplied, the failure message for the assertion
30
+ # @return [void]
31
+ def assert_not_include(text, subtext, msg = nil)
32
+ msg ||= "Expected #{text.inspect} not to include #{subtext.inspect}"
33
+ assert !text.include?(subtext), msg
34
+ end
35
+
36
+ # Assert that `actual_html` is the same as `expected_tag` with `opts`.
37
+ # This uses Scarpe's HTML tag-based renderer to render the tag and options
38
+ # into text, and valides that the text is the same.
39
+ #
40
+ # @see Scarpe::HTML.render
41
+ #
42
+ # @param actual_html [String] the html to compare to
43
+ # @param expected_tag [String,Symbol] the HTML tag, used to send a method call
44
+ # @param opts keyword options passed to the tag method call
45
+ # @yield block passed to the tag method call.
46
+ # @return [void]
47
+ def assert_html(actual_html, expected_tag, **opts, &block)
48
+ expected_html = Scarpe::HTML.render do |h|
49
+ h.public_send(expected_tag, opts, &block)
50
+ end
51
+
52
+ assert_equal expected_html, actual_html
53
+ end
54
+
55
+ # This does a final return of results. If it gets called
56
+ # multiple times, the test fails because that's not allowed.
57
+ #
58
+ # @param result_bool [Boolean] true if the results are success, false if failure
59
+ # @param msg [String] the message included with the results
60
+ # @param data [Hash] any additional data to pass with the results
61
+ # @return void
62
+ def return_results(result_bool, msg, data = {})
63
+ result_file = ENV["SCARPE_TEST_RESULTS"] || "./scarpe_results.txt"
64
+
65
+ result_structs = [result_bool, msg, data.merge(test_metadata)]
66
+ if File.exist?(result_file)
67
+ results_returned = JSON.parse File.read(result_file)
68
+ end
69
+
70
+ # Multiple different sets of results is bad, even if both are passing.
71
+ if results_returned && results_returned[0..1] != result_structs[0..1]
72
+ # Just raising here doesn't reliably fail the test.
73
+ # See: https://github.com/scarpe-team/scarpe/issues/212
74
+ Shoes::Log.logger("Test Results").error("Writing multi-result failure file to #{result_file.inspect}!")
75
+
76
+ new_res_data = { first_result: results_returned, second_result: result_structs }.merge(test_metadata)
77
+ bad_result = [false, "Returned two sets of results!", new_res_data]
78
+ File.write(result_file, JSON.pretty_generate(bad_result))
79
+
80
+ return
81
+ elsif results_returned
82
+ Shoes::Log.logger("Test Results").warn "Returning identical results twice: #{results_returned.inspect}"
83
+ end
84
+
85
+ Shoes::Log.logger("Test Results").debug("Writing results file #{result_file.inspect} to disk!")
86
+ File.write(result_file, JSON.pretty_generate(result_structs))
87
+ end
88
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Scarpe
4
- VERSION = "0.2.1"
4
+ VERSION = "0.2.2"
5
5
  end
@@ -23,8 +23,9 @@ class Scarpe
23
23
  end
24
24
  end
25
25
 
26
- private
26
+ protected
27
27
 
28
+ # If the whole widget is hidden, the parent style adds display:none
28
29
  def overlay_style
29
30
  {
30
31
  position: "fixed",
@@ -38,7 +39,7 @@ class Scarpe
38
39
  display: "flex",
39
40
  "align-items": "center",
40
41
  "justify-content": "center",
41
- }
42
+ }.merge(style)
42
43
  end
43
44
 
44
45
  def modal_style
data/lib/scarpe/wv/app.rb CHANGED
@@ -3,15 +3,11 @@
3
3
  class Scarpe
4
4
  # Scarpe::WebviewApp must only be used from the main thread, due to GTK+ limitations.
5
5
  class WebviewApp < WebviewWidget
6
- attr_reader :debug
7
6
  attr_reader :control_interface
8
7
 
9
8
  attr_writer :shoes_linkable_id
10
9
 
11
10
  def initialize(properties)
12
- # Is this a thing? Do we care about this?
13
- # opts = @control_interface.app_opts_get_override(opts)
14
-
15
11
  super
16
12
 
17
13
  # It's possible to provide a Ruby script by setting
@@ -24,6 +20,7 @@ class Scarpe
24
20
  # events, specify overrides and so on.
25
21
  @control_interface = ControlInterface.new
26
22
  if ENV["SCARPE_TEST_CONTROL"]
23
+ require "scarpe/components/unit_test_helpers"
27
24
  @control_interface.instance_eval File.read(ENV["SCARPE_TEST_CONTROL"])
28
25
  end
29
26
 
@@ -31,8 +28,9 @@ class Scarpe
31
28
  @view = Scarpe::WebWrangler.new title: @title,
32
29
  width: @width,
33
30
  height: @height,
34
- resizable: @resizable,
35
- debug: @debug
31
+ resizable: @resizable
32
+
33
+ @callbacks = {}
36
34
 
37
35
  # The control interface has to exist to get callbacks like "override Scarpe app opts".
38
36
  # But the Scarpe App needs those options to be created. So we can't pass these to
@@ -50,11 +48,11 @@ class Scarpe
50
48
  scarpe_app = self
51
49
 
52
50
  @view.init_code("scarpeInit") do
53
- @document_root.request_redraw!
51
+ request_redraw!
54
52
  end
55
53
 
56
54
  @view.bind("scarpeHandler") do |*args|
57
- @document_root.handle_callback(*args)
55
+ handle_callback(*args)
58
56
  end
59
57
 
60
58
  @view.bind("scarpeExit") do
@@ -81,5 +79,29 @@ class Scarpe
81
79
  @view = nil
82
80
  end
83
81
  end
82
+
83
+ # All JS callbacks to Scarpe widgets are dispatched
84
+ # via this handler
85
+ def handle_callback(name, *args)
86
+ @callbacks[name].call(*args)
87
+ end
88
+
89
+ # Bind a Scarpe callback name; see handle_callback above.
90
+ # See Scarpe::Widget for how the naming is set up
91
+ def bind(name, &block)
92
+ @callbacks[name] = block
93
+ end
94
+
95
+ # Request a full redraw if Webview is running. Otherwise
96
+ # this is a no-op.
97
+ #
98
+ # @return [void]
99
+ def request_redraw!
100
+ wrangler = WebviewDisplayService.instance.wrangler
101
+ if wrangler.is_running
102
+ wrangler.replace(@document_root.to_html)
103
+ end
104
+ nil
105
+ end
84
106
  end
85
107
  end
data/lib/scarpe/wv/arc.rb CHANGED
@@ -1,10 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "shape_helper"
4
-
5
3
  class Scarpe
6
4
  class WebviewArc < Scarpe::WebviewWidget
7
- include ShapeHelper
8
5
  def initialize(properties)
9
6
  super(properties)
10
7
  end
@@ -20,17 +17,19 @@ class Scarpe
20
17
  end
21
18
  end
22
19
 
23
- private
20
+ protected
24
21
 
25
22
  def style
26
- {
23
+ super.merge({
27
24
  left: "#{@left}px",
28
25
  top: "#{@top}px",
29
26
  width: "#{@width}px",
30
27
  height: "#{@height}px",
31
- }
28
+ })
32
29
  end
33
30
 
31
+ private
32
+
34
33
  def arc_path
35
34
  center_x = @width / 2
36
35
  center_y = @height / 2
@@ -1,13 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "scarpe/components/base64"
4
+
3
5
  class Scarpe
4
6
  module WebviewBackground
7
+ include Components::Base64
8
+
5
9
  def style
6
10
  styles = (super if defined?(super)) || {}
7
11
  return styles unless @background_color
8
12
 
9
- color = if @background_color.is_a?(Range)
13
+ color = case @background_color
14
+ when Array
15
+ "rgba(#{@background_color.join(", ")})"
16
+ when Range
10
17
  "linear-gradient(45deg, #{@background_color.first}, #{@background_color.last})"
18
+ when ->(value) { File.exist?(value) }
19
+ "url(data:image/png;base64,#{encode_file_to_base64(@background_color)})"
11
20
  else
12
21
  @background_color
13
22
  end
@@ -6,12 +6,14 @@ class Scarpe
6
6
  styles = (super if defined?(super)) || {}
7
7
  return styles unless @border_color
8
8
 
9
- border_color = if @border_color.is_a?(Range)
10
- { "border-image": "linear-gradient(45deg, #{@border_color.first}, #{@border_color.last}) 1" }
9
+ border_color = case @border_color
10
+ when Range
11
+ { "border-image": "linear-gradient(45deg, #{@border_color.first}, #{@border_color.last})" }
12
+ when Array
13
+ { "border-color": "rgba(#{@border_color.join(", ")})" }
11
14
  else
12
15
  { "border-color": @border_color }
13
16
  end
14
-
15
17
  styles.merge(
16
18
  "border-style": "solid",
17
19
  "border-width": "#{@options[:strokewidth] || 1}px",
@@ -20,24 +20,26 @@ class Scarpe
20
20
  end
21
21
  end
22
22
 
23
- private
23
+ protected
24
24
 
25
25
  def style
26
- styles = {}
27
- styles["background-color"] = @color
28
- styles["padding-top"] = @padding_top
29
- styles["padding-bottom"] = @padding_bottom
26
+ styles = super
27
+
28
+ styles[:"background-color"] = @color
29
+ styles[:"padding-top"] = @padding_top
30
+ styles[:"padding-bottom"] = @padding_bottom
30
31
  styles[:color] = @text_color
31
32
  styles[:width] = Dimensions.length(@width) if @width
32
33
  styles[:height] = Dimensions.length(@height) if @height
33
- styles["font-size"] = @font_size
34
+ styles[:"font-size"] = @font_size
34
35
 
35
36
  styles[:top] = Dimensions.length(@top) if @top
36
37
  styles[:left] = Dimensions.length(@left) if @left
37
38
  styles[:position] = "absolute" if @top || @left
38
- styles["font-size"] = Dimensions.length(font_size) if @size
39
- styles["font-family"] = @font if @font
40
- styles["color"] = rgb_to_hex(@stroke) if @stroke
39
+ styles[:"font-size"] = Dimensions.length(font_size) if @size
40
+ styles[:"font-family"] = @font if @font
41
+ styles[:color] = rgb_to_hex(@stroke) if @stroke
42
+
41
43
  styles
42
44
  end
43
45
 
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Scarpe
4
+ class WebviewCheck < Scarpe::WebviewWidget
5
+ attr_reader :text
6
+
7
+ def initialize(properties)
8
+ super
9
+
10
+ bind("click") do
11
+ send_self_event(event_name: "click", target: shoes_linkable_id)
12
+ end
13
+ end
14
+
15
+ def properties_changed(changes)
16
+ checked = changes.delete("checked")
17
+
18
+ html_element.toggle_input_button(checked)
19
+
20
+ super
21
+ end
22
+
23
+ def element
24
+ HTML.render do |h|
25
+ h.input(type: :checkbox, id: html_id, onclick: handler_js_code("click"), value: "hmm #{text}", checked: @checked, style:)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,18 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # The ControlInterface is used for testing. It's a way to register interest
4
- # in important events like redraw, init and shutdown, and to override
5
- # test-relevant values like the options to Shoes.app(). Note that no part
6
- # of the Scarpe framework should ever *depend* on ControlInterface. It's
7
- # for testing, not normal operation. If no ControlInterface were ever
8
- # created or called, Scarpe apps should run fine with no modifications.
4
+ # in important events like redraw, init and shutdown, and to configure a
5
+ # Shoes app for testing. Note that no part of the Scarpe framework should
6
+ # ever *depend* on ControlInterface. It's for testing, not normal operation.
7
+ # If no ControlInterface were ever created or called, Scarpe apps should run
8
+ # fine with no modifications.
9
9
  #
10
10
  # And if you depend on this from the framework, I'll add a check-mode that
11
11
  # never dispatches any events to any handlers. Do NOT test me on this.
12
12
 
13
13
  class Scarpe
14
14
  class ControlInterface
15
- include Scarpe::Log
15
+ include Shoes::Log
16
16
 
17
17
  SUBSCRIBE_EVENTS = [:init, :shutdown, :next_redraw, :every_redraw, :next_heartbeat, :every_heartbeat]
18
18
  DISPATCH_EVENTS = [:init, :shutdown, :redraw, :heartbeat]
@@ -33,7 +33,7 @@ class Scarpe
33
33
  "<#ControlInterface>"
34
34
  end
35
35
 
36
- # This should get called once, from Scarpe::App
36
+ # This should get called once, from Shoes::App
37
37
  def set_system_components(app:, doc_root:, wrangler:)
38
38
  unless app && wrangler
39
39
  @log.error("False app passed to set_system_components!") unless app
@@ -79,23 +79,17 @@ class Scarpe
79
79
  # The control interface has overrides for certain settings. If the override has been specified,
80
80
  # those settings will be overridden.
81
81
 
82
- # Override the Shoes app opts like "debug:" and "die_after:" with new ones.
83
- def override_app_opts(new_opts)
84
- @new_app_opts = new_opts
85
- end
86
-
87
- # Called by Scarpe::App to get the override options
88
- def app_opts_get_override(opts)
89
- @new_app_opts || opts
90
- end
91
-
92
82
  # On recognised events, this sets a handler for that event
93
83
  def on_event(event, &block)
94
84
  unless SUBSCRIBE_EVENTS.include?(event)
95
85
  raise "Illegal subscribe to event #{event.inspect}! Valid values are: #{SUBSCRIBE_EVENTS.inspect}"
96
86
  end
97
87
 
98
- @event_handlers[event] << block
88
+ @unsub_id ||= 0
89
+ @unsub_id += 1
90
+
91
+ @event_handlers[event] << { handler: block, unsub: @unsub_id }
92
+ @unsub_id
99
93
  end
100
94
 
101
95
  # Send out the specified event
@@ -145,8 +139,8 @@ class Scarpe
145
139
  private
146
140
 
147
141
  def dumb_dispatch_event(event, *args, **keywords)
148
- @event_handlers[event].each do |handler|
149
- instance_eval(*args, **keywords, &handler)
142
+ @event_handlers[event].each do |data|
143
+ instance_eval(*args, **keywords, &data[:handler])
150
144
  end
151
145
  end
152
146
  end