hyper-spec 0.1.0 → 0.1.1

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