Skip to content

Commit

Permalink
Refactor some test operations into a class
Browse files Browse the repository at this point in the history
Related to #13
  • Loading branch information
aalin committed Oct 31, 2022
1 parent 28df636 commit cd24971
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 102 deletions.
4 changes: 4 additions & 0 deletions example/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ PATH
source_map (~> 3.0)
syntax_tree (~> 3.6)
syntax_tree-haml (~> 1.3)
syntax_tree-xml (~> 0.1.0)
terminal-table (~> 3.0.1)
toml-rb (~> 2.2.0)

Expand Down Expand Up @@ -109,6 +110,9 @@ GEM
haml (>= 5.2)
prettier_print
syntax_tree (>= 2.0.1)
syntax_tree-xml (0.1.0)
prettier_print
syntax_tree (>= 2.0.1)
temple (0.8.2)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
Expand Down
135 changes: 33 additions & 102 deletions lib/mayu/vdom.test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ def handle_click_decrement(e)
:css
.foo { color: peru; }
.btn { background: peachpuff; }
.increment { background: peachpuff; }
.decrement { background: peachpuff; }
.increment { }
.decrement { }
.foo
%button.increment(data-test-id="increment" onclick=handle_click_increment)
Decrement
Expand All @@ -43,106 +43,37 @@ def handle_click_decrement(e)
HAML

Async do |task|
vtree = Mayu::TestHelper.setup_vtree

vtree.render(Mayu::VDOM.h(component))
update_finished = Async::Notification.new

updater = Mayu::VDOM::VTree::Updater.new(vtree)
update_task =
updater.run do |event, payload|
if event == :update_finished
update_finished.signal
else
Console.logger.info(
Mayu::VDOM::VTree::Updater,
event,
JSON.generate(payload)
)
end
end

doc = render_document(vtree)
puts Mayu::TestHelper.format_source(doc.to_html, :html)

button = doc.at_css("[data-test-id=increment]")
trigger_event(vtree, button, :click, { type: "click" })

update_finished.wait

doc = render_document(vtree)

assert_equal(Mayu::TestHelper.format_xml_plain(doc.to_html), <<~HTML)
<div class="lib/mayu/vdom.foo">
<button
data-test-id=\"increment\"
onclick=\"Mayu.handle(event,'1PH0HsFv5pGhqXU_')\"
class=\"lib/mayu/vdom.increment\"
>
Decrement
</button>
<button
data-test-id=\"decrement\"
onclick=\"Mayu.handle(event,'Lw7pLA6l7igSkyvD')\"
class=\"lib/mayu/vdom.decrement\"
>
Increment
</button>
<output>4</output>
</div>
HTML
ensure
update_task&.stop
end
end

private

CALLBACK_ID_RE = /\AMayu\.handle\(event,'(?<callback_id>\w+)'\)\z/

def render_document(vtree)
html = vtree.to_html

Nokogiri::HTML5::DocumentFragment
.parse(html)
.tap { validate_doc(_1, html) }
.tap { remove_css_hashes(_1) }
.tap { remove_mayu_id(_1) }
end

def validate_doc(doc, html)
doc.errors.each do |error|
puts "\e[31m#{error}\e[0m"
puts format("\e[31m%s\e[0m", error.message)
puts format("%s\e[0m", html.dup.insert(error.column, "\e[33m"))
end
end

def remove_mayu_id(doc)
doc
.css("[data-mayu-id]")
.each { |elem| elem.remove_attribute("data-mayu-id") }
end

def remove_css_hashes(doc)
doc
.css("[class]")
.each { |elem| elem["class"] = elem["class"].gsub(/\?[^$\s]+/, "") }
end

def trigger_event(vtree, element, event, payload = {})
vtree.handle_callback(callback_id(element, "on#{event}"), payload)
end

