hyper-spec 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,368 @@
1
+ # see component_test_helpers_spec.rb for examples
2
+
3
+ require 'parser/current'
4
+ require 'unparser'
5
+ require 'method_source'
6
+ require_relative '../../vendor/assets/javascripts/time_cop' #'hyper-spec/time_cop'
7
+
8
+
9
+ module ComponentTestHelpers
10
+
11
+ def self.compile_to_opal(&block)
12
+ Opal.compile(block.source.split("\n")[1..-2].join("\n"))
13
+ end
14
+
15
+
16
+ TOP_LEVEL_COMPONENT_PATCH = lambda { |&block| Opal.compile(block.source.split("\n")[1..-2].join("\n"))}.call do #ComponentTestHelpers.compile_to_opal do
17
+ module React
18
+ class TopLevelRailsComponent
19
+
20
+ class << self
21
+ attr_accessor :event_history
22
+
23
+ def callback_history_for(proc_name)
24
+ event_history[proc_name]
25
+ end
26
+
27
+ def last_callback_for(proc_name)
28
+ event_history[proc_name].last
29
+ end
30
+
31
+ def clear_callback_history_for(proc_name)
32
+ event_history[proc_name] = []
33
+ end
34
+
35
+ def event_history_for(event_name)
36
+ event_history["_on#{event_name.event_camelize}"]
37
+ end
38
+
39
+ def last_event_for(event_name)
40
+ event_history["_on#{event_name.event_camelize}"].last
41
+ end
42
+
43
+ def clear_event_history_for(event_name)
44
+ event_history["_on#{event_name.event_camelize}"] = []
45
+ end
46
+
47
+ end
48
+
49
+ def component
50
+ return @component if @component
51
+ paths_searched = []
52
+ if params.component_name.start_with? "::"
53
+ paths_searched << params.component_name.gsub(/^\:\:/,"")
54
+ @component = params.component_name.gsub(/^\:\:/,"").split("::").inject(Module) { |scope, next_const| scope.const_get(next_const, false) } rescue nil
55
+ return @component if @component && @component.method_defined?(:render)
56
+ else
57
+ self.class.search_path.each do |path|
58
+ # try each path + params.controller + params.component_name
59
+ paths_searched << "#{path.name + '::' unless path == Module}#{params.controller}::#{params.component_name}"
60
+ @component = "#{params.controller}::#{params.component_name}".split("::").inject(path) { |scope, next_const| scope.const_get(next_const, false) } rescue nil
61
+ return @component if @component && @component.method_defined?(:render)
62
+ end
63
+ self.class.search_path.each do |path|
64
+ # then try each path + params.component_name
65
+ paths_searched << "#{path.name + '::' unless path == Module}#{params.component_name}"
66
+ @component = "#{params.component_name}".split("::").inject(path) { |scope, next_const| scope.const_get(next_const, false) } rescue nil
67
+ return @component if @component && @component.method_defined?(:render)
68
+ end
69
+ end
70
+ @component = nil
71
+ raise "Could not find component class '#{params.component_name}' for params.controller '#{params.controller}' in any component directory. Tried [#{paths_searched.join(", ")}]"
72
+ end
73
+
74
+ before_mount do
75
+ TopLevelRailsComponent.event_history = Hash.new {|h,k| h[k] = [] }
76
+ @render_params = params.render_params
77
+ component.validator.rules.each do |name, rules|
78
+ if rules[:type] == Proc
79
+ TopLevelRailsComponent.event_history[name] = []
80
+ @render_params[name] = lambda { |*args| TopLevelRailsComponent.event_history[name] << args } #args.collect { |arg| Native(arg).to_n } }
81
+ end
82
+ end
83
+ end
84
+
85
+ def render
86
+ present component, @render_params
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ def build_test_url_for(controller)
93
+
94
+ unless controller
95
+ Object.const_set("ReactTestController", Class.new(ApplicationController)) unless defined?(::ReactTestController)
96
+ controller = ::ReactTestController
97
+ end
98
+
99
+ route_root = controller.name.gsub(/Controller$/,"").underscore
100
+
101
+ unless controller.method_defined? :test
102
+ controller.class_eval do
103
+ define_method(:test) do
104
+ route_root = self.class.name.gsub(/Controller$/,"").underscore
105
+ test_params = Rails.cache.read("/#{route_root}/#{params[:id]}")
106
+ @component_name = test_params[0]
107
+ @component_params = test_params[1]
108
+ render_params = test_params[2]
109
+ render_on = render_params.delete(:render_on) || :client_only
110
+ mock_time = render_params.delete(:mock_time)
111
+ style_sheet = render_params.delete(:style_sheet)
112
+ javascript = render_params.delete(:javascript)
113
+ code = render_params.delete(:code)
114
+ page = "<%= react_component @component_name, @component_params, { prerender: #{render_on != :client_only} } %>"
115
+ page = "<script type='text/javascript'>\n#{TOP_LEVEL_COMPONENT_PATCH}\n</script>\n#{page}"
116
+
117
+ if code
118
+ page = "<script type='text/javascript'>\n#{code}\n</script>\n"+page
119
+ end
120
+
121
+ #TODO figure out how to auto insert this line???? something like:
122
+ #page = "<%= javascript_include_tag 'reactrb-router' %>\n#{page}"
123
+
124
+
125
+ if true || Lolex.initialized?
126
+ page = "<%= javascript_include_tag 'time_cop' %>\n"+page
127
+ end
128
+ if (render_on != :server_only && !render_params[:layout]) || javascript
129
+ #page = "<script src='/assets/application.js?ts=#{Time.now.to_f}'></script>\n"+page
130
+ page = "<%= javascript_include_tag '#{javascript || 'application'}' %>\n"+page
131
+ end
132
+ if !render_params[:layout] || style_sheet
133
+ page = "<%= stylesheet_link_tag '#{style_sheet || 'application'}' %>\n"+page
134
+ end
135
+ if render_on == :server_only # so that test helper wait_for_ajax works
136
+ page = "<script type='text/javascript'>window.jQuery = {'active': 0}</script>\n#{page}"
137
+ else
138
+ page = "<%= javascript_include_tag 'jquery' %>\n<%= javascript_include_tag 'jquery_ujs' %>\n#{page}"
139
+ end
140
+ page = "<script type='text/javascript'>go = function() {window.hyper_spec_waiting_for_go = false}</script>\n#{page}"
141
+ title = view_context.escape_javascript(ComponentTestHelpers.current_example.description)
142
+ title = "#{title}...continued." if ComponentTestHelpers.description_displayed
143
+ page = "<script type='text/javascript'>console.log(console.log('%c#{title}','color:green; font-weight:bold; font-size: 200%'))</script>\n#{page}"
144
+ ComponentTestHelpers.description_displayed = true
145
+ render_params[:inline] = page
146
+ render render_params
147
+ end
148
+ end
149
+
150
+ # test_routes = Proc.new do
151
+ # get "/#{route_root}/:id", to: "#{route_root}#test"
152
+ # end
153
+ # Rails.application.routes.eval_block(test_routes)
154
+
155
+ begin
156
+ routes = Rails.application.routes
157
+ routes.disable_clear_and_finalize = true
158
+ routes.clear!
159
+ routes.draw do
160
+ get "/#{route_root}/:id", to: "#{route_root}#test"
161
+ end
162
+ Rails.application.routes_reloader.paths.each{ |path| load(path) }
163
+ routes.finalize!
164
+ ActiveSupport.on_load(:action_controller) { routes.finalize! }
165
+ ensure
166
+ routes.disable_clear_and_finalize = false
167
+ end
168
+ end
169
+
170
+ "/#{route_root}/#{@test_id = (@test_id || 0) + 1}"
171
+
172
+ end
173
+
174
+ def isomorphic(&block)
175
+ yield
176
+ on_client(&block)
177
+ end
178
+
179
+ def evaluate_ruby(str="", opts={}, &block)
180
+ insure_mount
181
+ str = "#{str}\n#{Unparser.unparse Parser::CurrentRuby.parse(block.source).children.last}" if block
182
+ js = Opal.compile(str).gsub("\n","").gsub("(Opal);","(Opal)")
183
+ JSON.parse(evaluate_script("[#{js}].$to_json()"), opts).first
184
+ end
185
+
186
+ def expect_evaluate_ruby(str = '', opts = {}, &block)
187
+ insure_mount
188
+ expect(evaluate_ruby(add_opal_block(str, block), opts))
189
+ end
190
+
191
+ def add_opal_block(str, block)
192
+ # big assumption here is that we are going to follow this with a .to
193
+ # hence .children.first followed by .children.last
194
+ # probably should do some kind of "search" to make this work nicely
195
+ return str unless block
196
+ "#{str}\n"\
197
+ "#{Unparser.unparse Parser::CurrentRuby.parse(block.source).children.first.children.last}"
198
+ end
199
+
200
+ def expect_promise(str = '', opts = {}, &block)
201
+ insure_mount
202
+ str = add_opal_block(str, block)
203
+ str = "#{str}.then { |args| args = [args]; `window.hyper_spec_promise_result = args` }"
204
+ js = Opal.compile(str).gsub("\n","").gsub("(Opal);","(Opal)")
205
+ page.evaluate_script("window.hyper_spec_promise_result = false")
206
+ page.execute_script(js)
207
+ Timeout.timeout(Capybara.default_max_wait_time) do
208
+ loop do
209
+ sleep 0.25
210
+ break if page.evaluate_script("!!window.hyper_spec_promise_result")
211
+ end
212
+ end
213
+ expect(JSON.parse(page.evaluate_script("window.hyper_spec_promise_result.$to_json()"), opts).first)
214
+ end
215
+
216
+ def ppr(str)
217
+ js = Opal.compile(str).gsub("\n","").gsub("(Opal);","(Opal)")
218
+ execute_script("console.log(#{js})")
219
+ end
220
+
221
+
222
+ def on_client(&block)
223
+ @client_code = "#{@client_code}#{Unparser.unparse Parser::CurrentRuby.parse(block.source).children.last}\n"
224
+ end
225
+
226
+ def debugger
227
+ `debugger`
228
+ nil
229
+ end
230
+
231
+ class << self
232
+ attr_accessor :current_example
233
+ attr_accessor :description_displayed
234
+ def display_example_description
235
+ "<script type='text/javascript'>console.log(console.log('%c#{current_example.description}','color:green; font-weight:bold; font-size: 200%'))</script>"
236
+ end
237
+ end
238
+
239
+ def insure_mount
240
+ # rescue in case page is not defined...
241
+ mount unless page.instance_variable_get("@hyper_spec_mounted") #rescue nil
242
+ end
243
+
244
+ def client_option(opts = {})
245
+ @client_options ||= {}
246
+ @client_options.merge! opts
247
+ end
248
+
249
+ alias client_options client_option
250
+
251
+ def mount(component_name = nil, params = nil, opts = {}, &block)
252
+ unless params
253
+ params = opts
254
+ opts = {}
255
+ end
256
+ opts = client_options opts
257
+ test_url = build_test_url_for(opts.delete(:controller))
258
+ if block || @client_code || component_name.nil?
259
+ block_with_helpers = <<-code
260
+ module ComponentHelpers
261
+ def self.js_eval(s)
262
+ `eval(s)`
263
+ end
264
+ def self.dasherize(s)
265
+ `s.replace(/[-_\\s]+/g, '-')
266
+ .replace(/([A-Z\\d]+)([A-Z][a-z])/g, '$1-$2')
267
+ .replace(/([a-z\\d])([A-Z])/g, '$1-$2')
268
+ .toLowerCase()`
269
+ end
270
+ def self.add_class(class_name, styles={})
271
+ style = styles.collect { |attr, value| "\#{dasherize(attr)}:\#{value}"}.join("; ")
272
+ s = "<style type='text/css'> .\#{class_name}{ \#{style} } </style>"
273
+ `$(\#{s}).appendTo("head");`
274
+ end
275
+ end
276
+ class React::Component::HyperTestDummy < React::Component::Base
277
+ def render; end
278
+ end
279
+ #{@client_code}
280
+ #{Unparser.unparse(Parser::CurrentRuby.parse(block.source).children.last) if block}
281
+ code
282
+ opts[:code] = Opal.compile(block_with_helpers)
283
+ end
284
+ component_name ||= 'React::Component::HyperTestDummy'
285
+ Rails.cache.write(test_url, [component_name, params, opts])
286
+ visit test_url
287
+ wait_for_ajax unless opts[:no_wait]
288
+ page.instance_variable_set("@hyper_spec_mounted", true)
289
+ Lolex.init(self, client_options[:time_zone], client_options[:clock_resolution])
290
+ end
291
+
292
+ [:callback_history_for, :last_callback_for, :clear_callback_history_for, :event_history_for, :last_event_for, :clear_event_history_for].each do |method|
293
+ define_method(method) { |event_name| evaluate_ruby("React::TopLevelRailsComponent.#{method}('#{event_name}')") }
294
+ end
295
+
296
+ def run_on_client(&block)
297
+ script = Opal.compile(Unparser.unparse Parser::CurrentRuby.parse(block.source).children.last)
298
+ execute_script(script)
299
+ end
300
+
301
+ def add_class(class_name, style)
302
+ @client_code = "#{@client_code}ComponentHelpers.add_class '#{class_name}', #{style}\n"
303
+ end
304
+
305
+ def open_in_chrome
306
+ if false && ['linux', 'freebsd'].include?(`uname`.downcase)
307
+ `google-chrome http://#{page.server.host}:#{page.server.port}#{page.current_path}`
308
+ else
309
+ `open http://#{page.server.host}:#{page.server.port}#{page.current_path}`
310
+ end
311
+ while true
312
+ sleep 1.hour
313
+ end
314
+ end
315
+
316
+ def pause(message = nil)
317
+ if message
318
+ puts message
319
+ page.evaluate_ruby "puts #{message.inspect}.to_s + ' (type go() to continue)'"
320
+ end
321
+ page.evaluate_script("window.hyper_spec_waiting_for_go = true")
322
+ loop do
323
+ sleep 0.25
324
+ break unless page.evaluate_script("window.hyper_spec_waiting_for_go")
325
+ end
326
+ end
327
+
328
+ def size_window(width=nil, height=nil)
329
+ width, height = [height, width] if width == :portrait
330
+ width, height = width if width.is_a? Array
331
+ portrait = true if height == :portrait
332
+ case width
333
+ when :small
334
+ width, height = [480, 320]
335
+ when :mobile
336
+ width, height = [640, 480]
337
+ when :tablet
338
+ width, height = [960, 640]
339
+ when :large
340
+ width, height = [1920, 6000]
341
+ when :default, nil
342
+ width, height = [1024, 768]
343
+ end
344
+ if portrait
345
+ width, height = [height, width]
346
+ end
347
+ if page.driver.browser.respond_to?(:manage)
348
+ page.driver.browser.manage.window.resize_to(width, height)
349
+ elsif page.driver.respond_to?(:resize)
350
+ page.driver.resize(width, height)
351
+ end
352
+ end
353
+
354
+ end
355
+
356
+ RSpec.configure do |config|
357
+ config.before(:each) do |example|
358
+ ComponentTestHelpers.current_example = example
359
+ ComponentTestHelpers.description_displayed = false
360
+ end
361
+ config.before(:all) do
362
+ ActiveRecord::Base.class_eval do
363
+ def attributes_on_client(page)
364
+ page.evaluate_ruby("#{self.class.name}.find(#{id}).attributes", symbolize_names: true)
365
+ end
366
+ end
367
+ end if defined? ActiveRecord
368
+ end
@@ -1,5 +1,4 @@
1
1
  require 'rails'
