diff --git a/.gitignore b/.gitignore index 79192be1..675730af 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ vendor .bundle copycat doc +spec/test_app/tmp/ +spec/test_app/log/ +BrowserStackLocal* diff --git a/Gemfile b/Gemfile index cfd5c90c..2b3a2b89 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,4 @@ source 'https://rubygems.org' -gemspec # specs gem 'rake' @@ -16,3 +15,10 @@ gem 'rest-client', require: false # browser gem 'opal', ['>= 1.0', '< 2.0'] gem 'paggio', github: 'meh/paggio' + +# hyper-spec (for testing http requests, and DOM features) +git 'https://github.com/hyperstack-org/hyperstack.git', branch: 'edge', glob: 'ruby/*/*.gemspec' do + gem 'hyper-spec' +end + +gemspec diff --git a/README.md b/README.md index 77336c86..abdc29eb 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,47 @@ The suggested polyfill is [wgxpath](https://code.google.com/p/wicked-good-xpath/), require it **before** opal-browser. +Contributing +------------ +git clone, then install the required gems: +``` +$ bundle install +``` +run the specs: +``` +$ bundle exec rake +``` + +Most of the specs are run using opal-rspec. The specs that test http +functions or need a full browser environment use the hyper-spec gem to +test communication between a test rails server and the client. These specs +are marked with the `:js` tag. + +You can also run all the tests in a browser by running: +``` +$ bundle exec rackup +``` +and then visiting `localhost:9292` + +#### Debugging tips: + +For the non-js specs (those that run strictly in the client) you can insert +a `debugger` breakpoint in the code. Run the browser server (`rackup`) but +before you load the page, bring up the browser debugger window. + +The specs will stop when they hit your debugging breakpoint and you can poke +around. + +For the js specs add the pry gem to the Gemfile, and then set a `binding.pry` +breakpoint in the problem spec. You can run the specific spec in the usual way +(i.e. `bundle exec spec/some_spec:227`) and you will hit the pry breakpoint. + +Now you can check the state of the client code and do some experiments with +the `c?` method (evaluate on client) +```ruby +$ c? { HTTP.get('/http') } # code in the block is run on the client +``` + License ======= diff --git a/Rakefile b/Rakefile index 759f5eb3..a556cd92 100644 --- a/Rakefile +++ b/Rakefile @@ -1,8 +1,42 @@ +# frozen_string_literal: true + require 'bundler' Bundler.require - require 'opal/rspec/rake_task' -Opal::RSpec::RakeTask.new(:default) do |_, task| +require 'bundler/gem_tasks' +require 'rspec/core/rake_task' + +# a few specs require a "real DOM" and/or a server response, so these +# specs are run using the hyper-spec gem. These specs are marked with +# the server_side_test tag. + +# The remaining specs can be run using opal-rspec + +# All the specs will run in the browser using opal-rspec: +# just run bundle exec rackup, and browse localhost:9292 + +# See spec/spec_helper.rb, spec/app.rb, and config.ru for more details + +RSpec::Core::RakeTask.new(:server_and_client_specs) do |t| + t.rspec_opts = '--tag js' + t.pattern = 'spec/http_spec.rb,spec/native_cached_wrapper_spec.rb,spec/canvas/**/*_spec.rb' +end + +Opal::RSpec::RakeTask.new(:opal_rspec_runner) do |_, task| task.default_path = 'spec' task.pattern = 'spec/**/*_spec.{rb,opal}' -end \ No newline at end of file +end + +task :client_only_specs do + # Must set the runner to chrome as after all we need a browser to test + # this stuff. + # + # Also can't see how to set the format to progress and exclude the js tags + # except by setting it up via SPEC_OPTS enviroment var and a require file + sh 'RUNNER=chrome '\ + "SPEC_OPTS='--format progress --require exclude_requires_server' "\ + 'rake opal_rspec_runner' +end + +task default: %i[server_and_client_specs client_only_specs] do +end diff --git a/config.ru b/config.ru index 89bf1534..f2aa2cae 100644 --- a/config.ru +++ b/config.ru @@ -1,5 +1,6 @@ require 'bundler' Bundler.require +require './spec/app' apps = [] @@ -15,67 +16,5 @@ apps << Opal::Sprockets::Server.new(sprockets: sprockets_env) { |s| s.debug = false } -apps << Class.new(Sinatra::Base) { - get '/http' do - "lol" - end - - post '/http' do - if params['lol'] == 'wut' - "ok" - else - "fail" - end - end - - put '/http' do - if params['lol'] == 'wut' - "ok" - else - "fail" - end - end - - delete '/http' do - "lol" - end - - post '/http-file' do - if params['lol'] == 'wut' && - params['file'][:filename] == 'yay.txt' && - params['file'][:tempfile].read == 'content' - - "ok" - else - "fail" - end - end - - get '/events' do - headers 'Content-Type' => 'text/event-stream' - - stream do |out| - sleep 0.2 - - out << "data: lol\n" << "\n" - out << "event: custom\n" << "data: omg\n" << "\n" - out << "data: wut\n" << "\n" - - sleep 10 - end - end - - get '/socket' do - request.websocket do |ws| - ws.onopen do - ws.send 'lol' - end - - ws.onmessage do |msg| - ws.send msg - end - end - end -} - +apps << app run Rack::Cascade.new(apps) diff --git a/opal-browser.gemspec b/opal-browser.gemspec index f09bcf5a..ef18e057 100644 --- a/opal-browser.gemspec +++ b/opal-browser.gemspec @@ -18,4 +18,9 @@ Gem::Specification.new {|s| s.add_dependency 'opal', ['>= 1.0', '< 2.0'] s.add_dependency 'paggio' + + # s.add_development_dependency 'hyper-spec' # pulled in on the Gemfile as we need a specific version from github + s.add_development_dependency 'rails', '~> 6.0' + s.add_development_dependency 'opal-rails' + s.add_development_dependency 'puma' } diff --git a/opal/browser/canvas.rb b/opal/browser/canvas.rb index eec171ca..92c93e21 100644 --- a/opal/browser/canvas.rb +++ b/opal/browser/canvas.rb @@ -12,7 +12,7 @@ class Canvas attr_reader :element, :style, :text - def initialize(*args) + def initialize(*args, &block) if DOM::Element === args.first element = args.shift @@ -49,6 +49,8 @@ def initialize(*args) if @image draw_image(@image) end + + instance_eval(&block) if block_given? end def width @@ -93,6 +95,10 @@ def gradient(*args, &block) Gradient.new(self, *args, &block) end + def stroke=(value) + Browser::Canvas::Style.new(self).stroke = value + end + def clear(x = nil, y = nil, width = nil, height = nil) x ||= 0 y ||= 0 diff --git a/opal/browser/canvas/data.rb b/opal/browser/canvas/data.rb index 6123aa34..465a2d40 100644 --- a/opal/browser/canvas/data.rb +++ b/opal/browser/canvas/data.rb @@ -35,6 +35,10 @@ def length `#@native.data.length` end + def to_a + `Array.prototype.slice.call(#@native.data)` + end + def [](index) `#@native.data[index]` end diff --git a/opal/browser/canvas/gradient.rb b/opal/browser/canvas/gradient.rb index e6904daf..e24dbc7c 100644 --- a/opal/browser/canvas/gradient.rb +++ b/opal/browser/canvas/gradient.rb @@ -8,18 +8,30 @@ class Gradient def initialize(context, *args, &block) @context = context - super(case args.length - when 4 then `#{@context.to_n}.createLinearGradient.apply(self, args)` - when 6 then `#{@context.to_n}.createRadialGradient.apply(self, args)` - else raise ArgumentError, "don't know where to dispatch" - end) - - instance_eval(&block) + _this = @context.to_n + + *args, stops = args if args.length.odd? + + super case args.length + when 4 then `_this.createLinearGradient.apply(_this, args)` + when 6 then `_this.createRadialGradient.apply(_this, args)` + else raise ArgumentError, + 'Gradients must be created with 4 or 6 parameters' + end + add_stops(stops) + instance_eval(&block) if block_given? end def add(position, color) - `#{@context.to_n}.addColorStop(position, color)` + `#{to_n}.addColorStop(position, color)` + self + end + + alias add_stop add + alias add_color_stop add + def add_stops(stops) + stops&.each { |position, color| add_stop(position, color) } self end end diff --git a/opal/browser/dom/element.rb b/opal/browser/dom/element.rb index a2a57d3a..e3a657f5 100644 --- a/opal/browser/dom/element.rb +++ b/opal/browser/dom/element.rb @@ -329,7 +329,7 @@ def inner_dom(&block) # FIXME: when block passing is fixed doc = document - self << Builder.new(doc, self, &block).to_a + self << Builder.new(doc, &block).to_a end # Set the inner DOM with the given node. diff --git a/spec/app.rb b/spec/app.rb new file mode 100644 index 00000000..38d885f6 --- /dev/null +++ b/spec/app.rb @@ -0,0 +1,82 @@ +require 'bundler' +Bundler.require + +get '/http' do + "lol" +end + +post '/http' do + if params['lol'] == 'wut' + "ok" + else + "fail" + end +end + +put '/http' do + if params['lol'] == 'wut' + "ok" + else + "fail" + end +end + +delete '/http' do + "lol" +end + +post '/http-file' do + if params['lol'] == 'wut' && + params['file'][:filename] == 'yay.txt' && + params['file'][:tempfile].read == 'content' + + "ok" + else + "fail" + end +end + +get '/events' do + headers 'Content-Type' => 'text/event-stream' + + stream do |out| + sleep 0.2 + + out << "data: lol\n" << "\n" + out << "event: custom\n" << "data: omg\n" << "\n" + out << "data: wut\n" << "\n" + + sleep 10 + end +end + +get '/socket' do + request.websocket do |ws| + ws.onopen do + ws.send 'lol' + end + + ws.onmessage do |msg| + ws.send msg + end + end +end + +module OpalSprocketsServer + def self.opal + @opal ||= Opal::Sprockets::Server.new do |s| + s.append_path 'spec/app' + s.main = 'application' + s.debug = ENV['RACK_ENV'] != 'production' + end + end +end + +def app + Rack::Builder.app do + map '/assets' do + run OpalSprocketsServer.opal.sprockets + end + run Sinatra::Application + end +end diff --git a/spec/app/application.rb b/spec/app/application.rb new file mode 100644 index 00000000..d46e8a05 --- /dev/null +++ b/spec/app/application.rb @@ -0,0 +1,4 @@ +require 'opal' +require 'browser' +require 'browser/http' +require 'browser/canvas' diff --git a/spec/browser/http.rb b/spec/browser/http.rb new file mode 100644 index 00000000..6d56d694 --- /dev/null +++ b/spec/browser/http.rb @@ -0,0 +1,9 @@ +# stub the Browser::HTTP class and supported? method +# for running code server side with hyper-spec +module Browser + class HTTP + def self.supported? + true + end + end +end diff --git a/spec/browser/native_cached_wrapper.rb b/spec/browser/native_cached_wrapper.rb new file mode 100644 index 00000000..6fc8157d --- /dev/null +++ b/spec/browser/native_cached_wrapper.rb @@ -0,0 +1,6 @@ +# stub the Browser::NativeCachedWrapper class method +# for running code server side with hyper-spec +module Browser + module NativeCachedWrapper + end +end diff --git a/spec/canvas/canvas_spec.rb b/spec/canvas/canvas_spec.rb new file mode 100644 index 00000000..82f52301 --- /dev/null +++ b/spec/canvas/canvas_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' +require 'browser/canvas' if RUBY_ENGINE == 'opal' + +# reset between each example to insure the DOM starts clean +describe 'Browser::Canvas', js: true, no_reset: false do + context 'Gradient' do + matcher :draw_the_correct_image do + match do |expected_canvas| + # Assume the spec is delivering a square canvas and + # each pixel is made of 4 bytes for R, G, B, and Alpha channel + + size = ((expected_canvas.length / 4)**0.5).to_i + canvas = expected_canvas.each_slice(4).each_slice(size).to_a + + # canvas should be a diagonal line of red pixels all the way, no + # green, and blue increasing from 0 to 255 +/- for antialiasing. + last_blue_pixel = 0 + + (size - 1).times do |i| + pixel = canvas[i][i] + break if pixel[0] != 255 || pixel[1] != 0 + break unless pixel[2].between?(last_blue_pixel, last_blue_pixel + 5) + + last_blue_pixel = pixel[2] + end && last_blue_pixel.between?(254, 255) + end + end + + html "" + + it 'can create a linear gradient' do + expect do + canvas = $document['canvas'] + context = Browser::Canvas.new(canvas) + gradient = context.gradient(50, 50, 150, 150) + gradient.add(0, '#ff0000') + gradient.add(1, '#ff00ff') + context.stroke = gradient + context.begin + context.line(50, 50, 150, 150) + context.stroke + context.close + context.data(50, 50, 101, 101).to_a + end.to_on_client draw_the_correct_image + end + + it 'can use blocks and helpers to clean things up' do + expect do + Browser::Canvas.new($document['canvas']) do + self.stroke = gradient(50, 50, 150, 150, 0 => '#ff0000', 1 => '#ff00ff') + path { line(50, 50, 150, 150).stroke } + end.data(50, 50, 101, 101).to_a + end.to_on_client draw_the_correct_image + end + end +end diff --git a/spec/dom/element_spec.rb b/spec/dom/element_spec.rb index cbf8eda2..2343d0c7 100644 --- a/spec/dom/element_spec.rb +++ b/spec/dom/element_spec.rb @@ -200,7 +200,7 @@ DOM { div.shadow_item "Hello world!" }.append_to($document[:shadowtest].shadow) - + expect($document[:shadowtest].at_css(".shadow_item")).to be_nil expect($document[:shadowtest].shadow.at_css(".shadow_item").text).to be("Hello world!") end diff --git a/spec/exclude_requires_server.rb b/spec/exclude_requires_server.rb new file mode 100644 index 00000000..eb4c861a --- /dev/null +++ b/spec/exclude_requires_server.rb @@ -0,0 +1,3 @@ +RSpec.configure do |config| + config.filter_run_excluding js: true +end diff --git a/spec/history_spec.rb b/spec/history_spec.rb index e98d8296..f8293cf2 100644 --- a/spec/history_spec.rb +++ b/spec/history_spec.rb @@ -2,15 +2,18 @@ require 'browser/history' describe Browser::History do + + let(:root) { `window.location.pathname` } + describe '#current' do it 'should return the current location' do - expect($window.history.current).to eq('/') + expect($window.history.current).to eq(root) end end describe '#push' do it 'should change location' do - expect($window.history.current).to eq('/') + expect($window.history.current).to eq(root) $window.history.push('/lol') expect($window.history.current).to eq('/lol') $window.history.push('/') @@ -20,14 +23,14 @@ describe '#back' do it 'should go back once' do - expect($window.history.current).to eq('/') + expect($window.history.current).to eq(root) $window.history.push('/wut') expect($window.history.current).to eq('/wut') promise = Promise.new $window.one 'pop:state' do |e| - expect($window.history.current).to eq('/') + expect($window.history.current).to eq(root) promise.resolve end diff --git a/spec/http_spec.rb b/spec/http_spec.rb index 88528d9a..b110a574 100644 --- a/spec/http_spec.rb +++ b/spec/http_spec.rb @@ -2,8 +2,10 @@ require 'browser/http' describe Browser::HTTP do - let(:path) { '/http' } - let(:path_file) { '/http-file' } + let!(:path) { '/http' } + let!(:path_file) { '/http-file' } + + # Actual requests are routed to test_app/app/test_controller.rb describe '.get' do it 'fetches a path' do @@ -16,8 +18,8 @@ end describe '.get!' do - it 'fetches a path' do - expect(Browser::HTTP.get!(path).text).to eq('lol') + it 'fetches a path', server_side_test do + expect { Browser::HTTP.get!(path).text }.on_client_to eq('lol') end end @@ -31,14 +33,16 @@ end end - describe '.post!' do + describe '.post!', server_side_test do it 'sends parameters properly' do - expect(Browser::HTTP.post!(path, lol: 'wut').text).to eq('ok') + expect { Browser::HTTP.post!(path, lol: 'wut').text }.on_client_to eq('ok') end it 'sends files properly' do - file = Browser::File.create(["content"], "yay.txt", type: "text/plain") - expect(Browser::HTTP.post!(path_file, lol: 'wut', file: file).text).to eq('ok') + expect do + file = Browser::File.create(["content"], "yay.txt", type: "text/plain") + Browser::HTTP.post!(path_file, lol: 'wut', file: file).text + end.on_client_to eq('ok') end end @@ -52,9 +56,9 @@ end end - describe '.put!' do + describe '.put!', server_side_test do it 'sends parameters properly' do - expect(Browser::HTTP.put!(path, lol: 'wut').text).to eq('ok') + expect { Browser::HTTP.put!(path, lol: 'wut').text }.on_client_to eq('ok') end end @@ -68,9 +72,9 @@ end end - describe '.delete!' do + describe '.delete!', server_side_test do it 'fetches a path' do - expect(Browser::HTTP.delete!(path).text).to eq('lol') + expect { Browser::HTTP.delete!(path).text }.on_client_to eq('lol') end end end if Browser::HTTP.supported? diff --git a/spec/native_cached_wrapper_spec.rb b/spec/native_cached_wrapper_spec.rb index cc3b265d..2fb27706 100644 --- a/spec/native_cached_wrapper_spec.rb +++ b/spec/native_cached_wrapper_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require 'browser/native_cached_wrapper' describe Browser::NativeCachedWrapper do it 'deduplicates DOM objects' do @@ -34,13 +35,12 @@ def self.new(arg1, arg2, arg3) HTML - it 'supports restricted objects' do + it 'supports restricted objects', server_side_test do # Window won't be restricted - expect($window.restricted?).to eq(false) + expect { $window.restricted? }.on_client_to eq(false) # Iframe itself won't be restricted - expect($document['ifr'].restricted?).to eq(false) - # But its content_window will be (due to CORS) - expect($document['ifr'].content_window.restricted?).to eq(true) + expect { $document['ifr'].restricted? }.on_client_to eq(false) + # But its content_window will be (due to CORS) (this does not work with opal-rspec RUNNER=chrome) + expect { $document['ifr'].content_window.restricted? }.on_client_to eq(true) end end - diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 10b19c09..33663837 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,34 +1,161 @@ -require 'browser' -require 'console' - -module HtmlHelper - # Add some html code to the body tag ready for testing. This will - # be added before each test, then removed after each test. It is - # convenient for adding html setup quickly. The code is wrapped - # inside a div, which is directly inside the body element. - # - # describe "DOM feature" do - # html <<-HTML - #
- # HTML - # - # it "foo should exist" do - # Document["#foo"] - # end - # end - # - # @param [String] html_string html content to add - def html(html_string='') - html = "