hyper-spec 0.1.0

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,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 catmando
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,378 @@
1
+ # HyperSpec
2
+
3
+ With HyperSpec you can run *isomorphic* specs for all your Hyperloop code using RSpec. Everything runs as standard RSpec test specs.
4
+
5
+ For example if you have a component like this:
6
+
7
+ ```ruby
8
+ class SayHello < React::Component::Base
9
+ param :name
10
+ render(DIV) do
11
+ "Hello there #{params.name}"
12
+ end
13
+ end
14
+ ```
15
+
16
+ Your test spec would look like this:
17
+
18
+ ```ruby
19
+ describe 'SayHello', js: true do
20
+ it 'has the correct content' do
21
+ mount "SayHello", name: 'Fred'
22
+ expect(page).to have_content('Hello there Fred')
23
+ end
24
+ end
25
+ ```
26
+
27
+ The `mount` method will setup a blank client window, and *mount* the named component in the window, passing any parameters.
28
+
29
+ Notice that the spec will need a client environment so we must set `js: true`.
30
+
31
+ The `mount` method can also take a block which will be recompiled and set to the client before mounting the component. You can place any client side code in the mount block including the definition of components.
32
+
33
+ ```ruby
34
+ describe "the mount's code block", js: true do
35
+ it 'will be recompiled on the client' do
36
+ mount 'ShowOff' do
37
+ class ShowOff < React::Component::Base
38
+ render(DIV) { 'Now how cool is that???' }
39
+ end
40
+ end
41
+ expect(page).to have_content('Now how cool is that???' )
42
+ end
43
+ end
44
+ ```
45
+
46
+ ## Why?
47
+
48
+ Hyperloop wants to make the server-client divide as transparent to the developer as practical. Given this, it makes sense that the testing should also be done with as little concern for client versus server.
49
+
50
+ HyperSpec allows you to directly use tools like FactoryGirl (or Hyperloop Operations) to setup some test data, then run a spec to make sure that a component correctly displays, or modifies that data. You can use Timecop to manipulate time and keep in sync between the server and client. This makes testing easier and more realistic without writing a lot of redundant code.
51
+
52
+
53
+ ## Installation
54
+
55
+ Add this line to your application's Gemfile in the test section:
56
+
57
+ ```ruby
58
+ gem 'hyper-spec'
59
+ ```
60
+
61
+ Execute:
62
+
63
+ $ bundle install
64
+
65
+ and then in your spec_helper.rb file
66
+
67
+ ```ruby
68
+ require 'hyper-spec'
69
+ ```
70
+
71
+ You will also need to install selenium, poltergeist and firefox version **46.0.1** (ff latest still does not play well with selenium).
72
+
73
+ Sadly at this time the selenium chrome driver does not play nicely with Opal, so you can't use Chrome. We are working on getting rid of the whole selenium business. Stay tuned.
74
+
75
+ ## Environment Variables
76
+
77
+ You can set `DRIVER` to `ff` to run the client in Firefox and see what is going on. By default tests will run in poltergeist which is quicker, but harder to debug problems.
78
+
79
+ ```
80
+ DRIVER=ff bundle exec rspec
81
+ ```
82
+
83
+ ## Spec Helpers
84
+
85
+ HyperSpec adds the following spec helpers to your test environment
86
+
87
+ + `mount`
88
+ + `client_option` and `client_options`
89
+ + `on_client`
90
+ + `isomorphic`
91
+ + `evaluate_ruby`
92
+ + `expect_evaluate_ruby`
93
+ + `expect_promise`
94
+ + call back and event history methods
95
+ + `pause`
96
+ + `attributes_on_client`
97
+ + `size_window`
98
+ + `add_class`
99
+
100
+ #### The `mount` Method
101
+
102
+ `mount` takes the name of a component, prepares an empty test window, and mounts the named component in the window.
103
+ You may give a block to `mount` which will be recompiled on the client, and run *before* mounting. This means that the component
104
+ mounted may be actually defined in the block, which is useful for setting up top level wrapper components, which will invoke your component under test. You can also modify existing components for white box testing, or local fixture data, constants, etc.
105
+
106
+ `mount` may also be given a hash of the parameters to be passed to the component.
107
+
108
+ ```ruby
109
+ mount 'Display', test: 123 do
110
+ class Display < React::Component::Base
111
+ param :test
112
+ render(DIV) { params.test.to_s }
113
+ end
114
+ end
115
+ ```
116
+
117
+ #### The `client_option` Method
118
+
119
+ There are several options that control the mounting process. Use `client_option` (or `client_options`) before accessing any client side to set any of these options:
120
+
121
+ + `render_on`: `:server_only`, `:client_only`, or `:both`, default is client_only.
122
+ + `layout`: specify the layout to be used. Default is :none.
123
+ + `style_sheet`: specify the name of the style sheet to be loaded. Defaults to the application stylesheet.
124
+ + `javascript`: specify the name of the javascript asset file to be loaded. Defaults to the application js file.
125
+
126
+ For example:
127
+
128
+ ```ruby
129
+ it "can be rendered server side only" do
130
+ client_option render_on: :server_only
131
+ mount 'SayHello', name: 'George'
132
+ expect(page).to have_content('Hello there George')
133
+ # Server only means no code is downloaded to the client
134
+ expect(evaluate_script('typeof React')).to eq('undefined')
135
+ end
136
+ ```
137
+
138
+ If you need to pull in alternative style sheets and javascript files, the recommended way to do this is to
139
+
140
+ 1. Add them to a `specs/assets/stylesheets` and `specs/assets/javascripts` directory and
141
+ 2. Add the following line to your `config/environment/test.rb` file:
142
+ ```ruby
143
+ config.assets.paths << ::Rails.root.join('spec', 'assets', 'stylesheets').to_s
144
+ config.assets.paths << ::Rails.root.join('spec', 'assets', 'javascripts').to_s
145
+ ```
146
+
147
+ This way you will not pollute your application with these 'test only' files.
148
+
149
+ *The javascript spec asset files can be `.rb` files and contain ruby code as well. See the specs for examples!*
150
+
151
+ #### The `on_client` Method
152
+
153
+ `on_client` takes a block and compiles and runs it on the client. This is useful in setting up test constants and client only fixtures.
154
+
155
+ Note that `on_client` needs to *proceed* any calls to `mount`, `evaluate_ruby`, `expect_evaluate_ruby` or `expect_promise` as these methods will initiate the client load process.
156
+
157
+ #### The `isomorphic` Method
158
+
159
+ Similar to `on_client` but the block is *also* run on the server. This is useful for setting constants shared by both client and server, and modifying behavior of isomorphic classes such as ActiveRecord models, and HyperOperations.
160
+
161
+ ```ruby
162
+ isomorphic do
163
+ class SomeModel < ActiveRecord::Base
164
+ def fake_attribute
165
+ 12
166
+ end
167
+ end
168
+ end
169
+ ```
170
+
171
+ #### The `evaluate_ruby` Method
172
+
173
+ Takes either a string or a block, dynamically compiles it, downloads it to the client and runs it.
174
+
175
+ ```ruby
176
+ evaluate_ruby do
177
+ i = 12
178
+ i * 2
179
+ end
180
+ # returns 24
181
+
182
+ isomorphic do
183
+ def factorial(n)
184
+ n == 1 ? 1 : n * factorial(n-1)
185
+ end
186
+ end
187
+
188
+ expect(evaluate_ruby("factorial(5)")).to eq(factorial(5))
189
+ ```
190
+
191
+ `evaluate_ruby` can also be very useful for debug. Set a breakpoint in your test, then use `evaluate_ruby` to interrogate the state of the client.
192
+
193
+ #### The `expect_evaluate_ruby` Method
194
+
195
+ Combines expect and evaluate methods:
196
+
197
+ ```ruby
198
+ expect_evaluate_ruby do
199
+ i = 1
200
+ 5.times { |n| i = i*n }
201
+ i
202
+ end.to eq(120)
203
+ ```
204
+
205
+ #### The `expect_promise` Method
206
+
207
+ Works like `expect_evaluate_ruby` but is used with promises. `expect_promise` will hang until the promise resolves and then return to the results.
208
+
209
+ ```ruby
210
+ expect_promise do
211
+ Promise.new.tap do |p|
212
+ after(2) { p.resolve('hello') }
213
+ end
214
+ end.to eq('hello')
215
+ ```
216
+
217
+ #### Call Back and Event History Methods
218
+
219
+ HyperReact components can *generate* events and perform callbacks. HyperSpec provides methods to test if an event or callback was made.
220
+
221
+ ```ruby
222
+ mount 'CallBackOnEveryThirdClick' do
223
+ class CallBackOnEveryThirdClick < React::Component::Base
224
+ param :click3, type: Proc
225
+ def increment_click
226
+ @clicks ||= 0
227
+ @clicks = (@clicks + 1)
228
+ params.click3(@clicks) if @clicks % 3 == 0
229
+ end
230
+ render do
231
+ DIV(class: :tp_clicker) { "click me" }
232
+ .on(:click) { increment_click }
233
+ end
234
+ end
235
+ end
236
+
237
+ 7.times { page.click('#tp_clicker') }
238
+ expect(callback_history_for(:click3)).to eq([[3], [6]])
239
+ ```
240
+
241
+ Note that for things to work, the param must be declared as a `type: Proc`.
242
+
243
+ + `callback_history_for`: the entire history given as an array of arrays
244
+ + `last_callback_for`: same as `callback_history_for(xxx).last`
245
+ + `clear_callback_history_for`: clears the array (userful for repeating test variations without remounting)
246
+ + `event_history_for, last_event_for, clear_event_history_for`: same but for events.
247
+
248
+ #### The `pause` Method
249
+
250
+ For debugging. Everything stops, until you type `go()` in the client console. Running binding.pry also has this effect, and is often sufficient, however it will also block the server from responding unless you have a multithreaded server.
251
+
252
+ #### The `attributes_on_client` Method
253
+
254
+ *This feature is currently untested - use at your own risk.*
255
+
256
+ This reads the value of active record model attributes on the client.
257
+
258
+ In other words the method `attributes_on_client` is added to all ActiveRecord models. You then take a model you have instance of on the server, and by passing the Capybara page object, you get back the attributes for that same model instance, currently on the client.
259
+
260
+ ```ruby
261
+ expect(some_record_on_server.attributes_on_client(page)[:fred]).to eq(12)
262
+ ```
263
+
264
+ Note that after persisting a record the client and server will be synced so this is mainly useful for debug or in rare cases where it is important to interrogate the value on the client before its persisted.
265
+
266
+ #### The `size_window` Method
267
+
268
+ Sets the size of the test window. You can say:
269
+ `size_window(width, height)` or pass one of the following standard sizes: to one of the following standard sizes:
270
+
271
+ + small: 480 X 320
272
+ + mobile: 640 X 480
273
+ + tablet: 960 X 640
274
+ + large: 1920 X 6000
275
+ + default: 1024 X 768
276
+
277
+ example: `size_window(:mobile)`
278
+
279
+ You can also modify the standard sizes with `:portrait`
280
+
281
+ example: `size_window(:table, :portrait)`
282
+
283
+ You can also specify the size by providing the width and height.
284
+
285
+ example: `size_window(600, 600)`
286
+
287
+ size_window with no parameters is the same as `size_window(:default)`
288
+
289
+ Typically you will use this in a `before(:each)` or `before(:step)` block
290
+
291
+ #### The `add_class` Method
292
+
293
+ Sometimes it's useful to change styles during testing (mainly for debug so that changes on screen are visible.)
294
+
295
+ The `add_class` method takes a class name (as a symbol or string), and hash representing the style.
296
+
297
+ ```ruby
298
+ it "can add classes during testing" do
299
+ add_class :some_class, borderStyle: :solid
300
+ mount 'StyledDiv' do
301
+ class StyledDiv < React::Component::Base
302
+ render(DIV, id: 'hello', class: 'some_class') do
303
+ 'Hello!'
304
+ end
305
+ end
306
+ end
307
+ expect(page.find('#hello').native.css_value('border-right-style')).to eq('solid')
308
+ end
309
+ ```
310
+
311
+ ## Integration with the Steps gem
312
+
313
+ The [rspec-steps gem](https://github.com/LRDesign/rspec-steps) can be useful in doing client side testing. Without rspec-steps, each test spec will cause a reload of the browser window. While this insures that each test runs in a clean environment, it is typically not necessary and can really slow down testing.
314
+
315
+ The rspec-steps gem will run each test without reloading the window, which is usually fine.
316
+
317
+ Checkout the rspec-steps example in the `hyper_spec.rb` file for an example.
318
+
319
+ *Note that hopefully in the near future we are going to build a custom capybara driver that will just directly talk to Hyperloop on the client side. Once this is in place these troubles should go away! - Volunteers welcome to help!*
320
+
321
+ ## Timecop Integration
322
+
323
+ HyperSpec is integrated with [Timecop](https://github.com/travisjeffery/timecop) to freeze, move and speed up time. The client and server times will be kept in sync when you use any these Timecop methods:
324
+
325
+ + `freeze`: Freezes time at the specified point in time (default is Time.now)
326
+ + `travel`: Time runs normally forward from the point specified.
327
+ + `scale`: Like travel but times runs faster.
328
+ + `return`: Return to normal system time.
329
+
330
+ For example:
331
+ ```ruby
332
+ Timecop.freeze # freeze time at current time
333
+ # ... test some stuff
334
+ Timecop.freeze Time.now+10.minutes # move time forward 10 minutes
335
+ # ... check to see if expected events happened etc
336
+ Timecop.return
337
+ ```
338
+
339
+ ```ruby
340
+ Timecop.scale 60, Time.now-1.year do
341
+ # Time will begin 1 year ago but advance 60 times faster than normal
342
+ sleep 10
343
+ # still sleeps for 10 seconds YOUR time, but server and client will
344
+ # think 10 minutes have passed
345
+ end
346
+ # no need for Timecop.return if using the block style
347
+ ```
348
+
349
+ See the Timecop [README](https://github.com/travisjeffery/timecop/blob/master/README.markdown) for more details.
350
+
351
+ There is one confusing thing to note: On the server if you `sleep` then you will sleep for the specified number of seconds when viewed *outside* of the test. However inside the test environment if you look at Time.now, you will see it advancing according to the scale factor. Likewise if you have a `after` or `every` block on the client, you will wait according to *simulated* time.
352
+
353
+ ## Common Problems
354
+
355
+ If you are getting failures on Poltergeist but not Firefox, make sure you are not requiring `browser` in your components.rb.
356
+ Requiring `browser/interval` or `browser/delay` is okay.
357
+
358
+ ## Development
359
+
360
+ After checking out the repo, run bundle install and you should be good to go.
361
+
362
+ Tests are run either by running `rake` or for more control:
363
+
364
+ ```
365
+ DRIVER=ff bundle exec rspec spec/hyper_spec.rb
366
+ ```
367
+
368
+ where DRIVER can be either `ff` (firefox) or `pg` (poltergeist - default).
369
+
370
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
371
+
372
+ ## Contributing
373
+
374
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/hyper-spec. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
375
+
376
+ ## License
377
+
378
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,5 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "hyper/spec"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,72 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib/', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'hyper-spec/version'
5
+
6
+ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
7
+ spec.name = 'hyper-spec'
8
+ spec.version = HyperSpec::VERSION
9
+ spec.authors = ['catmando', 'adamcreekroad']
10
+ spec.email = ['mitch@catprint.com']
11
+
12
+ spec.summary =
13
+ 'Drive your Hyperloop client and server specs from RSpec and Capybara'
14
+ spec.description =
15
+ 'A Hyperloop application consists of isomorphic React Components, '\
16
+ 'Active Record Models, Stores, Operations and Policiespec. '\
17
+ 'Test them all from Rspec, regardless if the code runs on the client or server.'
18
+ spec.homepage = 'https://github.com/ruby-hyperloop/hyper-spec'
19
+ spec.license = 'MIT'
20
+
21
+ # Prevent pushing this gem to RubyGemspec.org. To allow pushes either set the 'allowed_push_host'
22
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
23
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
24
+
25
+ spec.files =
26
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
27
+ spec.bindir = 'exe'
28
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ['lib']
30
+
31
+ # Test app dependencies
32
+ spec.add_development_dependency 'bundler', '~> 1.12'
33
+ spec.add_development_dependency 'hyper-react', '>= 0.10.0'
34
+ spec.add_development_dependency 'rails', '~>5.0.0'
35
+ spec.add_development_dependency 'rake', '~> 10.0'
36
+ spec.add_development_dependency 'react-rails', '< 1.10.0'
37
+
38
+ # Keep linter-rubocop happy
39
+ spec.add_development_dependency 'rubocop'
40
+
41
+ if RUBY_PLATFORM == 'java'
42
+ spec.add_development_dependency 'therubyrhino'
43
+ else
44
+ spec.add_development_dependency 'therubyracer', '0.12.2'
45
+
46
+ # Actual dependencies
47
+ spec.add_dependency 'capybara'
48
+ spec.add_dependency 'opal'
49
+ spec.add_dependency 'parser'
50
+ spec.add_dependency 'poltergeist'
51
+ spec.add_dependency 'pry'
52
+ spec.add_dependency 'rspec-rails'
53
+ spec.add_dependency 'selenium-webdriver', '2.53.4'
54
+ spec.add_dependency 'timecop'
55
+ spec.add_dependency 'unparser', '0.2.5'
56
+
57
+ # Test app dependencies
58
+ spec.add_development_dependency 'chromedriver-helper'
59
+ spec.add_development_dependency 'method_source'
60
+ spec.add_development_dependency 'opal-browser'
61
+ spec.add_development_dependency 'opal-rails'
62
+ spec.add_development_dependency 'puma'
63
+ spec.add_development_dependency 'rspec-collection_matchers'
64
+ spec.add_development_dependency 'rspec-expectations'
65
+ spec.add_development_dependency 'rspec-its'
66
+ spec.add_development_dependency 'rspec-mocks'
67
+ spec.add_development_dependency 'rspec-steps'
68
+ spec.add_development_dependency 'shoulda'
69
+ spec.add_development_dependency 'shoulda-matchers'
70
+ spec.add_development_dependency 'spring-commands-rspec'
71
+ end
72
+ end