2
-
3
2
  module HyperSpec
4
3
  module Rails
5
4
  class Engine < ::Rails::Engine
@@ -1,44 +1,113 @@
1
- require 'timecop'
1
+ # Interface to the Lolex package running on the client side
2
+ # Below we will monkey patch Timecop to call these methods
3
+ class Lolex
4
+ class << self
5
+ def init(page, client_time_zone, resolution)
6
+ @capybara_page = page
7
+ @resolution = resolution || 10
8
+ @client_time_zone = client_time_zone
9
+ run_pending_evaluations
10
+ @initialized = true
11
+ end
2
12
 
3
- module HyperSpec
4
- class Timecop
5
- private
13
+ def initialized?
14
+ @initialized
15
+ end
6
16
 
7
- def travel(mock_type, *args, &block)
8
- raise SafeModeException if Timecop.safe_mode? && !block_given?
17
+ def push(mock_type, *args)
18
+ scale = if mock_type == :freeze
19
+ 0
20
+ elsif mock_type == :scale
21
+ args[0]
22
+ else
23
+ 1
24
+ end
25
+ evaluate_ruby do
26
+ "Lolex.push('#{time_string_in_zone}', #{scale}, #{@resolution})"
27
+ end
28
+ end
9
29
 
10
- stack_item = TimeStackItem.new(mock_type, *args)
30
+ def pop
31
+ evaluate_ruby { 'Lolex.pop' }
32
+ end
11
33
 
