| # |
| # This class spawns a io.js process to run a HTTP server which accepts |
| # POST requests containing Vue/jsdom scripts and responds with |
| # HTML results. It provides a Rack interface, enabling this server to |
| # be run with Capybara/RackTest. |
| # |
| |
| require 'ruby2js' |
| require 'net/http' |
| require 'stringio' |
| |
| require 'capybara/rspec' |
| require 'ruby2js/filter/vue' |
| |
| class VueServer |
| @@pid = nil |
| @@port = nil |
| |
| # start a new server |
| def self.start |
| return if @@pid |
| |
| # select an available port |
| server = TCPServer.new('127.0.0.1', 0) |
| @@port = server.addr[1] |
| server.close |
| |
| # spawn a server process |
| nodejs = (`which nodejs`.empty? ? 'node' : 'nodejs') |
| @@pid = spawn(nodejs, '-e', |
| Ruby2JS.convert(@@server, {ivars: {:@port => @@port}})) |
| |
| # wait for server to start |
| (0..10).each do |i| |
| begin |
| response = new.call('rack.input' => StringIO.new("response.end('hi')")) |
| return if response.first == '200' and response.last == 'hi' |
| STDERR.puts response |
| raise RuntimeError('Invalid VueServer response received') |
| rescue Errno::ECONNREFUSED |
| sleep i * 0.1 |
| end |
| end |
| end |
| |
| # rack compatible interface |
| def call(env) |
| http = Net::HTTP.new('localhost', @@port) |
| request = Net::HTTP::Post.new('/', {}) |
| request.body = env['rack.input'].read |
| response = http.request(request) |
| [response.code, response.to_hash(), response.body] |
| end |
| |
| # stop server |
| def self.stop |
| return unless @@pid |
| |
| begin |
| http = Net::HTTP.new('localhost', @@port) |
| request = Net::HTTP::Post.new('/', {}) |
| request.body = "response.end('bye'); process.exit(0)" |
| response = http.request(request) |
| rescue Errno::ECONNREFUSED, EOFError |
| nil |
| ensure |
| Process.wait(@@pid) |
| @@pid = nil |
| end |
| end |
| |
| # the server itself |
| @@server = proc do |
| cleanup = require("jsdom-global/register") |
| delete global.XMLHttpRequest |
| |
| process.env.VUE_ENV = 'server' |
| |
| Vue = require('vue') |
| Vue.config.productionTip = false |
| |
| # render a response, using server side rendering |
| def Vue.renderResponse(component, response) |
| renderer = require('vue-server-renderer').createRenderer() |
| app = Vue.new(render: proc {|h| return h(component)}) |
| |
| renderer.renderToString(app) do |err, html| |
| if err |
| response.end(err.toString() + "\n" + err.stack) |
| else |
| response.end(html) |
| end |
| end |
| end |
| |
| # render a element, using client side rendering |
| def Vue.renderElement(component) |
| outer = document.createElement('div') |
| inner = document.createElement('span') |
| outer.appendChild(inner); |
| Vue.new(el: inner, render: proc {|h| return h(component)}) |
| return outer.firstChild |
| end |
| |
| # render an app, using client side rendering. Convenience methods are |
| # provided to querySelector, and to extract outerHTML. |
| def Vue.renderApp(component) |
| outer = document.createElement('div') |
| inner = document.createElement('span') |
| outer.appendChild(inner); |
| app = Vue.new(el: inner, render: proc {|h| return h(component)}) |
| inner = outer.firstChild |
| |
| def app.outerHTML |
| return inner.outerHTML |
| end |
| |
| def app.querySelector(selector) |
| return outer.querySelector(selector) |
| end |
| |
| return app |
| end |
| |
| jQuery = require('jquery') |
| |
| http = require('http') |
| server = http.createServer do |request, response| |
| data = '' |
| request.on('data') do |chunk| |
| data += chunk |
| end |
| |
| request.on 'error' do |error| |
| console.log "VueServer error: #{error.message}" |
| end |
| |
| request.on 'end' do |
| response.writeHead(200, 'Content-Type' => 'text/plain') |
| |
| begin |
| eval(data) |
| rescue => error |
| response.end(error.toString()); |
| end |
| end |
| end |
| |
| server.listen(@port) |
| end |
| end |
| |
| shared_context "vue_server", server: :vue do |
| # |
| # administrivia |
| # |
| before :all do |
| VueServer.start |
| Dir.chdir File.expand_path('../../views', __FILE__) do |
| @_script = Ruby2JS.convert(File.read('app.js.rb'), file: 'app.js.rb') |
| end |
| end |
| |
| before :each do |
| @_app, Capybara.app = Capybara.app, VueServer.new |
| end |
| |
| def on_vue_server(&block) |
| locals = {} |
| instance_variables.each do |ivar| |
| next if ivar.to_s.start_with? '@_' |
| locals[ivar] = instance_variable_get(ivar) |
| end |
| |
| page.driver.post('/', @_script + ';' + |
| Ruby2JS.convert(block, vue: true, ivars: locals).to_s) |
| end |
| |
| after :each do |
| Capybara.app = @_app |
| end |
| |
| at_exit do |
| VueServer.stop |
| end |
| end |