Skip to content

Commit

Permalink
ShoesSpec support via HTML files
Browse files Browse the repository at this point in the history
  • Loading branch information
noahgibbs committed Jul 5, 2024
1 parent 9904d9a commit 70ea53f
Show file tree
Hide file tree
Showing 16 changed files with 212 additions and 45 deletions.
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ PATH
specs:
space_shoes (0.1.0)
lacci (~> 0.4.0)
minitest (~> 5.22)
scarpe-components (~> 0.4.0)

GEM
Expand Down
6 changes: 2 additions & 4 deletions lib/scarpe/space_shoes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,8 @@
"Monaco",
)

# When we require SpaceShoes' shoes-spec it will fill this in on the host side
module Scarpe; module Test; end; end
require "shoes-spec"
Shoes::Spec.instance = Scarpe::Test
require "space_shoes/guest/shoes-spec"
Shoes::Spec.instance = SpaceShoes::ShoesSpec

require "scarpe/components/html"
module SpaceShoes
Expand Down
22 changes: 21 additions & 1 deletion lib/space_shoes/guest/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@ def initialize(properties)
bind_shoes_event(event_name: "init") { init }
bind_shoes_event(event_name: "run") { run }
bind_shoes_event(event_name: "destroy") { destroy }

# Run ShoesSpec tests on next heartbeat
@control_interface.on_event(:next_heartbeat) do
# If there's no error, this happens once. If an error is raised, keeps happening...

## Put the true/false pass/fail into a location where JS can access it
e = Object.new # fake parallel executor
def e.shutdown; end
def e.start; end
Minitest.parallel_executor = e # No threads available in ruby.wasm
result = Minitest.run []
# TODO: add a DOM element or similar so we know when this is complete
JS.global[:document][:shoes_spec_passed] = result
elt = JS.global[:document].createElement("div")
elt[:className] = "minitest_result"
elt[:innerHTML] = "<p>#{result ? "passed" : "failed"}</p>"
JS.global[:document][:body].appendChild(elt)

Shoes.APPS.each(&:destroy) # Need more recent version of Lacci with multi-app
end
end

attr_writer :document_root
Expand Down Expand Up @@ -88,7 +108,7 @@ def bind(name, &block)
#
# @return [void]
def request_redraw!
wrangler = DisplayService.instance.wrangler
wrangler = WebWrangler.instance
if wrangler.is_running
wrangler.replace(@document_root.to_html)
end
Expand Down
9 changes: 9 additions & 0 deletions lib/space_shoes/guest/control_interface.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,19 @@ class ControlInterface
attr_writer :doc_root
attr_reader :do_shutdown

class << self
attr_accessor :instance
end

# The control interface needs to see major system components to hook into their events
def initialize
log_init("SpaceShoes::ControlInterface")

if SpaceShoes::ControlInterface.instance
raise Shoes::Errors::TooManyInstancesError, "Cannot create multiple SpaceShoes::ControlInterface objects!"
end
SpaceShoes::ControlInterface.instance = self