12
- stack_backup = @_stack.dup
13
- @_stack << stack_item
34
+ def unmock
35
+ evaluate_ruby { "Lolex.unmock('#{time_string_in_zone}', #{@resolution})" }
36
+ end
14
37
 
15
- Lolex.push(mock_type, *args)
38
+ def restore
39
+ evaluate_ruby { 'Lolex.restore' }
40
+ end
41
+
42
+ private
43
+
44
+ def time_string_in_zone
45
+ Time.now.in_time_zone(@client_time_zone).strftime('%Y/%m/%d %H:%M:%S %z')
46
+ end
47
+
48
+ def pending_evaluations
49
+ @pending_evaluations ||= []
50
+ end
16
51
 
17
- if block_given?
18
- begin
19
- yield stack_item.time
20
- ensure
21
- Lolex.pop
22
- @_stack.replace stack_backup
23
- end
52
+ def evaluate_ruby(&block)
53
+ if @capybara_page
54
+ @capybara_page.evaluate_ruby(yield)
55
+ else
56
+ pending_evaluations << block
24
57
  end
25
58
  end
26
59
 
27
- def return(&block)
28
- current_stack = @_stack
29
- current_baseline = @baseline
30
- unmock!
31
- yield
32
- ensure
33
- Lolex.restore
34
- @_stack = current_stack
35
- @baseline = current_baseline
60
+ def run_pending_evaluations
61
+ return if pending_evaluations.empty?
62
+ @capybara_page.evaluate_ruby(pending_evaluations.collect do |block|
63
+ block.call
64
+ end.join("\n"))
65
+ @pending_evaluations ||= []
36
66
  end