def callback_id(element, attr)
if match = CALLBACK_ID_RE.match(element[attr])
match[:callback_id]
else
$stderr.puts <<~EOF
\e[7;31mCould not find an #{attr}-handler:\e[0m
#{Mayu::TestHelper.format_source(element.to_html, :html)}
EOF
raise "Element does not have an #{attr}-handler: #{element.to_s}"
Mayu::TestHelper::Page.run do |page|
page.render(Mayu::VDOM.h(component))
page.wait_for_update

button = page.find_by_test_id("increment")
page.fire_event(button, :click)

page.debug!

page.wait_for_update

assert_equal(page.to_html, <<~HTML)
<div class="lib/mayu/vdom.foo">
<button
data-test-id=\"increment\"
onclick=\"Mayu.handle(event,'1PH0HsFv5pGhqXU_')\"
class=\"lib/mayu/vdom.increment\"
>
Decrement
</button>
<button
data-test-id=\"decrement\"
onclick=\"Mayu.handle(event,'Lw7pLA6l7igSkyvD')\"
class=\"lib/mayu/vdom.decrement\"
>
Increment
</button>
<output>4</output>
</div>
HTML
end
end
end
end
1 change: 1 addition & 0 deletions test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module TestHelper

autoload :Components, "test_helper/components"
autoload :Formatting, "test_helper/formatting"
autoload :Page, "test_helper/page"
autoload :VDOM, "test_helper/vdom"

sig { returns(Mayu::AppMetrics) }
Expand Down
147 changes: 147 additions & 0 deletions test_helper/page.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# typed: strict
# frozen_string_literal: true

require "mayu/vdom/vtree"
require_relative "page/document"

module Mayu
module TestHelper
class Page
class UserEvent
extend T::Sig

sig { returns(String) }
attr_reader :type
sig { returns(T::Hash[String, T.untyped]) }
attr_reader :payload

sig { params(type: String, payload: T::Hash[String, T.untyped]).void }
def initialize(type, payload = {})
@type = type
@payload =
T.let(payload.merge("type" => type), T::Hash[String, T.untyped])
freeze
end
end

extend T::Sig

DEFAULT_UPDATE_TIMEOUT = 0.5
CALLBACK_ID_RE = /\AMayu\.handle\(event,'(?<callback_id>\w+)'\)\z/

sig { params(block: T.proc.params(arg0: Page).void).void }
def self.run(&block)
page = Page.new
updater_task = page.run
yield page
ensure
updater_task&.stop
end

sig { params(task: Async::Task).void }
def initialize(task: Async::Task.current)
@vtree = T.let(TestHelper.setup_vtree, Mayu::VDOM::VTree)
@on_update_finished =
T.let(Async::Notification.new, Async::Notification)
@doc = T.let(Document.new, Document)
end

sig { returns(Async::Task) }
def run
Mayu::VDOM::VTree::Updater
.new(@vtree)
.run do |event, payload|
if event == :update_finished
@doc.replace_html(@vtree.to_html)
@on_update_finished.signal
else
# TODO: Handle patch events and apply them
# just like we do in the browser..
Console.logger.debug(
Mayu::VDOM::VTree::Updater,
event,
JSON.generate(payload)
)
end
end
end

sig { params(descriptor: Mayu::VDOM::Descriptor).void }
def render(descriptor)
@vtree.render(descriptor)
@doc.replace_html(@vtree.to_html)
end

sig { params(timeout: Float, task: Async::Task).void }
def wait_for_update(
timeout: DEFAULT_UPDATE_TIMEOUT,
task: Async::Task.current
)
task.with_timeout(timeout) do
@on_update_finished.wait
rescue Async::TimeoutError
# noop
end
end

sig { returns(String) }
def to_html
Mayu::TestHelper.format_xml_plain(@doc.to_html)
end

sig { void }
def debug!
Console.logger.info(
"#{self.class.name}##{__method__}",
"Caller: #{caller.find { !_1.include?("sorbet-runtime") }}",
debug_html
)
end

sig { returns(String) }
def debug_html
Mayu::TestHelper.format_source(@doc.to_html, :html)
end

sig do
params(
element: T.nilable(Nokogiri::XML::Element),
type: Symbol,
payload: T::Hash[String, String]
).void
end
def fire_event(element, type, payload = {})
raise ArgumentError, "element is nil" unless element

event = UserEvent.new(type.to_s, payload)
callback_id = callback_id_from_attr(element, "on#{type}")
@vtree.handle_callback(callback_id, event.payload)
end

sig { params(test_id: String).returns(T.nilable(Nokogiri::XML::Element)) }
def find_by_test_id(test_id)
@doc.at_css("[data-test-id='#{test_id}']")
end

sig { params(rule: String).returns(T.nilable(Nokogiri::XML::Element)) }
def find_by_css(rule)
@doc.at_css(rule)
end

sig do
params(element: Nokogiri::XML::Element, attr: String).returns(String)
end
def callback_id_from_attr(element, attr)
if match = CALLBACK_ID_RE.match(element[attr])
return match[:callback_id].to_s
end

$stderr.puts <<~EOF
\e[7;31mCould not find an #{attr}-handler:\e[0m
#{Mayu::TestHelper.format_source(element.to_html, :html)}
EOF
raise "Element does not have an #{attr}-handler: #{element.to_s}"
end
end
end
end
74 changes: 74 additions & 0 deletions test_helper/page/document.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# typed: strict
# frozen_string_literal: true

require "nokogiri"

module Mayu
module TestHelper
class Page
class Document
extend T::Sig

sig { void }
def initialize
@doc =
T.let(
Nokogiri::HTML5::DocumentFragment.parse(""),
Nokogiri::HTML5::DocumentFragment
)
end

sig { params(html: String).void }
def replace_html(html)
@doc = render_document(html)
end

sig { params(rule: String).returns(T.nilable(Nokogiri::XML::Element)) }
def at_css(rule)
@doc.at_css(rule)
end

sig { returns(String) }
def to_html
@doc.to_html
end

private

sig { params(html: String).returns(Nokogiri::HTML5::DocumentFragment) }
def render_document(html)
Nokogiri::HTML5::DocumentFragment
.parse(html)
.tap { validate_doc(_1, html) }
.tap { remove_css_hashes(_1) }
.tap { remove_mayu_id(_1) }
end

sig do
params(doc: Nokogiri::HTML5::DocumentFragment, html: String).void
end
def validate_doc(doc, html)
doc.errors.each do |error|
puts "\e[31m#{error}\e[0m"
puts format("\e[31m%s\e[0m", error.message)
puts format("%s\e[0m", html.dup.insert(error.column, "\e[33m"))
end
end

sig { params(doc: Nokogiri::HTML5::DocumentFragment).void }
def remove_mayu_id(doc)
doc
.css("[data-mayu-id]")
.each { |elem| elem.remove_attribute("data-mayu-id") }
end

sig { params(doc: Nokogiri::HTML5::DocumentFragment).void }
def remove_css_hashes(doc)
doc
.css("[class]")
.each { |elem| elem["class"] = elem["class"].gsub(/\?[^$\s]+/, "") }
end
end
end
end
end

0 comments on commit cd24971

Please sign in to comment.