@do_shutdown = false
@event_handlers = {}
(SUBSCRIBE_EVENTS | DISPATCH_EVENTS).each { |e| @event_handlers[e] = [] }
Expand Down
4 changes: 2 additions & 2 deletions lib/space_shoes/guest/display_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ def create_display_drawable_for(drawable_class_name, drawable_id, properties, is

display_app = SpaceShoes::App.new(properties)
display_app.document_root = @doc_root
@control_interface = display_app.control_interface
@control_interface = SpaceShoes::ControlInterface.instance
@control_interface.doc_root = @doc_root
@app = @control_interface.app
@wrangler = @control_interface.wrangler
@wrangler = SpaceShoes::WebWrangler.instance

set_drawable_pairing(drawable_id, display_app)

Expand Down
86 changes: 86 additions & 0 deletions lib/space_shoes/guest/shoes-spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# frozen_string_literal: true

require "minitest"

module SpaceShoes
class ShoesSpec
def self.run_shoes_spec_test_code(code, class_name: nil, test_name: nil)
if @shoes_spec_init
raise Shoes::Errors::MultipleShoesSpecRunsError, "SpaceShoes can only run a single Shoes spec per process!"
end
@shoes_spec_init = true

class_name ||= ENV["SHOES_MINITEST_CLASS_NAME"] || "TestShoesSpecCode"
test_name ||= ENV["SHOES_MINITEST_METHOD_NAME"] || "test_shoes_spec"

test_class = Class.new(SpaceShoes::ShoesSpecTest)
Object.const_set(Scarpe::Components::StringHelpers.camelize(class_name), test_class)
test_name = "test_" + test_name unless test_name.start_with?("test_")
test_class.class_eval(<<~CLASS_EVAL)
def #{test_name}
#{code}
end
CLASS_EVAL
end
end
end

# For now we send most events at the Scarpe layer. It would be quite sensible
# to send browser events rather than Scarpe events, but it will also take a lot
# more code to do it. We'll get there.

class SpaceShoes::ShoesSpecTest < Minitest::Test
Shoes::Drawable.drawable_classes.each do |drawable_class|
finder_name = drawable_class.dsl_name

define_method(finder_name) do |*args|
drawables = Shoes::App.find_drawables_by(drawable_class, *args)

raise Shoes::Errors::MultipleDrawablesFoundError, "Found more than one #{finder_name} matching #{args.inspect}!" if drawables.size > 1
raise Shoes::Errors::NoDrawablesFoundError, "Found no #{finder_name} matching #{args.inspect}!" if drawables.empty?

SpaceShoes::ShoesSpecProxy.new(drawables[0])
end
end

def drawable(*specs)
drawables = Shoes::App.find_drawables_by(*specs)
raise Shoes::Errors::MultipleDrawablesFoundError, "Found more than one #{finder_name} matching #{args.inspect}!" if drawables.size > 1
raise Shoes::Errors::NoDrawablesFoundError, "Found no #{finder_name} matching #{args.inspect}!" if drawables.empty?
SpaceShoes::ShoesSpecProxy.new(drawables[0])
end
end

class SpaceShoes::ShoesSpecProxy
attr_reader :obj
attr_reader :linkable_id
attr_reader :display

def initialize(obj)
@obj = obj
@linkable_id = obj.linkable_id
@display = ::Shoes::DisplayService.display_service.query_display_drawable_for(obj.linkable_id)
end

def method_missing(method, ...)
if @obj.respond_to?(method)
self.singleton_class.define_method(method) do |*args, **kwargs, &block|
@obj.send(method, *args, **kwargs, &block)
end
send(method, ...)
else
super # raise an exception
end
end

JS_EVENTS = [:click, :hover, :leave]
JS_EVENTS.each do |event|
define_method("trigger_#{event}") do |*args, **kwargs|
::Shoes::DisplayService.dispatch_event(event.to_s, @linkable_id, *args, **kwargs)
end
end

def respond_to_missing?(method_name, include_private = false)
@obj.respond_to_missing?(method_name, include_private)
end
end
6 changes: 2 additions & 4 deletions lib/space_shoes/guest/subscription_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,18 @@ def initialize(properties)
send_self_event(*args, event_name: @shoes_api_name)
end

@wrangler = DisplayService.instance.wrangler

case @shoes_api_name
when "animate"
frame_rate = (@args[0] || 10)
@counter = 0
@wrangler.periodic_code("animate_#{@shoes_linkable_id}", 1.0 / frame_rate) do
WebWrangler.instance.periodic_code("animate_#{@shoes_linkable_id}", 1.0 / frame_rate) do
@counter += 1
send_self_event(@counter, event_name: @shoes_api_name)
end
when "every"
delay = @args[0]
@counter = 0
@wrangler.periodic_code("every_#{@shoes_linkable_id}", delay) do
WebWrangler.instance.periodic_code("every_#{@shoes_linkable_id}", delay) do
@counter += 1
send_self_event(@counter, event_name: @shoes_api_name)
end
Expand Down
20 changes: 13 additions & 7 deletions lib/space_shoes/guest/web_wrangler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ module SpaceShoes
class WebWrangler
include Shoes::Log

class << self
attr_accessor :instance
end

attr_reader :is_running
attr_reader :is_terminated
attr_reader :heartbeat # This is the heartbeat duration in seconds, usually fractional
Expand All @@ -20,6 +24,11 @@ class WebWrangler
def initialize(title:, width:, height:, resizable: false, heartbeat: 0.1)
log_init("SpaceShoes::WebWrangler")

if SpaceShoes::WebWrangler.instance
raise Shoes::Errors::TooManyInstancesError, "Cannot create multiple SpaceShoes::WebWrangler objects!"
end
SpaceShoes::WebWrangler.instance = self

@log.debug("Creating WebWrangler...")

@wasm = SpaceShoes::WasmCalls.new
Expand All @@ -37,8 +46,10 @@ def initialize(title:, width:, height:, resizable: false, heartbeat: 0.1)
# Ruby receives scarpeHeartbeat messages via the window library's main loop.
# So this is a way for Ruby to be notified periodically, in time with that loop.
@wasm.bind("scarpeHeartbeat") do
@heartbeat_handlers.each(&:call)
@control_interface.dispatch_event(:heartbeat)
unless @control_interface.do_shutdown
@heartbeat_handlers.each(&:call)
@control_interface.dispatch_event(:heartbeat)
end
end
js_interval = (heartbeat.to_f * 1_000.0).to_i
@wasm.init("setInterval(scarpeHeartbeat,#{js_interval})")
Expand Down Expand Up @@ -158,9 +169,6 @@ class ElementWrangler
# @param selector [String|NilClass] the selector to get the DOM element(s)
# @param multi [Boolean] whether the selector may return multiple DOM elements
def initialize(html_id: nil, selector: nil, multi: false)
@webwrangler = SpaceShoes::DisplayService.instance.wrangler
raise Scarpe::MissingWranglerError, "Can't get WebWrangler!" unless @webwrangler

@html_id = html_id
@multi = multi
@selector = selector
Expand All @@ -171,11 +179,9 @@ def initialize(html_id: nil, selector: nil, multi: false)
def on_each(&block)
if @multi
items = JS.eval(@selector)
STDERR.puts "Multi selector: #{@selector.inspect} #{items.inspect}"
items.each(&block)
else
item = JS.global[:document].getElementById(@html_id)
STDERR.puts "Single selector: #{@html_id.inspect} / #{item.inspect}" if item == JS_NULL
yield(item) if item != JS_NULL
end
end
Expand Down
2 changes: 1 addition & 1 deletion packaging/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ gem "space_shoes", path: ".."

gem "ruby_wasm", "~> 2.5"
gem "js"
gem "lacci", "~>0.4.0"
gem "lacci", "~>0.4.0", github: "scarpe-team/scarpe", glob: "lacci/*.gemspec", branch: "multi_app"
gem "scarpe-components", "~>0.4.0"
15 changes: 12 additions & 3 deletions packaging/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
GIT
remote: https://github.com/scarpe-team/scarpe.git
revision: bddaf436b1152b2a8748c1d767db8cc31b52379a
branch: multi_app
glob: lacci/*.gemspec
specs:
lacci (0.4.0)
scarpe-components (~> 0.4.0)

PATH
remote: ..
specs:
space_shoes (0.1.0)
lacci (~> 0.4.0)
minitest (~> 5.22)
scarpe-components (~> 0.4.0)

GEM
remote: https://rubygems.org/
specs:
js (2.6.0)
lacci (0.4.0)
scarpe-components (~> 0.4.0)
minitest (5.22.3)
ruby_wasm (2.6.0)
ruby_wasm (2.6.0-aarch64-linux)
ruby_wasm (2.6.0-aarch64-linux-musl)
Expand All @@ -31,7 +40,7 @@ PLATFORMS

DEPENDENCIES
js
lacci (~> 0.4.0)
lacci (~> 0.4.0)!
ruby_wasm (~> 2.5)
scarpe-components (~> 0.4.0)
space_shoes!
Expand Down
3 changes: 3 additions & 0 deletions space_shoes.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ Gem::Specification.new do |spec|
spec.add_dependency "lacci", "~>0.4.0"
spec.add_dependency "scarpe-components", "~>0.4.0"

# For now, require as a direct dependency - needed so that minitest can run inside the browser in wasm
spec.add_dependency "minitest", "~>5.22"

# For more information and examples about making a new gem, check out our
# guide at: https://bundler.io/guides/creating_gem.html
end
2 changes: 1 addition & 1 deletion test/cache/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ source "https://rubygems.org"

gem "space_shoes"
gem "webrick"

gem "lacci", github: "scarpe-team/scarpe", glob: "lacci/*.gemspec", branch: "multi_app"
12 changes: 10 additions & 2 deletions test/cache/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
GEM
remote: https://rubygems.org/
GIT
remote: https://github.com/scarpe-team/scarpe.git
revision: bddaf436b1152b2a8748c1d767db8cc31b52379a
branch: multi_app
glob: lacci/*.gemspec
specs:
lacci (0.4.0)
scarpe-components (~> 0.4.0)

GEM
remote: https://rubygems.org/
specs:
scarpe-components (0.4.0)
space_shoes (0.1.0)
lacci
Expand All @@ -14,6 +21,7 @@ PLATFORMS
x86_64-darwin-22

DEPENDENCIES
lacci!
space_shoes
webrick

Expand Down
35 changes: 15 additions & 20 deletions test/cache/spacewalk.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ vm.eval(`
`);

async function runShoesApps(vm) {
// If there's a Shoes-Spec script, make sure it gets loaded before the
// apps.
const tag = document.querySelector('script[type="text/shoes-spec"]');
if(tag) {
vm.eval(`
test_elt = JS.global[:document].querySelector('script[type="text/shoes-spec"]')
test_code = test_elt[:innerText]
Shoes::Spec.instance.run_shoes_spec_test_code test_code
`);
}

const tags = document.querySelectorAll('script[type="text/ruby"]');

// Get Ruby scripts in parallel.
Expand All @@ -32,31 +43,15 @@ async function runShoesApps(vm) {
for await (const script of promisingRubyScripts) {
if (script) {
const { scriptContent, evalStyle } = script;
switch (evalStyle) {
case "async":
vm.evalAsync(scriptContent);
break;
case "sync":
vm.eval(scriptContent);
break;
}
vm.eval(scriptContent);
break;
}
}
}

function deriveEvalStyle(tag) {
const rawEvalStyle = tag.getAttribute("data-eval") || "sync";
if (rawEvalStyle !== "async" && rawEvalStyle !== "sync") {
console.warn(
`data-eval attribute of script tag must be "async" or "sync". ${rawEvalStyle} is ignored and "sync" is used instead.`,
);
return "sync";
}
return rawEvalStyle;
};
}

async function loadScriptAsync(tag) {
const evalStyle = deriveEvalStyle(tag);
const evalStyle = "sync";
if (tag.hasAttribute("src")) {
const url = tag.getAttribute("src");
const response = await fetch(url);
Expand Down
Loading

0 comments on commit 70ea53f

Please sign in to comment.