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
|
@@ -0,0 +1,267 @@
|
|
|
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/testing"
|
|
9
|
+
|
|
10
|
+
# Exercises the CRuby-side glue around the PicoRuby ("picotest") client test
|
|
11
|
+
# runner: the Result value object's tally math, the assertion bridging in
|
|
12
|
+
# Funicular::Testing.assert_picotests, and NodeRunner's pure output helpers.
|
|
13
|
+
# The actual Node/WASM subprocess launch (NodeRunner#run, Testing.run!) is
|
|
14
|
+
# integration-only and not exercised here.
|
|
15
|
+
class TestingTest < Minitest::Test
|
|
16
|
+
Runner = Funicular::Testing::NodeRunner
|
|
17
|
+
Result = Runner::Result
|
|
18
|
+
|
|
19
|
+
# Stand-in for Process::Status: success? is all Result reads from it.
|
|
20
|
+
Status = Struct.new(:ok) do
|
|
21
|
+
def success? = ok
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Minimal Minitest::Test stand-in so assert_picotests can drive `assert`
|
|
25
|
+
# and mutate `assertions` without touching this test's own counters.
|
|
26
|
+
class FakeTestCase
|
|
27
|
+
attr_accessor :assertions
|
|
28
|
+
|
|
29
|
+
def initialize
|
|
30
|
+
@assertions = 0
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def assert(cond, msg = nil)
|
|
34
|
+
@assertions += 1
|
|
35
|
+
raise Minitest::Assertion, msg.to_s unless cond
|
|
36
|
+
|
|
37
|
+
true
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def result(picotest_results, ok: true, stdout: "out", stderr: "")
|
|
42
|
+
Result.new(status: Status.new(ok), stdout: stdout, stderr: stderr,
|
|
43
|
+
picotest_results: picotest_results)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def sample_results
|
|
47
|
+
{
|
|
48
|
+
"AlphaTest" => { "success_count" => 3, "failures" => [{}], "skipped_count" => 1 },
|
|
49
|
+
"BetaTest" => { "success_count" => 2, "exceptions" => [{}], "crashes" => [{}] }
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# --- Result tallies ---------------------------------------------------
|
|
54
|
+
|
|
55
|
+
def test_assertion_count_sums_successes_failures_exceptions_crashes
|
|
56
|
+
# 3 + 1 (fail) + 2 + 1 (exc) + 1 (crash) = 8
|
|
57
|
+
assert_equal 8, result(sample_results).picotest_assertion_count
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_skip_and_test_counts
|
|
61
|
+
r = result(sample_results)
|
|
62
|
+
assert_equal 1, r.picotest_skip_count
|
|
63
|
+
assert_equal 9, r.picotest_test_count # 8 assertions + 1 skip
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_failure_exception_crash_counts
|
|
67
|
+
r = result(sample_results)
|
|
68
|
+
assert_equal 1, r.picotest_failure_count
|
|
69
|
+
assert_equal 1, r.picotest_exception_count
|
|
70
|
+
assert_equal 1, r.picotest_crash_count
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_counts_tolerate_missing_keys_and_nil_results
|
|
74
|
+
empty = result(nil)
|
|
75
|
+
assert_equal 0, empty.picotest_assertion_count
|
|
76
|
+
assert_equal 0, empty.picotest_skip_count
|
|
77
|
+
|
|
78
|
+
sparse = result({ "T" => {} })
|
|
79
|
+
assert_equal 0, sparse.picotest_assertion_count
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_success_and_output
|
|
83
|
+
assert_predicate result({}, ok: true), :success?
|
|
84
|
+
refute_predicate result({}, ok: false), :success?
|
|
85
|
+
|
|
86
|
+
# output joins non-empty stdout/stderr with a newline.
|
|
87
|
+
assert_equal "out\nboom", result({}, stdout: "out", stderr: "boom").output
|
|
88
|
+
assert_equal "out", result({}, stdout: "out", stderr: "").output
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_summary_reports_every_dimension
|
|
92
|
+
summary = result(sample_results).picotest_summary
|
|
93
|
+
assert_includes summary, "9 tests"
|
|
94
|
+
assert_includes summary, "8 assertions"
|
|
95
|
+
assert_includes summary, "1 failures"
|
|
96
|
+
assert_includes summary, "1 exceptions"
|
|
97
|
+
assert_includes summary, "1 crashes"
|
|
98
|
+
assert_includes summary, "1 skips"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# --- Testing.assert_picotests ----------------------------------------
|
|
102
|
+
|
|
103
|
+
def test_assert_picotests_passes_and_bridges_assertion_count
|
|
104
|
+
tc = FakeTestCase.new
|
|
105
|
+
out, = capture_io do
|
|
106
|
+
Funicular::Testing.assert_picotests(tc, result(sample_results))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
assert_includes out, "Funicular picotest:"
|
|
110
|
+
# 1 for the pass/fail `assert`, plus (assertion_count - 1) bridged.
|
|
111
|
+
assert_equal 8, tc.assertions
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def test_assert_picotests_can_suppress_summary
|
|
115
|
+
tc = FakeTestCase.new
|
|
116
|
+
out, = capture_io do
|
|
117
|
+
Funicular::Testing.assert_picotests(tc, result(sample_results), print_summary: false)
|
|
118
|
+
end
|
|
119
|
+
assert_equal "", out
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def test_assert_picotests_fails_with_output_when_run_failed
|
|
123
|
+
tc = FakeTestCase.new
|
|
124
|
+
r = result({}, ok: false, stdout: "boom detail")
|
|
125
|
+
error = nil
|
|
126
|
+
capture_io do
|
|
127
|
+
error = assert_raises(Minitest::Assertion) do
|
|
128
|
+
Funicular::Testing.assert_picotests(tc, r)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
assert_includes error.message, "boom detail"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# --- NodeRunner pure helpers -----------------------------------------
|
|
135
|
+
|
|
136
|
+
def test_strip_ansi_removes_color_codes
|
|
137
|
+
runner = Runner.new(app_root: Dir.pwd)
|
|
138
|
+
assert_equal "hello", runner.send(:strip_ansi, "\e[32mhello\e[0m")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def test_extract_picotest_results_pulls_marker_line_out_of_stdout
|
|
142
|
+
runner = Runner.new(app_root: Dir.pwd)
|
|
143
|
+
marker = Runner::RESULT_MARKER
|
|
144
|
+
stdout = "before\n#{marker}{\"AlphaTest\":{\"success_count\":1}}\nafter\n"
|
|
145
|
+
|
|
146
|
+
results, remaining = runner.send(:extract_picotest_results, stdout)
|
|
147
|
+
|
|
148
|
+
assert_equal({ "AlphaTest" => { "success_count" => 1 } }, results)
|
|
149
|
+
assert_equal "before\nafter\n", remaining
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def test_extract_picotest_results_ignores_malformed_marker
|
|
153
|
+
runner = Runner.new(app_root: Dir.pwd)
|
|
154
|
+
stdout = "#{Runner::RESULT_MARKER}not-json\n"
|
|
155
|
+
|
|
156
|
+
results, remaining = runner.send(:extract_picotest_results, stdout)
|
|
157
|
+
|
|
158
|
+
assert_equal({}, results)
|
|
159
|
+
assert_equal stdout, remaining
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def test_initialize_defaults_derive_source_dir_from_app_root
|
|
163
|
+
Dir.mktmpdir do |dir|
|
|
164
|
+
runner = Runner.new(app_root: dir)
|
|
165
|
+
assert_equal File.join(dir, "app", "funicular"), runner.source_dir
|
|
166
|
+
assert_equal Runner::DEFAULT_TEST_GLOB, runner.test_glob
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# --- NodeRunner file gathering / manifest ----------------------------
|
|
171
|
+
|
|
172
|
+
def test_source_files_skips_initializers_for_a_generic_source_dir
|
|
173
|
+
Dir.mktmpdir do |dir|
|
|
174
|
+
source = File.join(dir, "src")
|
|
175
|
+
FileUtils.mkdir_p(File.join(source, "components"))
|
|
176
|
+
File.write(File.join(source, "components", "a_component.rb"), "")
|
|
177
|
+
File.write(File.join(source, "top.rb"), "")
|
|
178
|
+
File.write(File.join(source, "initializer.rb"), "")
|
|
179
|
+
File.write(File.join(source, "app_initializer.rb"), "")
|
|
180
|
+
|
|
181
|
+
runner = Runner.new(app_root: dir, source_dir: source)
|
|
182
|
+
basenames = runner.send(:source_files).map { |f| File.basename(f) }
|
|
183
|
+
|
|
184
|
+
assert_includes basenames, "a_component.rb"
|
|
185
|
+
assert_includes basenames, "top.rb"
|
|
186
|
+
refute_includes basenames, "initializer.rb"
|
|
187
|
+
refute_includes basenames, "app_initializer.rb"
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def test_source_files_empty_when_dir_absent
|
|
192
|
+
Dir.mktmpdir do |dir|
|
|
193
|
+
runner = Runner.new(app_root: dir, source_dir: File.join(dir, "missing"))
|
|
194
|
+
assert_equal [], runner.send(:source_files)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def test_test_files_are_globbed_from_app_root
|
|
199
|
+
Dir.mktmpdir do |dir|
|
|
200
|
+
target = File.join(dir, "test", "funicular", "client")
|
|
201
|
+
FileUtils.mkdir_p(target)
|
|
202
|
+
File.write(File.join(target, "x_picotest.rb"), "")
|
|
203
|
+
File.write(File.join(target, "not_a_test.rb"), "")
|
|
204
|
+
|
|
205
|
+
runner = Runner.new(app_root: dir)
|
|
206
|
+
found = runner.send(:test_files).map { |f| File.basename(f) }
|
|
207
|
+
|
|
208
|
+
assert_equal ["x_picotest.rb"], found
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def test_build_manifest_shape
|
|
213
|
+
Dir.mktmpdir do |dir|
|
|
214
|
+
runner = Runner.new(app_root: dir, timeout_ms: 1234)
|
|
215
|
+
manifest = runner.send(:build_manifest)
|
|
216
|
+
|
|
217
|
+
assert_equal dir, manifest[:appRoot]
|
|
218
|
+
assert_equal 1234, manifest[:timeoutMs]
|
|
219
|
+
assert_includes manifest[:html], "<div id=\"app\">"
|
|
220
|
+
assert_kind_of Array, manifest[:sourceFiles]
|
|
221
|
+
assert_kind_of Array, manifest[:testFiles]
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def test_with_manifest_file_writes_json_and_cleans_up
|
|
226
|
+
runner = Runner.new(app_root: Dir.pwd)
|
|
227
|
+
captured = nil
|
|
228
|
+
runner.send(:with_manifest_file, { appRoot: "/x", n: 1 }) do |path|
|
|
229
|
+
captured = path
|
|
230
|
+
assert File.exist?(path)
|
|
231
|
+
assert_equal({ "appRoot" => "/x", "n" => 1 }, JSON.parse(File.read(path)))
|
|
232
|
+
end
|
|
233
|
+
refute File.exist?(captured), "manifest tempfile should be unlinked"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def test_app_root_defaults_to_rails_root
|
|
237
|
+
Dir.mktmpdir do |dir|
|
|
238
|
+
Rails.reset_stub!
|
|
239
|
+
Rails.root = Pathname(dir)
|
|
240
|
+
assert_equal File.expand_path(dir), Runner.new.app_root
|
|
241
|
+
ensure
|
|
242
|
+
Rails.reset_stub!
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def test_source_files_uses_compiler_order_for_a_rails_app_dir
|
|
247
|
+
Dir.mktmpdir do |dir|
|
|
248
|
+
app_funicular = File.join(dir, "app", "funicular")
|
|
249
|
+
FileUtils.mkdir_p(File.join(app_funicular, "models"))
|
|
250
|
+
FileUtils.mkdir_p(File.join(app_funicular, "components"))
|
|
251
|
+
File.write(File.join(app_funicular, "models", "m_model.rb"), "")
|
|
252
|
+
File.write(File.join(app_funicular, "components", "c_component.rb"), "")
|
|
253
|
+
File.write(File.join(app_funicular, "initializer.rb"), "")
|
|
254
|
+
|
|
255
|
+
runner = Runner.new(app_root: dir)
|
|
256
|
+
basenames = runner.send(:source_files).map { |f| File.basename(f) }
|
|
257
|
+
|
|
258
|
+
# Compiler order (models before components) and initializer excluded.
|
|
259
|
+
assert_equal ["m_model.rb", "c_component.rb"], basenames
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def test_runner_js_points_at_the_bundled_mjs
|
|
264
|
+
runner = Runner.new(app_root: Dir.pwd)
|
|
265
|
+
assert_match %r{node_runner\.mjs\z}, runner.send(:runner_js)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class ViewContextTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
Funicular::SSR::Runtime.load_framework!
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_explicit_state_styles_and_resources_do_not_collide_with_html_methods
|
|
11
|
+
klass = Class.new(Funicular::Component) do
|
|
12
|
+
styles { |css| css.define :hash, "hash-class" }
|
|
13
|
+
|
|
14
|
+
use_suspense :data, ->(resolve, _reject) { resolve.call("loaded") }
|
|
15
|
+
|
|
16
|
+
def initialize_state
|
|
17
|
+
{ class: "state-class", hash: "state-hash" }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def render(h)
|
|
21
|
+
h.data(class: h.styles[:hash]) do
|
|
22
|
+
"#{state[:class]} #{state[:hash]} #{h.resources[:data]}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
component = klass.new
|
|
28
|
+
component.instance_variable_get(:@suspense_data)[:data] = "loaded"
|
|
29
|
+
component.instance_variable_get(:@suspense_states)[:data] = :resolved
|
|
30
|
+
|
|
31
|
+
vnode = component.build_vdom
|
|
32
|
+
|
|
33
|
+
assert_equal "data", vnode.tag
|
|
34
|
+
assert_equal "hash-class", vnode.props[:class]
|
|
35
|
+
assert_equal ["state-class state-hash loaded"], vnode.children
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_component_children_are_vdom_children_not_props
|
|
39
|
+
child = Class.new(Funicular::Component) do
|
|
40
|
+
def render(h)
|
|
41
|
+
h.div { children }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
parent = Class.new(Funicular::Component) do
|
|
46
|
+
define_method(:render) do |h|
|
|
47
|
+
h.component(child, title: "x") do |hh|
|
|
48
|
+
hh.span { "inside" }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
vnode = parent.new.build_vdom
|
|
54
|
+
|
|
55
|
+
assert_instance_of Funicular::VDOM::Component, vnode
|
|
56
|
+
assert_equal({ title: "x" }, vnode.props)
|
|
57
|
+
refute_includes vnode.props.keys, :children_block
|
|
58
|
+
assert_equal 1, vnode.children.length
|
|
59
|
+
assert_equal "span", vnode.children.first.tag
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_route_helpers_are_runtime_scoped
|
|
63
|
+
router_a = Funicular::Router.new(nil)
|
|
64
|
+
router_b = Funicular::Router.new(nil)
|
|
65
|
+
router_a.get("/users/:id", to: Class.new(Funicular::Component), as: "user")
|
|
66
|
+
router_b.get("/accounts/:id", to: Class.new(Funicular::Component), as: "user")
|
|
67
|
+
|
|
68
|
+
component_a = Class.new(Funicular::Component).new
|
|
69
|
+
component_b = Class.new(Funicular::Component).new
|
|
70
|
+
component_a.runtime = Funicular::Runtime.new(router_a)
|
|
71
|
+
component_b.runtime = Funicular::Runtime.new(router_b)
|
|
72
|
+
|
|
73
|
+
assert_equal "/users/7", Funicular::ViewContext.new(component_a).routes.user_path(7)
|
|
74
|
+
assert_equal "/accounts/7", Funicular::ViewContext.new(component_b).routes.user_path(7)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_child_collector_is_restored_when_render_raises
|
|
78
|
+
klass = Class.new(Funicular::Component) do
|
|
79
|
+
attr_reader :captured
|
|
80
|
+
|
|
81
|
+
def render(h)
|
|
82
|
+
begin
|
|
83
|
+
h.div do |hh|
|
|
84
|
+
hh.span { "before" }
|
|
85
|
+
raise "boom"
|
|
86
|
+
end
|
|
87
|
+
rescue
|
|
88
|
+
@captured = current_children
|
|
89
|
+
h.div { "after" }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
instance = klass.new
|
|
95
|
+
vnode = instance.build_vdom
|
|
96
|
+
|
|
97
|
+
assert_nil instance.current_children
|
|
98
|
+
assert_equal "div", vnode.tag
|
|
99
|
+
assert_equal ["after"], vnode.children
|
|
100
|
+
end
|
|
101
|
+
end
|