hyper-spec 1.0.alpha1.2 → 1.0.alpha1.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,64 @@
1
+ # don't put this in directory lib/rspec/ as that will cause stack overflow with rails/rspec loads
2
+ module RSpec
3
+ module Expectations
4
+ class ExpectationTarget; end
5
+ module HyperSpecInstanceMethods
6
+ def self.included(base)
7
+ base.include HyperSpec::Helpers
8
+ end
9
+
10
+ def to_on_client(matcher, message = nil, &block)
11
+ evaluate_client.to(matcher, message, &block)
12
+ end
13
+
14
+ alias on_client_to to_on_client
15
+ alias to_then to_on_client
16
+ alias then_to to_on_client
17
+
18
+ def to_on_client_not(matcher, message = nil, &block)
19
+ evaluate_client.not_to(matcher, message, &block)
20
+ end
21
+
22
+ alias on_client_to_not to_on_client_not
23
+ alias on_client_not_to to_on_client_not
24
+ alias to_not_on_client to_on_client_not
25
+ alias not_to_on_client to_on_client_not
26
+ alias then_to_not to_on_client_not
27
+ alias then_not_to to_on_client_not
28
+ alias to_not_then to_on_client_not
29
+ alias not_to_then to_on_client_not
30
+
31
+ private
32
+
33
+ def evaluate_client
34
+ source = add_opal_block(@args_str, @target)
35
+ value = @target.binding.eval("evaluate_ruby(#{source.inspect}, {}, {})")
36
+ ExpectationTarget.for(value, nil)
37
+ end
38
+ end
39
+
40
+ class OnClientWithArgsTarget
41
+ include HyperSpecInstanceMethods
42
+
43
+ def initialize(target, args)
44
+ unless args.is_a? Hash
45
+ raise ExpectationNotMetError,
46
+ "You must pass a hash of local var, value pairs to the 'with' modifier"
47
+ end
48
+
49
+ @target = target
50
+ @args_str = args.collect do |name, value|
51
+ set_local_var(name, value)
52
+ end.join("\n")
53
+ end
54
+ end
55
+
56
+ class BlockExpectationTarget < ExpectationTarget
57
+ include HyperSpecInstanceMethods
58
+
59
+ def with(args)
60
+ OnClientWithArgsTarget.new(@target, args)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,221 @@
1
+ module HyperSpec
2
+ module Helpers
3
+ include Internal::ClientExecution
4
+ include Internal::Controller
5
+ include Internal::ComponentMount
6
+ include Internal::CopyLocals
7
+ include Internal::WindowSizing
8
+
9
+ ##
10
+ # Mount a component on a page, with a full execution environment
11
+ # i.e. `mount('MyComponent')` will mount MyComponent on the page.
12
+
13
+ # The params argument is a hash of parameters to be passed to the
14
+ # component.
15
+ # i.e. `mount('MyComponent', title: 'hello')`
16
+
17
+ # The options parameters can set things like:
18
+ # + controller: the controller class, defaults to HyperSpecTestController
19
+ # + no_wait: do not wait for any JS to finish executing before proceeding with the spec
20
+ # + render_on: :client_only (default), :client_and_server, or :server_only
21
+ # + style_sheet: style sheet file defaults to 'application'
22
+ # + javascript: javascript file defaults to 'application'
23
+ # + layout: if provided will use the specified layout otherwise no layout is used
24
+ # Note that if specifying options the params will have to inclosed in their
25
+ # own hash.
26
+ # i.e. `mount('MyComponent', { title: 'hello' }, render_on: :server_only)`
27
+ # The options can be specified globally using the client_options method (see below.)
28
+
29
+ # You may provide a block to mount. This block will be executed on the client
30
+ # before mounting the component. This is useful for setting up test
31
+ # components or modifying a components behavior.
32
+ # i.e.
33
+ # ```ruby
34
+ # mount('MyComponent', title: 'hello') do
35
+ # # this line will be printed on the client console
36
+ # puts "I'm about to mount my component!"
37
+ # end
38
+ # ```
39
+ def mount(component_name = nil, params = nil, opts = {}, &block)
40
+ unless params
41
+ params = opts
42
+ opts = {}
43
+ end
44
+ internal_mount(component_name, params, client_options(opts), &block)
45
+ end
46
+
47
+ ##
48
+ # The following methods retrieve callback and event responses from
49
+ # the mounted components. The history methods contain the array of all
50
+ # responses, while last_... returns the last response.
51
+ # i.e. event_history_for(:save) would return any save events
52
+ # that the component has raised.
53
+
54
+ %i[
55
+ callback_history_for last_callback_for clear_callback_history_for
56
+ event_history_for last_event_for clear_event_history_for
57
+ ].each do |method|
58
+ define_method(method) do |event_name|
59
+ evaluate_ruby(
60
+ "Hyperstack::Internal::Component::TopLevelRailsComponent.#{method}('#{event_name}')"
61
+ )
62
+ end
63
+ end
64
+
65
+ ##
66
+ # Define a code block to be prefixed to the mount code.
67
+ # Useful in before(:each) blocks.
68
+
69
+ # In legacy code this was called `on_client`. To get the legacy
70
+ # behavior alias on_client before_mount
71
+ # but be aware that on_client is now by default the method
72
+ # for executing a block of code on the client which was called
73
+ # evaluate_ruby
74
+
75
+ def before_mount(&block)
76
+ @_hyperspec_private_client_code =
77
+ "#{@_hyperspec_private_client_code}#{add_opal_block('', block)}"
78
+ end
79
+
80
+ # Execute the block both on the client and on the server. Useful
81
+ # for mocking isomorphic classes such as ActiveRecord models.
82
+
83
+ def isomorphic(&block)
84
+ yield
85
+ before_mount(&block)
86
+ end
87
+
88
+ # Allows options to the mount method to be specified globally
89
+
90
+ def client_option(opts = {})
91
+ @_hyperspec_private_client_options ||= { arity_check: default_arity_check }
92
+ @_hyperspec_private_client_options.merge! opts
93
+ build_var_inclusion_lists
94
+ @_hyperspec_private_client_options
95
+ end
96
+
97
+ def default_arity_check
98
+ Rails.application.config.opal.arity_check_enabled if defined? Rails
99
+ rescue StandardError
100
+ false
101
+ end
102
+
103
+ alias client_options client_option
104
+
105
+ ##
106
+ # shorthand for mount with no params (which will effectively reload the page.)
107
+ # also aliased as reload_page
108
+ def load_page
109
+ mount
110
+ end
111
+
112
+ alias reload_page load_page
113
+
114
+ ##
115
+ # evaluate a block (or string) on the client
116
+ # on_client(<optional str>, <opts and/or vars>, &block)
117
+ #
118
+ # normal use is to pass a block that will be compiled to the client
119
+ # but if the ruby code can be supplied as a string in the first arg.
120
+
121
+ # opts are passed on to JSON.parse when retrieving the result
122
+ # from the client.
123
+
124
+ # vars is a hash of name: value pairs. Each name will be initialized
125
+ # as a local variable on the client.
126
+
127
+ # example: on_client(x: 12) { x * x } => 144
128
+
129
+ # in legacy code on_client was called before_mount
130
+ # to get legacy on_client behavior you can alias
131
+ # on_client before_mount
132
+
133
+ alias on_client internal_evaluate_ruby
134
+
135
+ # attempt to set the window to a particular size
136
+
137
+ def size_window(width = nil, height = nil)
138
+ hs_internal_resize_to(*determine_size(width, height))
139
+ rescue StandardError
140
+ true
141
+ end
142
+
143
+ # same signature as on_client, but just returns the compiled
144
+ # js code. Useful for debugging suspected issues with the
145
+ # Opal compiler, etc.
146
+
147
+ def to_js(*args, &block)
148
+ opal_compile(*process_params(*args, &block))
149
+ end
150
+
151
+ # legacy methods for backwards compatibility
152
+ # these may be removed in a future version
153
+
154
+ def expect_evaluate_ruby(*args, &block)
155
+ expect(evaluate_ruby(*args, &block))
156
+ end
157
+
158
+ alias evaluate_ruby internal_evaluate_ruby
159
+ alias evaluate_promise evaluate_ruby
160
+ alias expect_promise expect_evaluate_ruby
161
+
162
+ def run_on_client(&block)
163
+ script = opal_compile(Unparser.unparse(Parser::CurrentRuby.parse(block.source).children.last))
164
+ page.execute_script(script)
165
+ end
166
+
167
+ def insert_html(str)
168
+ @_hyperspec_private_html_block = "#{@_hyperspec_private_html_block}\n#{str}"
169
+ end
170
+
171
+ def ppr(str)
172
+ js = opal_compile(str)
173
+ execute_script("console.log(#{js})")
174
+ end
175
+
176
+ def debugger
177
+ `debugger`
178
+ nil
179
+ end
180
+
181
+ def add_class(class_name, style)
182
+ @_hyperspec_private_client_code =
183
+ "#{@_hyperspec_private_client_code}ComponentHelpers.add_class('#{class_name}', #{style})\n"
184
+ end
185
+
186
+ def attributes_on_client(model)
187
+ evaluate_ruby("#{model.class.name}.find(#{model.id}).attributes").symbolize_keys
188
+ end
189
+
190
+ ### --- Debugging Helpers ----
191
+
192
+ def pause(message = nil)
193
+ if message
194
+ puts message
195
+ internal_evaluate_ruby "puts #{message.inspect}.to_s + ' (type go() to continue)'"
196
+ end
197
+
198
+ page.evaluate_script('window.hyper_spec_waiting_for_go = true')
199
+
200
+ loop do
201
+ sleep 0.25
202
+ break unless page.evaluate_script('window.hyper_spec_waiting_for_go')
203
+ end
204
+ end
205
+
206
+ def open_in_chrome
207
+ # if ['linux', 'freebsd'].include?(`uname`.downcase)
208
+ # `google-chrome http://#{page.server.host}:#{page.server.port}#{page.current_path}`
209
+ # else
210
+ `open http://#{page.server.host}:#{page.server.port}#{page.current_path}`
211
+ # end
212
+
213
+ loop do
214
+ sleep 1.hour
215
+ end
216
+ end
217
+
218
+ # short hand for use in pry sessions
219
+ alias c? internal_evaluate_ruby
220
+ end
221
+ end
@@ -0,0 +1,94 @@
1
+ module HyperSpec
2
+ module Internal
3
+ module ClientExecution
4
+ def internal_evaluate_ruby(*args, &block)
5
+ insure_page_loaded
6
+ add_promise_execute_and_wait(*process_params(*args, &block))
7
+ end
8
+
9
+ private
10
+
11
+ def add_opal_block(str, block)
12
+ return str unless block
13
+
14
+ source = block.source
15
+ ast = Parser::CurrentRuby.parse(source)
16
+ ast = find_block(ast)
17
+ raise "could not find block within source: #{block.source}" unless ast
18
+
19
+ "#{add_locals(str, block)}\n#{Unparser.unparse ast.children.last}"
20
+ end
21
+
22
+ def add_promise_execute_and_wait(str, opts)
23
+ js = opal_compile(add_promise_wrapper(str))
24
+ page.execute_script("window.hyper_spec_promise_result = false; #{js}")
25
+ Timeout.timeout(Capybara.default_max_wait_time) do
26
+ loop do
27
+ break if page.evaluate_script('!!window.hyper_spec_promise_result')
28
+ page.evaluate_script('!!window.hyper_spec_promise_failed && Opal.Opal.$raise(window.hyper_spec_promise_failed)')
29
+
30
+ sleep 0.25
31
+ end
32
+ end
33
+ JSON.parse(page.evaluate_script('window.hyper_spec_promise_result.$to_json()'), opts).first
34
+ end
35
+
36
+ def add_promise_wrapper(str)
37
+ <<~RUBY
38
+ (#{str}).tap do |r|
39
+ if defined?(Promise) && r.is_a?(Promise)
40
+ r.then { |args| `window.hyper_spec_promise_result = [args]` }
41
+ .fail { |e| `window.hyper_spec_promise_failed = e` }
42
+ else
43
+ #after(0) do
44
+ #puts "setting window.hyper_spec_promise_result = [\#{r}]"
45
+ `window.hyper_spec_promise_result = [r]`
46
+ #end
47
+ end
48
+ end
49
+ RUBY
50
+ end
51
+
52
+ def find_block(node)
53
+ # find a block with the ast tree.
54
+
55
+ return false unless node.class == Parser::AST::Node
56
+ return node if the_node_you_are_looking_for?(node)
57
+
58
+ node.children.each do |child|
59
+ found = find_block(child)
60
+ return found if found
61
+ end
62
+ false
63
+ end
64
+
65
+ def process_params(*args, &block)
66
+ args = ['', *args] if args[0].is_a? Hash
67
+ args = [args[0], {}, args[1] || {}] if args.length < 3
68
+ str, opts, vars = args
69
+ vars.each do |name, value|
70
+ str = "#{name} = #{value.inspect}\n#{str}"
71
+ end
72
+ [add_opal_block(str, block), opts]
73
+ end
74
+
75
+ def the_node_you_are_looking_for?(node)
76
+ # we could also check that the block is going to the right method
77
+ # respond_to?(node.children.first.children[1]) &&
78
+ # method(node.children.first.children[1]) == method(:evaluate_ruby)
79
+ # however that does not work for expect { ... }.on_client_to ...
80
+ # because now the block is being sent to expect... so we could
81
+ # check the above OR node.children.first.children[1] == :expect
82
+ # but what if there are two blocks? on and on...
83
+ node.type == :block &&
84
+ node.children.first.class == Parser::AST::Node &&
85
+ node.children.first.type == :send
86
+ end
87
+
88
+
89
+ def opal_compile(str)
90
+ Opal.hyperspec_compile(str, arity_check: client_options[:arity_check])
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,140 @@
1
+ module HyperSpec
2
+ module Internal
3
+ module ComponentMount
4
+ private
5
+
6
+ TEST_CODE_KEY = 'hyper_spec_prerender_test_code.js'.freeze
7
+
8
+ # rubocop:disable Metrics/MethodLength
9
+ def add_block_with_helpers(component_name, opts, block)
10
+ return unless block || @_hyperspec_private_client_code || component_name.nil?
11
+
12
+ block_with_helpers = <<-RUBY
13
+ module ComponentHelpers
14
+ def self.js_eval(s)
15
+ `eval(s)`
16
+ end
17
+ def self.dasherize(s)
18
+ res = %x{
19
+ s.replace(/[-_\\s]+/g, '-')
20
+ .replace(/([A-Z\\d]+)([A-Z][a-z])/g, '$1-$2')
21
+ .replace(/([a-z\\d])([A-Z])/g, '$1-$2')
22
+ .toLowerCase()
23
+ }
24
+ res
25
+ end
26
+ def self.add_class(class_name, styles={})
27
+ style = styles.collect { |attr, value| "\#{dasherize(attr)}:\#{value}" }.join("; ")
28
+ cs = class_name.to_s
29
+ %x{
30
+ var style_el = document.createElement("style");
31
+ var css = "." + cs + " { " + style + " }";
32
+ style_el.type = "text/css";
33
+ if (style_el.styleSheet){
34
+ style_el.styleSheet.cssText = css;
35
+ } else {
36
+ style_el.appendChild(document.createTextNode(css));
37
+ }
38
+ document.head.appendChild(style_el);
39
+ }
40
+ end
41
+ end
42
+ #{test_dummy}
43
+ #{@_hyperspec_private_client_code}
44
+ #{"#{add_locals('', block)}\n#{Unparser.unparse(Parser::CurrentRuby.parse(block.source).children.last)}" if block}
45
+ RUBY
46
+ @_hyperspec_private_client_code = nil
47
+ opts[:code] = opal_compile(block_with_helpers)
48
+ end
49
+ # rubocop:enable Metrics/MethodLength
50
+
51
+ def build_test_url_for(controller = nil, ping = nil)
52
+ id = ping ? 'ping' : Controller.test_id
53
+ "/#{route_root_for(controller)}/#{id}"
54
+ end
55
+
56
+ def insure_page_loaded(only_if_code_or_html_exists = nil)
57
+ return if only_if_code_or_html_exists && !@_hyperspec_private_client_code && !@_hyperspec_private_html_block
58
+
59
+ # if we are not resetting between examples, or think its mounted
60
+ # then look for Opal, but if we can't find it, then ping to clear and try again
61
+ if !HyperSpec.reset_between_examples? || page.instance_variable_get('@hyper_spec_mounted')
62
+ r = evaluate_script('Opal && true') rescue nil
63
+ return if r
64
+
65
+ page.visit build_test_url_for(nil, true) rescue nil
66
+ end
67
+ load_page
68
+ end
69
+
70
+ def internal_mount(component_name, params, opts, &block)
71
+ # TODO: refactor this
72
+ test_url = build_test_url_for(opts.delete(:controller))
73
+ add_block_with_helpers(component_name, opts, block)
74
+ send_params_to_controller_via_cache(test_url, component_name, params, opts)
75
+ setup_prerendering(opts)
76
+ page.instance_variable_set('@hyper_spec_mounted', false)
77
+ visit test_url
78
+ wait_for_ajax unless opts[:no_wait]
79
+ page.instance_variable_set('@hyper_spec_mounted', true)
80
+ Lolex.init(self, client_options[:time_zone], client_options[:clock_resolution])
81
+ end
82
+
83
+ def prerendering?(opts)
84
+ %i[both server_only].include?(opts[:render_on])
85
+ end
86
+
87
+ def send_params_to_controller_via_cache(test_url, component_name, params, opts)
88
+ component_name ||= 'Hyperstack::Internal::Component::TestDummy' if test_dummy
89
+ Controller.cache_write(
90
+ test_url,
91
+ [component_name, params, @_hyperspec_private_html_block, opts]
92
+ )
93
+ @_hyperspec_private_html_block = nil
94
+ end
95
+
96
+ # test_code_key = "hyper_spec_prerender_test_code.js"
97
+ # if defined? ::Hyperstack::Component
98
+ # @@original_server_render_files ||= ::Rails.configuration.react.server_renderer_options[:files]
99
+ # if opts[:render_on] == :both || opts[:render_on] == :server_only
100
+ # unless opts[:code].blank?
101
+ # ComponentTestHelpers.cache_write(test_code_key, opts[:code])
102
+ # ::Rails.configuration.react.server_renderer_options[:files] = @@original_server_render_files + [test_code_key]
103
+ # ::React::ServerRendering.reset_pool # make sure contexts are reloaded so they dont use code from cache, as the rails filewatcher doesnt look for cache changes
104
+ # else
105
+ # ComponentTestHelpers.cache_delete(test_code_key)
106
+ # ::Rails.configuration.react.server_renderer_options[:files] = @@original_server_render_files
107
+ # ::React::ServerRendering.reset_pool # make sure contexts are reloaded so they dont use code from cache, as the rails filewatcher doesnt look for cache changes
108
+ # end
109
+ # end
110
+ # end
111
+
112
+ def setup_prerendering(opts)
113
+ return unless defined?(::Hyperstack::Component) && prerendering?(opts)
114
+
115
+ @@original_server_render_files ||= ::Rails.configuration.react.server_renderer_options[:files]
116
+ ::Rails.configuration.react.server_renderer_options[:files] = @@original_server_render_files
117
+ if opts[:code].blank?
118
+ Controller.cache_delete(TEST_CODE_KEY)
119
+ else
120
+ Controller.cache_write(TEST_CODE_KEY, opts[:code])
121
+ ::Rails.configuration.react.server_renderer_options[:files] += [TEST_CODE_KEY]
122
+ end
123
+ ::React::ServerRendering.reset_pool
124
+ # make sure contexts are reloaded so they dont use code from cache, as the rails filewatcher
125
+ # doesnt look for cache changes
126
+ end
127
+
128
+ def test_dummy
129
+ return unless defined? ::Hyperstack::Component
130
+
131
+ <<-RUBY
132
+ class Hyperstack::Internal::Component::TestDummy
133
+ include Hyperstack::Component
134
+ render {}
135
+ end
136
+ RUBY
137
+ end
138
+ end
139
+ end
140
+ end