funicular 0.2.1 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +54 -0
- data/README.md +2 -2
- data/Rakefile +23 -15
- data/demo/test_chartjs.html +17 -17
- data/demo/test_component.html +15 -15
- data/demo/test_error_boundary.html +41 -41
- data/demo/test_router.html +57 -57
- data/demo/tic-tac-toe.html +29 -29
- data/docs/architecture.md +43 -30
- data/lib/funicular/compiler.rb +13 -13
- data/lib/funicular/helpers/picoruby_helper.rb +24 -1
- data/lib/funicular/ssr/runtime.rb +2 -0
- data/lib/funicular/ssr.rb +2 -1
- data/lib/funicular/testing/node_runner.rb +1 -1
- data/lib/funicular/vendor/mrbc/VERSION +1 -0
- data/lib/funicular/vendor/{picorbc/picorbc.js → mrbc/mrbc.js} +14 -1
- data/lib/funicular/vendor/mrbc/mrbc.wasm +0 -0
- data/lib/funicular/vendor/picoruby/VERSION +1 -1
- data/lib/funicular/vendor/picoruby/debug/init.iife.js +19 -4
- data/lib/funicular/vendor/picoruby/debug/picoruby.js +16 -10
- data/lib/funicular/vendor/picoruby/debug/picoruby.wasm +0 -0
- data/lib/funicular/vendor/picoruby/dist/init.iife.js +19 -4
- data/lib/funicular/vendor/picoruby/dist/picoruby.js +1 -1
- data/lib/funicular/vendor/picoruby/dist/picoruby.wasm +0 -0
- data/lib/funicular/vendor/picoruby-test-node/VERSION +1 -1
- data/lib/funicular/vendor/picoruby-test-node/picoruby.js +85 -84
- data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm +0 -0
- data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm.map +1 -1
- data/lib/funicular/version.rb +1 -1
- data/lib/generators/funicular/chat/templates/funicular_chat_component.rb.tt +47 -46
- data/lib/tasks/funicular.rake +1 -1
- data/minitest/commands_routes_test.rb +97 -0
- data/minitest/compiler_test.rb +195 -0
- data/minitest/configuration_test.rb +64 -0
- data/minitest/fixtures/funicular_app/components/greeting_component.rb +6 -6
- data/minitest/form_for_test.rb +2 -2
- data/minitest/funicular_test.rb +28 -2
- data/minitest/hydration_test.rb +2 -2
- data/minitest/middleware_test.rb +154 -0
- data/minitest/picoruby_helper_test.rb +139 -0
- data/minitest/route_parser_test.rb +139 -0
- data/minitest/schema_test.rb +23 -0
- data/minitest/ssr_test.rb +57 -0
- data/minitest/support/rails_stub.rb +59 -0
- data/minitest/test_helper.rb +20 -0
- data/minitest/testing_test.rb +267 -0
- data/minitest/view_context_test.rb +101 -0
- data/mrblib/component.rb +158 -237
- data/mrblib/debug.rb +7 -6
- data/mrblib/differ.rb +3 -1
- data/mrblib/error_boundary.rb +19 -27
- data/mrblib/form_builder.rb +28 -24
- data/mrblib/funicular.rb +2 -1
- data/mrblib/html_serializer.rb +14 -13
- data/mrblib/http.rb +12 -1
- data/mrblib/patcher.rb +12 -9
- data/mrblib/router.rb +9 -5
- data/mrblib/runtime.rb +28 -0
- data/mrblib/styles.rb +13 -17
- data/mrblib/vdom.rb +90 -9
- data/mrblib/view_context.rb +135 -0
- data/sig/component.rbs +34 -85
- data/sig/error_boundary.rbs +4 -4
- data/sig/form_builder.rbs +2 -1
- data/sig/html_serializer.rbs +2 -1
- data/sig/http.rbs +1 -0
- data/sig/patcher.rbs +1 -1
- data/sig/router.rbs +2 -13
- data/sig/runtime.rbs +13 -0
- data/sig/styles.rbs +3 -4
- data/sig/vdom.rbs +11 -2
- data/sig/view_context.rbs +47 -0
- metadata +18 -5
- data/lib/funicular/vendor/picorbc/VERSION +0 -1
- data/lib/funicular/vendor/picorbc/picorbc.wasm +0 -0
data/lib/funicular/version.rb
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
class FunicularChatComponent < Funicular::Component
|
|
2
|
-
styles do
|
|
3
|
-
shell "funicular-chat"
|
|
4
|
-
panel "funicular-chat__panel"
|
|
5
|
-
header "funicular-chat__header"
|
|
6
|
-
title "funicular-chat__title"
|
|
7
|
-
subtitle "funicular-chat__subtitle"
|
|
8
|
-
messages "funicular-chat__messages"
|
|
9
|
-
empty "funicular-chat__empty"
|
|
10
|
-
message "funicular-chat__message"
|
|
11
|
-
name "funicular-chat__message-name"
|
|
12
|
-
body "funicular-chat__message-body"
|
|
13
|
-
form "funicular-chat__form"
|
|
14
|
-
row "funicular-chat__form-row"
|
|
15
|
-
input "funicular-chat__input"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
2
|
+
styles do |css|
|
|
3
|
+
css.define :shell, "funicular-chat"
|
|
4
|
+
css.define :panel, "funicular-chat__panel"
|
|
5
|
+
css.define :header, "funicular-chat__header"
|
|
6
|
+
css.define :title, "funicular-chat__title"
|
|
7
|
+
css.define :subtitle, "funicular-chat__subtitle"
|
|
8
|
+
css.define :messages, "funicular-chat__messages"
|
|
9
|
+
css.define :empty, "funicular-chat__empty"
|
|
10
|
+
css.define :message, "funicular-chat__message"
|
|
11
|
+
css.define :name, "funicular-chat__message-name"
|
|
12
|
+
css.define :body, "funicular-chat__message-body"
|
|
13
|
+
css.define :form, "funicular-chat__form"
|
|
14
|
+
css.define :row, "funicular-chat__form-row"
|
|
15
|
+
css.define :input, "funicular-chat__input"
|
|
16
|
+
css.define :button,
|
|
17
|
+
base: "funicular-chat__button",
|
|
18
|
+
variants: {
|
|
19
|
+
enabled: "funicular-chat__button--enabled",
|
|
20
|
+
disabled: "funicular-chat__button--disabled"
|
|
21
|
+
}
|
|
22
|
+
css.define :error, "funicular-chat__error"
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def initialize_state
|
|
@@ -55,7 +56,7 @@ class FunicularChatComponent < Funicular::Component
|
|
|
55
56
|
def subscribe
|
|
56
57
|
@consumer = Funicular::Cable.create_consumer("/cable")
|
|
57
58
|
@subscription = @consumer.subscriptions.create(channel: "FunicularChatChannel") do |message|
|
|
58
|
-
patch(messages: state
|
|
59
|
+
patch(messages: state[:messages] + [message])
|
|
59
60
|
end
|
|
60
61
|
@subscription.on_connected do
|
|
61
62
|
patch(connected: true)
|
|
@@ -72,10 +73,10 @@ class FunicularChatComponent < Funicular::Component
|
|
|
72
73
|
|
|
73
74
|
def send_message(event = nil)
|
|
74
75
|
event.preventDefault if event
|
|
75
|
-
return if state
|
|
76
|
+
return if state[:sending]
|
|
76
77
|
|
|
77
|
-
name = state
|
|
78
|
-
body = state
|
|
78
|
+
name = state[:name].to_s.strip
|
|
79
|
+
body = state[:body].to_s.strip
|
|
79
80
|
if name == "" || body == ""
|
|
80
81
|
patch(error: "Name and message are required")
|
|
81
82
|
return
|
|
@@ -94,38 +95,38 @@ class FunicularChatComponent < Funicular::Component
|
|
|
94
95
|
end
|
|
95
96
|
end
|
|
96
97
|
|
|
97
|
-
def render
|
|
98
|
-
div(class:
|
|
99
|
-
div(class:
|
|
100
|
-
div(class:
|
|
101
|
-
h1(class:
|
|
102
|
-
p(class:
|
|
103
|
-
state
|
|
98
|
+
def render(h)
|
|
99
|
+
h.div(class: h.styles[:shell]) do |hh|
|
|
100
|
+
hh.div(class: h.styles[:panel]) do |panel|
|
|
101
|
+
panel.div(class: h.styles[:header]) do |header|
|
|
102
|
+
header.h1(class: h.styles[:title]) { "Funicular Chat" }
|
|
103
|
+
header.p(class: h.styles[:subtitle]) do
|
|
104
|
+
state[:connected] ? "Realtime Ruby chat over ActionCable" : "Connecting..."
|
|
104
105
|
end
|
|
105
106
|
end
|
|
106
107
|
|
|
107
|
-
div(class:
|
|
108
|
-
if state
|
|
109
|
-
p(class:
|
|
108
|
+
panel.div(class: h.styles[:messages]) do |messages|
|
|
109
|
+
if state[:messages].empty?
|
|
110
|
+
messages.p(class: h.styles[:empty]) { "No messages yet." }
|
|
110
111
|
else
|
|
111
|
-
state
|
|
112
|
-
div(class:
|
|
113
|
-
div(class:
|
|
114
|
-
div(class:
|
|
112
|
+
state[:messages].each do |message|
|
|
113
|
+
messages.div(class: h.styles[:message], key: message["id"]) do |item|
|
|
114
|
+
item.div(class: h.styles[:name]) { message["name"] }
|
|
115
|
+
item.div(class: h.styles[:body]) { message["body"] }
|
|
115
116
|
end
|
|
116
117
|
end
|
|
117
118
|
end
|
|
118
119
|
end
|
|
119
120
|
|
|
120
|
-
form(onsubmit: :send_message, class:
|
|
121
|
-
if state
|
|
122
|
-
div(class:
|
|
121
|
+
panel.form(onsubmit: :send_message, class: h.styles[:form]) do |form|
|
|
122
|
+
if state[:error]
|
|
123
|
+
form.div(class: h.styles[:error]) { state[:error] }
|
|
123
124
|
end
|
|
124
|
-
div(class:
|
|
125
|
-
input(type: "text", value: state
|
|
126
|
-
input(type: "text", value: state
|
|
127
|
-
button(type: "submit", class:
|
|
128
|
-
state
|
|
125
|
+
form.div(class: h.styles[:row]) do |row|
|
|
126
|
+
row.input(type: "text", value: state[:name], placeholder: "Name", oninput: :update_name, class: h.styles[:input])
|
|
127
|
+
row.input(type: "text", value: state[:body], placeholder: "Message", oninput: :update_body, class: h.styles[:input])
|
|
128
|
+
row.button(type: "submit", class: h.styles[:button, state[:sending] ? :disabled : :enabled], disabled: state[:sending]) do
|
|
129
|
+
state[:sending] ? "Sending..." : "Send"
|
|
129
130
|
end
|
|
130
131
|
end
|
|
131
132
|
end
|
data/lib/tasks/funicular.rake
CHANGED
|
@@ -29,7 +29,7 @@ namespace :funicular do
|
|
|
29
29
|
rescue Funicular::Plugin::Error => e
|
|
30
30
|
puts "ERROR: #{e.message}"
|
|
31
31
|
exit 1
|
|
32
|
-
rescue Funicular::Compiler::
|
|
32
|
+
rescue Funicular::Compiler::MrbcMissingError => e
|
|
33
33
|
puts "ERROR: #{e.message}"
|
|
34
34
|
exit 1
|
|
35
35
|
rescue => e
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
require "test_helper"
|
|
7
|
+
require_relative "support/rails_stub"
|
|
8
|
+
require "funicular/commands/routes"
|
|
9
|
+
|
|
10
|
+
# Exercises the `funicular:routes` CLI command: its guard clauses (not a Rails
|
|
11
|
+
# app / no initializer / no routes) and the aligned routes table it prints.
|
|
12
|
+
class CommandsRoutesTest < Minitest::Test
|
|
13
|
+
def setup
|
|
14
|
+
Rails.reset_stub!
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def teardown
|
|
18
|
+
Rails.reset_stub!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Runs Routes#execute inside a throwaway app dir, capturing stdout and the
|
|
22
|
+
# SystemExit status (nil when the command runs to completion).
|
|
23
|
+
def run_in_app(files: {})
|
|
24
|
+
Dir.mktmpdir do |dir|
|
|
25
|
+
files.each do |rel, contents|
|
|
26
|
+
path = File.join(dir, rel)
|
|
27
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
28
|
+
File.write(path, contents)
|
|
29
|
+
end
|
|
30
|
+
Rails.root = Pathname(dir)
|
|
31
|
+
|
|
32
|
+
status = nil
|
|
33
|
+
out = +""
|
|
34
|
+
Dir.chdir(dir) do
|
|
35
|
+
out, = capture_io do
|
|
36
|
+
begin
|
|
37
|
+
Funicular::Commands::Routes.new.execute
|
|
38
|
+
rescue SystemExit => e
|
|
39
|
+
status = e.status
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
yield out, status
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_errors_when_not_in_a_rails_app
|
|
48
|
+
run_in_app(files: {}) do |out, status|
|
|
49
|
+
assert_equal 1, status
|
|
50
|
+
assert_includes out, "Not in a Rails application"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_reports_when_initializer_is_missing
|
|
55
|
+
run_in_app(files: { "config/application.rb" => "# app\n" }) do |out, status|
|
|
56
|
+
assert_equal 0, status
|
|
57
|
+
assert_includes out, "No Funicular routes found"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_reports_when_initializer_defines_no_routes
|
|
62
|
+
run_in_app(files: {
|
|
63
|
+
"config/application.rb" => "# app\n",
|
|
64
|
+
"app/funicular/initializer.rb" => "# nothing here\n"
|
|
65
|
+
}) do |out, status|
|
|
66
|
+
assert_equal 0, status
|
|
67
|
+
assert_includes out, "No routes defined"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_prints_aligned_table_with_totals
|
|
72
|
+
initializer = <<~RUBY
|
|
73
|
+
router.get("/", to: HomeComponent, as: "home")
|
|
74
|
+
router.get("/channels/:id", to: ChannelComponent)
|
|
75
|
+
RUBY
|
|
76
|
+
|
|
77
|
+
run_in_app(files: {
|
|
78
|
+
"config/application.rb" => "# app\n",
|
|
79
|
+
"app/funicular/initializer.rb" => initializer
|
|
80
|
+
}) do |out, status|
|
|
81
|
+
assert_nil status # ran to completion, no exit
|
|
82
|
+
assert_includes out, "Method"
|
|
83
|
+
assert_includes out, "Component"
|
|
84
|
+
assert_includes out, "HomeComponent"
|
|
85
|
+
assert_includes out, "home_path"
|
|
86
|
+
assert_includes out, "/channels/:id"
|
|
87
|
+
assert_includes out, "Total: 2 routes"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_total_is_singular_for_one_route
|
|
92
|
+
routes = [{ method: "GET", path: "/", component: "HomeComponent", helper: nil }]
|
|
93
|
+
out, = capture_io { Funicular::Commands::Routes.new.send(:print_routes_table, routes) }
|
|
94
|
+
assert_includes out, "Total: 1 route"
|
|
95
|
+
refute_includes out, "1 routes"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
require "test_helper"
|
|
7
|
+
require_relative "support/rails_stub"
|
|
8
|
+
|
|
9
|
+
# Exercises the environment-independent parts of Funicular::Compiler: the
|
|
10
|
+
# source-file discovery/ordering (shared with the SSR runtime), the `node`
|
|
11
|
+
# executable lookup, and the vendored-mrbc availability guard. The actual
|
|
12
|
+
# spawn of mrbc via Node is integration-only and not covered here.
|
|
13
|
+
class CompilerTest < Minitest::Test
|
|
14
|
+
def make_compiler(source_dir: "/tmp/app", output_file: "/tmp/out.mrb", **opts)
|
|
15
|
+
Funicular::Compiler.new(source_dir: source_dir, output_file: output_file, **opts)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# --- source_files ordering -------------------------------------------
|
|
19
|
+
|
|
20
|
+
def test_source_files_orders_models_stores_components_then_initializers
|
|
21
|
+
Dir.mktmpdir do |dir|
|
|
22
|
+
%w[models stores components].each { |d| FileUtils.mkdir_p(File.join(dir, d)) }
|
|
23
|
+
File.write(File.join(dir, "models", "b_model.rb"), "")
|
|
24
|
+
File.write(File.join(dir, "models", "a_model.rb"), "")
|
|
25
|
+
File.write(File.join(dir, "stores", "s_store.rb"), "")
|
|
26
|
+
File.write(File.join(dir, "components", "c_component.rb"), "")
|
|
27
|
+
File.write(File.join(dir, "app_initializer.rb"), "")
|
|
28
|
+
File.write(File.join(dir, "initializer.rb"), "")
|
|
29
|
+
|
|
30
|
+
files = Funicular::Compiler.source_files(dir).map { |f| f.sub("#{dir}/", "") }
|
|
31
|
+
|
|
32
|
+
assert_equal(
|
|
33
|
+
[
|
|
34
|
+
"models/a_model.rb",
|
|
35
|
+
"models/b_model.rb",
|
|
36
|
+
"stores/s_store.rb",
|
|
37
|
+
"components/c_component.rb",
|
|
38
|
+
"app_initializer.rb",
|
|
39
|
+
"initializer.rb"
|
|
40
|
+
],
|
|
41
|
+
files
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_source_files_recurses_into_subdirectories
|
|
47
|
+
Dir.mktmpdir do |dir|
|
|
48
|
+
FileUtils.mkdir_p(File.join(dir, "components", "nested"))
|
|
49
|
+
File.write(File.join(dir, "components", "nested", "deep_component.rb"), "")
|
|
50
|
+
|
|
51
|
+
files = Funicular::Compiler.source_files(dir)
|
|
52
|
+
|
|
53
|
+
assert_equal 1, files.size
|
|
54
|
+
assert_match %r{components/nested/deep_component\.rb\z}, files.first
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_source_files_empty_for_missing_directory
|
|
59
|
+
assert_equal [], Funicular::Compiler.source_files("/no/such/dir")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# --- node executable lookup ------------------------------------------
|
|
63
|
+
|
|
64
|
+
def test_node_command_prefers_node_env_var
|
|
65
|
+
original = ENV["NODE"]
|
|
66
|
+
ENV["NODE"] = "/custom/bin/node"
|
|
67
|
+
assert_equal "/custom/bin/node", make_compiler.send(:node_command)
|
|
68
|
+
ensure
|
|
69
|
+
ENV["NODE"] = original
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def test_which_finds_executable_on_path
|
|
73
|
+
Dir.mktmpdir do |dir|
|
|
74
|
+
exe = File.join(dir, "faketool")
|
|
75
|
+
File.write(exe, "#!/bin/sh\n")
|
|
76
|
+
File.chmod(0o755, exe)
|
|
77
|
+
|
|
78
|
+
original = ENV["PATH"]
|
|
79
|
+
ENV["PATH"] = dir
|
|
80
|
+
assert_equal exe, make_compiler.send(:which, "faketool")
|
|
81
|
+
assert_nil make_compiler.send(:which, "does_not_exist_tool")
|
|
82
|
+
ensure
|
|
83
|
+
ENV["PATH"] = original
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# --- availability guard ----------------------------------------------
|
|
88
|
+
|
|
89
|
+
def test_compile_raises_when_vendored_mrbc_missing
|
|
90
|
+
skip "vendored mrbc present in this checkout" if File.exist?(Funicular::Compiler::MRBC_JS)
|
|
91
|
+
|
|
92
|
+
error = assert_raises(Funicular::Compiler::MrbcMissingError) { make_compiler.compile }
|
|
93
|
+
assert_includes error.message, "Vendored mrbc not found"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_prepend_source_files_are_coerced_to_strings
|
|
97
|
+
compiler = make_compiler(prepend_source_files: [Pathname.new("/a.rb"), :b])
|
|
98
|
+
assert_equal ["/a.rb", "b"], compiler.prepend_source_files
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# --- source gathering (writes the FUNICULAR_ENV temp file) -----------
|
|
102
|
+
|
|
103
|
+
def test_gather_source_files_prepends_extras_and_writes_env_file
|
|
104
|
+
Rails.reset_stub!
|
|
105
|
+
Rails.env_name = "test"
|
|
106
|
+
Dir.mktmpdir do |dir|
|
|
107
|
+
FileUtils.mkdir_p(File.join(dir, "components"))
|
|
108
|
+
File.write(File.join(dir, "components", "a_component.rb"), "")
|
|
109
|
+
output = File.join(dir, "build", "app.mrb")
|
|
110
|
+
|
|
111
|
+
compiler = make_compiler(source_dir: dir, output_file: output,
|
|
112
|
+
prepend_source_files: ["/plugin/x.rb"])
|
|
113
|
+
compiler.send(:gather_source_files)
|
|
114
|
+
|
|
115
|
+
gathered = compiler.instance_variable_get(:@source_files)
|
|
116
|
+
assert_equal "/plugin/x.rb", gathered.first
|
|
117
|
+
assert(gathered.any? { |f| f.end_with?("a_component.rb") })
|
|
118
|
+
|
|
119
|
+
env_file = compiler.instance_variable_get(:@env_file)
|
|
120
|
+
assert_match(/FUNICULAR_ENV.*test/, File.read(env_file))
|
|
121
|
+
ensure
|
|
122
|
+
Rails.reset_stub!
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def test_gather_source_files_raises_without_ruby_files
|
|
127
|
+
Dir.mktmpdir do |dir|
|
|
128
|
+
compiler = make_compiler(source_dir: dir, output_file: File.join(dir, "out.mrb"))
|
|
129
|
+
error = assert_raises(RuntimeError) { compiler.send(:gather_source_files) }
|
|
130
|
+
assert_includes error.message, "No Ruby files found"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# --- compile_to_mrb (spawns the mrbc command) ------------------------
|
|
135
|
+
#
|
|
136
|
+
# `true`/`false` stand in for `node mrbc.js ...`: both ignore their argv, so
|
|
137
|
+
# the exit status alone drives the success/failure branches without needing a
|
|
138
|
+
# real Node/WASM toolchain.
|
|
139
|
+
|
|
140
|
+
def prepared_compiler(dir, node:, debug_mode: false)
|
|
141
|
+
src = File.join(dir, "a.rb")
|
|
142
|
+
File.write(src, "")
|
|
143
|
+
compiler = make_compiler(source_dir: dir, output_file: File.join(dir, "out.mrb"),
|
|
144
|
+
debug_mode: debug_mode)
|
|
145
|
+
compiler.instance_variable_set(:@node_command, node)
|
|
146
|
+
compiler.instance_variable_set(:@source_files, [src])
|
|
147
|
+
compiler.instance_variable_set(:@env_file, nil)
|
|
148
|
+
compiler
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def test_compile_to_mrb_succeeds_when_command_exits_zero
|
|
152
|
+
Dir.mktmpdir do |dir|
|
|
153
|
+
compiler = prepared_compiler(dir, node: "/bin/true", debug_mode: true)
|
|
154
|
+
out, = capture_io { compiler.send(:compile_to_mrb) }
|
|
155
|
+
assert_includes out, "Successfully compiled"
|
|
156
|
+
assert_includes out, "Debug mode: true"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def test_compile_to_mrb_raises_when_command_fails
|
|
161
|
+
Dir.mktmpdir do |dir|
|
|
162
|
+
compiler = prepared_compiler(dir, node: "/bin/false")
|
|
163
|
+
error = nil
|
|
164
|
+
capture_io do
|
|
165
|
+
error = assert_raises(RuntimeError) { compiler.send(:compile_to_mrb) }
|
|
166
|
+
end
|
|
167
|
+
assert_includes error.message, "Failed to compile with mrbc"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def test_compile_to_mrb_deletes_env_file_unless_kept
|
|
172
|
+
Dir.mktmpdir do |dir|
|
|
173
|
+
compiler = prepared_compiler(dir, node: "/bin/true")
|
|
174
|
+
env_file = File.join(dir, "out.mrb.env.rb")
|
|
175
|
+
File.write(env_file, "ENV['FUNICULAR_ENV'] = 'test'\n")
|
|
176
|
+
compiler.instance_variable_set(:@env_file, env_file)
|
|
177
|
+
|
|
178
|
+
capture_io { compiler.send(:compile_to_mrb) }
|
|
179
|
+
refute File.exist?(env_file), "temp env file should be removed"
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def test_logger_is_used_when_provided
|
|
184
|
+
Dir.mktmpdir do |dir|
|
|
185
|
+
messages = []
|
|
186
|
+
fake_logger = Object.new
|
|
187
|
+
fake_logger.define_singleton_method(:info) { |m| messages << m }
|
|
188
|
+
compiler = prepared_compiler(dir, node: "/bin/true")
|
|
189
|
+
compiler.instance_variable_set(:@logger, fake_logger)
|
|
190
|
+
|
|
191
|
+
compiler.send(:compile_to_mrb)
|
|
192
|
+
assert(messages.any? { |m| m.include?("Successfully compiled") })
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
# Exercises Funicular::Configuration: the per-environment source selection, the
|
|
6
|
+
# source allowlist validation, and the cdn_version fallback to the vendored
|
|
7
|
+
# wasm version.
|
|
8
|
+
class ConfigurationTest < Minitest::Test
|
|
9
|
+
def setup
|
|
10
|
+
@config = Funicular::Configuration.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_defaults
|
|
14
|
+
assert_equal :local_debug, @config.development_source
|
|
15
|
+
assert_equal :local_debug, @config.test_source
|
|
16
|
+
assert_equal :local_dist, @config.production_source
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_source_for_known_environments
|
|
20
|
+
assert_equal :local_debug, @config.source_for("development")
|
|
21
|
+
assert_equal :local_debug, @config.source_for("test")
|
|
22
|
+
assert_equal :local_dist, @config.source_for("production")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_source_for_accepts_symbols
|
|
26
|
+
assert_equal :local_dist, @config.source_for(:production)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_source_for_unknown_environment_falls_back_to_development
|
|
30
|
+
assert_equal :local_debug, @config.source_for("staging")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_setters_accept_valid_sources
|
|
34
|
+
@config.development_source = :cdn
|
|
35
|
+
@config.test_source = "local_dist"
|
|
36
|
+
@config.production_source = :cdn
|
|
37
|
+
|
|
38
|
+
assert_equal :cdn, @config.development_source
|
|
39
|
+
assert_equal :local_dist, @config.test_source
|
|
40
|
+
assert_equal :cdn, @config.source_for("production")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_setters_reject_invalid_sources
|
|
44
|
+
error = assert_raises(ArgumentError) { @config.production_source = :nonsense }
|
|
45
|
+
assert_includes error.message, "Invalid Funicular source"
|
|
46
|
+
assert_includes error.message, "nonsense"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def test_cdn_version_prefers_explicit_value
|
|
50
|
+
@config.cdn_version = "9.9.9"
|
|
51
|
+
assert_equal "9.9.9", @config.cdn_version
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_cdn_version_falls_back_to_vendored_wasm_version
|
|
55
|
+
# No explicit version set -> delegates to Funicular.vendored_wasm_version
|
|
56
|
+
# (nil in a source checkout without vendored artifacts).
|
|
57
|
+
vendored = Funicular.vendored_wasm_version
|
|
58
|
+
if vendored.nil?
|
|
59
|
+
assert_nil @config.cdn_version
|
|
60
|
+
else
|
|
61
|
+
assert_equal vendored, @config.cdn_version
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -3,12 +3,12 @@ class GreetingComponent < Funicular::Component
|
|
|
3
3
|
{ title: "Default Title", items: [] }
|
|
4
4
|
end
|
|
5
5
|
|
|
6
|
-
def render
|
|
7
|
-
div(class: "greeting") do
|
|
8
|
-
h1 { state
|
|
9
|
-
ul do
|
|
10
|
-
state
|
|
11
|
-
li(key: item["id"]) { item["name"] }
|
|
6
|
+
def render(h)
|
|
7
|
+
h.div(class: "greeting") do |hh|
|
|
8
|
+
hh.h1 { state[:title] }
|
|
9
|
+
hh.ul do |hhh|
|
|
10
|
+
state[:items].each do |item|
|
|
11
|
+
hhh.li(key: item["id"]) { item["name"] }
|
|
12
12
|
end
|
|
13
13
|
end
|
|
14
14
|
end
|
data/minitest/form_for_test.rb
CHANGED
|
@@ -19,8 +19,8 @@ class FormForTest < Minitest::Test
|
|
|
19
19
|
@submitted = data
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def render
|
|
23
|
-
form_for(:comment, on_submit: :handle_submit) do |f|
|
|
22
|
+
def render(h)
|
|
23
|
+
h.form_for(:comment, on_submit: :handle_submit) do |f|
|
|
24
24
|
f.textarea(:body)
|
|
25
25
|
f.submit("Post")
|
|
26
26
|
end
|
data/minitest/funicular_test.rb
CHANGED
|
@@ -2,12 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
require "test_helper"
|
|
4
4
|
|
|
5
|
+
# Exercises the top-level Funicular module: version constant, the
|
|
6
|
+
# configuration accessor/DSL, and the vendored wasm version reader.
|
|
5
7
|
class FunicularTest < Minitest::Test
|
|
6
8
|
def test_VERSION
|
|
7
9
|
assert ::Funicular.const_defined?(:VERSION)
|
|
10
|
+
assert_match(/\A\d+\.\d+\.\d+/, Funicular::VERSION)
|
|
8
11
|
end
|
|
9
12
|
|
|
10
|
-
def
|
|
11
|
-
|
|
13
|
+
def test_configuration_is_memoized
|
|
14
|
+
assert_instance_of Funicular::Configuration, Funicular.configuration
|
|
15
|
+
assert_same Funicular.configuration, Funicular.configuration
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_configure_yields_the_configuration
|
|
19
|
+
yielded = nil
|
|
20
|
+
Funicular.configure { |config| yielded = config }
|
|
21
|
+
assert_same Funicular.configuration, yielded
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_vendored_wasm_version_reads_version_file
|
|
25
|
+
# The vendored artifacts are not present in a source checkout, so the
|
|
26
|
+
# reader swallows Errno::ENOENT and returns nil. When they are present it
|
|
27
|
+
# returns the trimmed VERSION file contents.
|
|
28
|
+
version_file = File.join(Funicular::VENDOR_PICORUBY_DIR, "VERSION")
|
|
29
|
+
Funicular.instance_variable_set(:@vendored_wasm_version, nil)
|
|
30
|
+
|
|
31
|
+
if File.exist?(version_file)
|
|
32
|
+
assert_equal File.read(version_file).strip, Funicular.vendored_wasm_version
|
|
33
|
+
else
|
|
34
|
+
assert_nil Funicular.vendored_wasm_version
|
|
35
|
+
end
|
|
36
|
+
ensure
|
|
37
|
+
Funicular.instance_variable_set(:@vendored_wasm_version, nil)
|
|
12
38
|
end
|
|
13
39
|
end
|
data/minitest/hydration_test.rb
CHANGED
|
@@ -25,8 +25,8 @@ class HydrationMatchTest < Minitest::Test
|
|
|
25
25
|
# invoked here (we feed vnodes directly); it only satisfies the abstract API.
|
|
26
26
|
def probe
|
|
27
27
|
klass = Class.new(Funicular::Component) do
|
|
28
|
-
def render
|
|
29
|
-
div { "x" }
|
|
28
|
+
def render(h)
|
|
29
|
+
h.div { "x" }
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def match?(vnode, dom)
|