hyper-spec 1.0.alpha1.5 → 1.0.alpha1.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/console +0 -0
- data/bin/setup +0 -0
- data/hyper-spec.gemspec +13 -10
- data/lib/hyper-spec.rb +162 -28
- data/lib/hyper-spec/controller_helpers.rb +164 -0
- data/lib/hyper-spec/expectations.rb +64 -0
- data/lib/hyper-spec/helpers.rb +221 -0
- data/lib/hyper-spec/internal/client_execution.rb +94 -0
- data/lib/hyper-spec/internal/component_mount.rb +140 -0
- data/lib/hyper-spec/internal/controller.rb +70 -0
- data/lib/hyper-spec/internal/copy_locals.rb +103 -0
- data/lib/hyper-spec/internal/patches.rb +86 -0
- data/lib/hyper-spec/internal/rails_controller_helpers.rb +50 -0
- data/lib/hyper-spec/{time_cop.rb → internal/time_cop.rb} +14 -2
- data/lib/hyper-spec/internal/window_sizing.rb +73 -0
- data/lib/hyper-spec/rack.rb +67 -0
- data/lib/hyper-spec/version.rb +1 -1
- data/lib/hyper-spec/wait_for_ajax.rb +1 -1
- data/multi_level_how_it_works.md +49 -0
- metadata +92 -54
- data/lib/hyper-spec/component_test_helpers.rb +0 -365
- data/lib/hyper-spec/unparser_patch.rb +0 -10
@@ -0,0 +1,70 @@
|
|
1
|
+
module HyperSpec
|
2
|
+
module Internal
|
3
|
+
module Controller
|
4
|
+
class << self
|
5
|
+
attr_accessor :current_example
|
6
|
+
attr_accessor :description_displayed
|
7
|
+
|
8
|
+
def test_id
|
9
|
+
@_hyperspec_private_test_id ||= 0
|
10
|
+
@_hyperspec_private_test_id += 1
|
11
|
+
end
|
12
|
+
|
13
|
+
include ActionView::Helpers::JavaScriptHelper
|
14
|
+
|
15
|
+
def current_example_description!
|
16
|
+
title = "#{title}...continued." if description_displayed
|
17
|
+
self.description_displayed = true
|
18
|
+
"#{escape_javascript(current_example.description)}#{title}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def file_cache
|
22
|
+
@file_cache ||= FileCache.new('cache', '/tmp/hyper-spec-caches', 30, 3)
|
23
|
+
end
|
24
|
+
|
25
|
+
def cache_read(key)
|
26
|
+
file_cache.get(key)
|
27
|
+
end
|
28
|
+
|
29
|
+
def cache_write(key, value)
|
30
|
+
file_cache.set(key, value)
|
31
|
+
end
|
32
|
+
|
33
|
+
def cache_delete(key)
|
34
|
+
file_cache.delete(key)
|
35
|
+
rescue StandardError
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# By default we assume we are operating in a Rails environment and will
|
41
|
+
# hook in using a rails controller. To override this define the
|
42
|
+
# HyperSpecController class in your spec helper. See the rack.rb file
|
43
|
+
# for an example of how to do this.
|
44
|
+
|
45
|
+
def hyper_spec_test_controller
|
46
|
+
return ::HyperSpecTestController if defined?(::HyperSpecTestController)
|
47
|
+
|
48
|
+
base = if defined? ApplicationController
|
49
|
+
Class.new ApplicationController
|
50
|
+
elsif defined? ::ActionController::Base
|
51
|
+
Class.new ::ActionController::Base
|
52
|
+
else
|
53
|
+
raise "Unless using Rails you must define the HyperSpecTestController\n"\
|
54
|
+
'For rack apps try requiring hyper-spec/rack.'
|
55
|
+
end
|
56
|
+
Object.const_set('HyperSpecTestController', base)
|
57
|
+
end
|
58
|
+
|
59
|
+
# First insure we have a controller, then make sure it responds to the test method
|
60
|
+
# if not, then add the rails specific controller methods. The RailsControllerHelpers
|
61
|
+
# module will automatically add a top level route back to the controller.
|
62
|
+
|
63
|
+
def route_root_for(controller)
|
64
|
+
controller ||= hyper_spec_test_controller
|
65
|
+
controller.include RailsControllerHelpers unless controller.method_defined?(:test)
|
66
|
+
controller.route_root
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module HyperSpec
|
2
|
+
module Internal
|
3
|
+
module CopyLocals
|
4
|
+
private
|
5
|
+
|
6
|
+
def build_var_inclusion_lists
|
7
|
+
build_included_list
|
8
|
+
build_excluded_list
|
9
|
+
end
|
10
|
+
|
11
|
+
def build_included_list
|
12
|
+
@_hyperspec_private_included_vars = nil
|
13
|
+
return unless @_hyperspec_private_client_options.key? :include_vars
|
14
|
+
|
15
|
+
included = @_hyperspec_private_client_options[:include_vars]
|
16
|
+
if included.is_a? Symbol
|
17
|
+
@_hyperspec_private_included_vars = [included]
|
18
|
+
elsif included.is_a?(Array)
|
19
|
+
@_hyperspec_private_included_vars = included
|
20
|
+
elsif !included
|
21
|
+
@_hyperspec_private_included_vars = []
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
PRIVATE_VARIABLES = %i[
|
26
|
+
@__inspect_output @__memoized @example @_hyperspec_private_client_code
|
27
|
+
@_hyperspec_private_html_block @fixture_cache
|
28
|
+
@fixture_connections @connection_subscriber @loaded_fixtures
|
29
|
+
@_hyperspec_private_client_options
|
30
|
+
@_hyperspec_private_included_vars
|
31
|
+
@_hyperspec_private_excluded_vars
|
32
|
+
b __ _ _ex_ pry_instance _out_ _in_ _dir_ _file_
|
33
|
+
]
|
34
|
+
|
35
|
+
def build_excluded_list
|
36
|
+
return unless @_hyperspec_private_client_options
|
37
|
+
|
38
|
+
excluded = @_hyperspec_private_client_options[:exclude_vars]
|
39
|
+
if excluded.is_a? Symbol
|
40
|
+
@_hyperspec_private_excluded_vars = [excluded]
|
41
|
+
elsif excluded.is_a?(Array)
|
42
|
+
@_hyperspec_private_excluded_vars = excluded
|
43
|
+
elsif excluded
|
44
|
+
@_hyperspec_private_included_vars = []
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def var_excluded?(var, binding)
|
49
|
+
return true if PRIVATE_VARIABLES.include? var
|
50
|
+
|
51
|
+
excluded = binding.eval('instance_variable_get(:@_hyperspec_private_excluded_vars)')
|
52
|
+
return true if excluded&.include?(var)
|
53
|
+
|
54
|
+
included = binding.eval('instance_variable_get(:@_hyperspec_private_included_vars)')
|
55
|
+
included && !included.include?(var)
|
56
|
+
end
|
57
|
+
|
58
|
+
def add_locals(in_str, block)
|
59
|
+
b = block.binding
|
60
|
+
add_instance_vars(b, add_local_vars(b, add_memoized_vars(b, in_str)))
|
61
|
+
end
|
62
|
+
|
63
|
+
def add_memoized_vars(binding, in_str)
|
64
|
+
memoized = binding.eval('__memoized').instance_variable_get(:@memoized)
|
65
|
+
return in_str unless memoized
|
66
|
+
|
67
|
+
memoized.inject(in_str) do |str, pair|
|
68
|
+
next str if var_excluded?(pair.first, binding)
|
69
|
+
|
70
|
+
"#{str}\n#{set_local_var(pair.first, pair.last)}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def add_local_vars(binding, in_str)
|
75
|
+
binding.local_variables.inject(in_str) do |str, var|
|
76
|
+
next str if var_excluded?(var, binding)
|
77
|
+
|
78
|
+
"#{str}\n#{set_local_var(var, binding.local_variable_get(var))}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def add_instance_vars(binding, in_str)
|
83
|
+
binding.eval('instance_variables').inject(in_str) do |str, var|
|
84
|
+
next str if var_excluded?(var, binding)
|
85
|
+
|
86
|
+
"#{str}\n#{set_local_var(var, binding.eval("instance_variable_get('#{var}')"))}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def set_local_var(name, object)
|
91
|
+
serialized = object.opal_serialize
|
92
|
+
if serialized
|
93
|
+
"#{name} = #{serialized}"
|
94
|
+
else
|
95
|
+
"self.class.define_method(:#{name}) "\
|
96
|
+
"{ raise 'Attempt to access the variable #{name} "\
|
97
|
+
'that was defined in the spec, but its value could not be serialized '\
|
98
|
+
"so it is undefined on the client.' }"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Opal
|
2
|
+
# strips off stuff that confuses things when transmitting to the client
|
3
|
+
# and prints offending code if it can't be compiled
|
4
|
+
def self.hyperspec_compile(str, opts = {})
|
5
|
+
compile(str, opts).gsub("// Prepare super implicit arguments\n", '')
|
6
|
+
.delete("\n").gsub('(Opal);', '(Opal)')
|
7
|
+
# rubocop:disable Lint/RescueException
|
8
|
+
# we are going to reraise it anyway, so its fine to catch EVERYTHING!
|
9
|
+
rescue Exception => e
|
10
|
+
puts "puts could not compile: \n\n#{str}\n\n"
|
11
|
+
raise e
|
12
|
+
end
|
13
|
+
# rubocop:enable Lint/RescueException
|
14
|
+
end
|
15
|
+
|
16
|
+
module Unparser
|
17
|
+
class Emitter
|
18
|
+
# Emitter for send
|
19
|
+
class Send < self
|
20
|
+
def local_variable_clash?
|
21
|
+
selector =~ /^[A-Z]/ ||
|
22
|
+
local_variable_scope.local_variable_defined_for_node?(node, selector)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
module MethodSource
|
29
|
+
class << self
|
30
|
+
alias original_lines_for_before_hyper_spec lines_for
|
31
|
+
alias original_source_helper_before_hyper_spec source_helper
|
32
|
+
|
33
|
+
def source_helper(source_location, name = nil)
|
34
|
+
source_location[1] = 1 if source_location[0] == '(pry)'
|
35
|
+
original_source_helper_before_hyper_spec source_location, name
|
36
|
+
end
|
37
|
+
|
38
|
+
def lines_for(file_name, name = nil)
|
39
|
+
if file_name == '(pry)'
|
40
|
+
HyperSpec.current_pry_code_block
|
41
|
+
else
|
42
|
+
original_lines_for_before_hyper_spec file_name, name
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Object
|
49
|
+
def opal_serialize
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Hash
|
55
|
+
def opal_serialize
|
56
|
+
"{#{collect { |k, v| "#{k.opal_serialize} => #{v.opal_serialize}" }.join(', ')}}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class Array
|
61
|
+
def opal_serialize
|
62
|
+
"[#{collect { |v| v.opal_serialize }.join(', ')}]"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
[FalseClass, Float, Integer, NilClass, String, Symbol, TrueClass].each do |klass|
|
67
|
+
klass.send(:define_method, :opal_serialize) do
|
68
|
+
inspect
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# rubocop:disable Lint/UnifiedInteger - patch for ruby prior to 2.4
|
73
|
+
if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.4.0')
|
74
|
+
[Bignum, Fixnum].each do |klass|
|
75
|
+
klass.send(:define_method, :opal_serialize) do
|
76
|
+
inspect
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
# rubocop:enable Lint/UnifiedInteger
|
81
|
+
|
82
|
+
class Time
|
83
|
+
def to_opal_expression
|
84
|
+
"Time.parse('#{inspect}')"
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module HyperSpec
|
2
|
+
module Internal
|
3
|
+
module RailsControllerHelpers
|
4
|
+
def self.included(base)
|
5
|
+
base.include ControllerHelpers
|
6
|
+
base.include Helpers
|
7
|
+
routes = ::Rails.application.routes
|
8
|
+
routes.disable_clear_and_finalize = true
|
9
|
+
routes.clear!
|
10
|
+
routes.draw { get "/#{base.route_root}/:id", to: "#{base.route_root}#test" }
|
11
|
+
::Rails.application.routes_reloader.paths.each { |path| load(path) }
|
12
|
+
routes.finalize!
|
13
|
+
ActiveSupport.on_load(:action_controller) { routes.finalize! }
|
14
|
+
ensure
|
15
|
+
routes.disable_clear_and_finalize = false
|
16
|
+
end
|
17
|
+
|
18
|
+
module Helpers
|
19
|
+
def ping!
|
20
|
+
head(:no_content)
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def mount_component!
|
25
|
+
@page << '<%= react_component @component_name, @component_params, '\
|
26
|
+
"{ prerender: #{@render_on != :client_only} } %>"
|
27
|
+
end
|
28
|
+
|
29
|
+
def application!(file)
|
30
|
+
@page << "<%= javascript_include_tag '#{file}' %>"
|
31
|
+
end
|
32
|
+
|
33
|
+
def style_sheet!(file)
|
34
|
+
@page << "<%= stylesheet_link_tag '#{file}' %>"
|
35
|
+
end
|
36
|
+
|
37
|
+
def deliver!
|
38
|
+
@render_params[:inline] = @page
|
39
|
+
response.headers['Cache-Control'] = 'max-age=120'
|
40
|
+
response.headers['X-Tracking-ID'] = '123456'
|
41
|
+
render @render_params
|
42
|
+
end
|
43
|
+
|
44
|
+
def server_only?
|
45
|
+
@render_on == :server_only
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -59,6 +59,10 @@ if RUBY_ENGINE == 'opal'
|
|
59
59
|
ticker
|
60
60
|
end
|
61
61
|
|
62
|
+
def init(scale: 1, resolution: 10)
|
63
|
+
update_lolex(Time.now, scale, resolution)
|
64
|
+
end
|
65
|
+
|
62
66
|
def update_lolex(time, scale, resolution)
|
63
67
|
`#{@lolex}.uninstall()` && return if scale.nil?
|
64
68
|
@mock_start_time = time.to_f * 1000
|
@@ -80,6 +84,14 @@ if RUBY_ENGINE == 'opal'
|
|
80
84
|
end
|
81
85
|
end
|
82
86
|
|
87
|
+
# create an alias for Lolex.init so we can say Timecop.init on the client
|
88
|
+
|
89
|
+
class Timecop
|
90
|
+
def self.init(*args)
|
91
|
+
Lolex.init(*args)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
83
95
|
else
|
84
96
|
require 'timecop'
|
85
97
|
|
@@ -136,7 +148,7 @@ else
|
|
136
148
|
|
137
149
|
def evaluate_ruby(&block)
|
138
150
|
if @capybara_page
|
139
|
-
@capybara_page.
|
151
|
+
@capybara_page.internal_evaluate_ruby(yield)
|
140
152
|
else
|
141
153
|
pending_evaluations << block
|
142
154
|
end
|
@@ -144,7 +156,7 @@ else
|
|
144
156
|
|
145
157
|
def run_pending_evaluations
|
146
158
|
return if pending_evaluations.empty?
|
147
|
-
@capybara_page.
|
159
|
+
@capybara_page.internal_evaluate_ruby(pending_evaluations.collect(&:call).join("\n"))
|
148
160
|
@pending_evaluations ||= []
|
149
161
|
end
|
150
162
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module HyperSpec
|
2
|
+
module Internal
|
3
|
+
module WindowSizing
|
4
|
+
private
|
5
|
+
|
6
|
+
STD_SIZES = {
|
7
|
+
small: [480, 320],
|
8
|
+
mobile: [640, 480],
|
9
|
+
tablet: [960, 640],
|
10
|
+
large: [1920, 6000],
|
11
|
+
default: [1024, 768]
|
12
|
+
}
|
13
|
+
|
14
|
+
def determine_size(width, height)
|
15
|
+
width, height = [height, width] if width == :portrait
|
16
|
+
width, height = width if width.is_a? Array
|
17
|
+
portrait = true if height == :portrait
|
18
|
+
width ||= :default
|
19
|
+
width, height = STD_SIZES[width] if STD_SIZES[width]
|
20
|
+
width, height = [height, width] if portrait
|
21
|
+
[width + debugger_width, height]
|
22
|
+
end
|
23
|
+
|
24
|
+
def debugger_width
|
25
|
+
RSpec.configuration.debugger_width ||= begin
|
26
|
+
hs_internal_resize_to(1000, 500) do
|
27
|
+
sleep RSpec.configuration.wait_for_initialization_time
|
28
|
+
end
|
29
|
+
inner_width = evaluate_script('window.innerWidth')
|
30
|
+
1000 - inner_width
|
31
|
+
end
|
32
|
+
RSpec.configuration.debugger_width
|
33
|
+
end
|
34
|
+
|
35
|
+
def hs_internal_resize_to(width, height)
|
36
|
+
Capybara.current_session.current_window.resize_to(width, height)
|
37
|
+
yield if block_given?
|
38
|
+
wait_for_size(width, height)
|
39
|
+
end
|
40
|
+
|
41
|
+
def wait_for_size(width, height)
|
42
|
+
@start_time = Capybara::Helpers.monotonic_time
|
43
|
+
@stable_count_w = @stable_count_h = 0
|
44
|
+
prev_size = [0, 0]
|
45
|
+
loop do
|
46
|
+
sleep 0.05
|
47
|
+
curr_size = evaluate_script('[window.innerWidth, window.innerHeight]')
|
48
|
+
|
49
|
+
return true if curr_size == [width, height] || stalled?(prev_size, curr_size)
|
50
|
+
|
51
|
+
prev_size = curr_size
|
52
|
+
check_time!
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def check_time!
|
57
|
+
if (Capybara::Helpers.monotonic_time - @start_time) >
|
58
|
+
Capybara.current_session.config.default_max_wait_time
|
59
|
+
raise Capybara::WindowError,
|
60
|
+
'Window size not stable within '\
|
61
|
+
"#{Capybara.current_session.config.default_max_wait_time} seconds."
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def stalled?(prev_size, curr_size)
|
66
|
+
# some maximum or minimum is reached and size doesn't change anymore
|
67
|
+
@stable_count_w += 1 if prev_size[0] == curr_size[0]
|
68
|
+
@stable_count_h += 1 if prev_size[1] == curr_size[1]
|
69
|
+
@stable_count_w > 4 || @stable_count_h > 4
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'hyper-spec'
|
2
|
+
|
3
|
+
class HyperSpecTestController < SimpleDelegator
|
4
|
+
include HyperSpec::ControllerHelpers
|
5
|
+
|
6
|
+
class << self
|
7
|
+
attr_reader :sprocket_server
|
8
|
+
attr_reader :asset_path
|
9
|
+
|
10
|
+
def wrap(app:, append_path: 'app', asset_path: '/assets')
|
11
|
+
@sprocket_server = Opal::Sprockets::Server.new do |s|
|
12
|
+
s.append_path append_path
|
13
|
+
end
|
14
|
+
|
15
|
+
@asset_path = asset_path
|
16
|
+
|
17
|
+
::Rack::Builder.app(app) do
|
18
|
+
map "/#{HyperSpecTestController.route_root}" do
|
19
|
+
use HyperSpecTestController
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def sprocket_server
|
26
|
+
self.class.sprocket_server
|
27
|
+
end
|
28
|
+
|
29
|
+
def asset_path
|
30
|
+
self.class.asset_path
|
31
|
+
end
|
32
|
+
|
33
|
+
def ping!
|
34
|
+
[204, {}, []]
|
35
|
+
end
|
36
|
+
|
37
|
+
def application!(file)
|
38
|
+
@page << Opal::Sprockets.javascript_include_tag(
|
39
|
+
file,
|
40
|
+
debug: true,
|
41
|
+
sprockets: sprocket_server.sprockets,
|
42
|
+
prefix: asset_path
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def json!
|
47
|
+
@page << Opal::Sprockets.javascript_include_tag(
|
48
|
+
'json',
|
49
|
+
debug: true,
|
50
|
+
sprockets: sprocket_server.sprockets,
|
51
|
+
prefix: asset_path
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def style_sheet!(_file_); end
|
57
|
+
|
58
|
+
def deliver!
|
59
|
+
[200, { 'Content-Type' => 'text/html' }, [@page]]
|
60
|
+
end
|
61
|
+
|
62
|
+
def call(env)
|
63
|
+
__setobj__(Rack::Request.new(env))
|
64
|
+
params[:id] = path.split('/').last
|
65
|
+
test
|
66
|
+
end
|
67
|
+
end
|