67
+ end
68
+ end
69
+
70
+ require 'timecop'
71
+
72
+ # Monkey patches to call our Lolex interface
73
+ class Timecop
74
+
75
+ private
76
+
77
+ def travel(mock_type, *args, &block)
78
+ raise SafeModeException if Timecop.safe_mode? && !block_given?
79
+
80
+ stack_item = TimeStackItem.new(mock_type, *args)
37
81
 
38
- def unmock! #:nodoc:
39
- @baseline = nil
40
- @_stack = []
41
- Lolex.unmock
82
+ stack_backup = @_stack.dup
83
+ @_stack << stack_item
84
+
85
+ Lolex.push(mock_type, *args)
86
+
87
+ if block_given?
88
+ begin
89
+ yield stack_item.time
90
+ ensure
91
+ Lolex.pop
92
+ @_stack.replace stack_backup
93
+ end
42
94
  end
43
95
  end
96
+
97
+ def return(&block)
98
+ current_stack = @_stack
99
+ current_baseline = @baseline
100
+ unmock!
101
+ yield
102
+ ensure
103
+ Lolex.restore
104
+ @_stack = current_stack
105
+ @baseline = current_baseline
106
+ end
107
+
108
+ def unmock! #:nodoc:
109
+ @baseline = nil
110
+ @_stack = []
111
+ Lolex.unmock
112
+ end
44
113
  end