scarpe 0.2.1 → 0.2.2

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