funicular 0.0.1 → 0.1.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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -1
  3. data/README.md +58 -20
  4. data/Rakefile +74 -2
  5. data/demo/keymap_editor.html +582 -0
  6. data/demo/test_cable.html +179 -0
  7. data/demo/test_chartjs.html +235 -0
  8. data/demo/test_component.html +201 -0
  9. data/demo/test_diff_patch.html +146 -0
  10. data/demo/test_error_boundary.html +284 -0
  11. data/demo/test_router.html +257 -0
  12. data/demo/test_vdom.html +100 -0
  13. data/demo/tic-tac-toe.html +201 -0
  14. data/docs/README.md +419 -0
  15. data/docs/advanced-features.md +632 -0
  16. data/docs/architecture.md +409 -0
  17. data/docs/components-and-state.md +539 -0
  18. data/docs/data-fetching.md +528 -0
  19. data/docs/forms.md +446 -0
  20. data/docs/rails-integration.md +426 -0
  21. data/docs/realtime.md +543 -0
  22. data/docs/routing-and-navigation.md +427 -0
  23. data/docs/styling.md +285 -0
  24. data/exe/funicular +32 -0
  25. data/lib/funicular/assets/funicular.rb +21 -0
  26. data/lib/funicular/assets/funicular_debug.css +73 -0
  27. data/lib/funicular/assets/funicular_debug.js +183 -0
  28. data/lib/funicular/commands/routes.rb +69 -0
  29. data/lib/funicular/compiler.rb +135 -0
  30. data/lib/funicular/configuration.rb +76 -0
  31. data/lib/funicular/helpers/picoruby_helper.rb +50 -0
  32. data/lib/funicular/middleware.rb +98 -0
  33. data/lib/funicular/railtie.rb +26 -0
  34. data/lib/funicular/route_parser.rb +137 -0
  35. data/lib/funicular/vendor/picorbc/VERSION +1 -0
  36. data/lib/funicular/vendor/picorbc/picorbc.js +5283 -0
  37. data/lib/funicular/vendor/picorbc/picorbc.wasm +0 -0
  38. data/lib/funicular/vendor/picoruby/VERSION +1 -0
  39. data/lib/funicular/vendor/picoruby/debug/init.iife.js +130 -0
  40. data/lib/funicular/vendor/picoruby/debug/picoruby.js +6404 -0
  41. data/lib/funicular/vendor/picoruby/debug/picoruby.wasm +0 -0
  42. data/lib/funicular/vendor/picoruby/dist/init.iife.js +130 -0
  43. data/lib/funicular/vendor/picoruby/dist/picoruby.js +2 -0
  44. data/lib/funicular/vendor/picoruby/dist/picoruby.wasm +0 -0
  45. data/lib/funicular/version.rb +1 -1
  46. data/lib/funicular.rb +29 -1
  47. data/lib/tasks/funicular.rake +135 -0
  48. data/minitest/funicular_test.rb +13 -0
  49. data/minitest/test_helper.rb +7 -0
  50. data/mrbgem.rake +15 -0
  51. data/mrblib/cable.rb +417 -0
  52. data/mrblib/component.rb +911 -0
  53. data/mrblib/debug.rb +205 -0
  54. data/mrblib/differ.rb +244 -0
  55. data/mrblib/environment_inquirer.rb +34 -0
  56. data/mrblib/error_boundary.rb +125 -0
  57. data/mrblib/file_upload.rb +184 -0
  58. data/mrblib/form_builder.rb +284 -0
  59. data/mrblib/funicular.rb +156 -0
  60. data/mrblib/http.rb +89 -0
  61. data/mrblib/model.rb +146 -0
  62. data/mrblib/patcher.rb +203 -0
  63. data/mrblib/router.rb +229 -0
  64. data/mrblib/styles.rb +83 -0
  65. data/mrblib/vdom.rb +273 -0
  66. data/sig/cable.rbs +65 -0
  67. data/sig/component.rbs +141 -0
  68. data/sig/debug.rbs +28 -0
  69. data/sig/differ.rbs +18 -0
  70. data/sig/environment_iquirer.rbs +10 -0
  71. data/sig/error_boundary.rbs +14 -0
  72. data/sig/file_upload.rbs +18 -0
  73. data/sig/form_builder.rbs +29 -0
  74. data/sig/funicular.rbs +11 -1
  75. data/sig/http.rbs +22 -0
  76. data/sig/model.rbs +23 -0
  77. data/sig/patcher.rbs +15 -0
  78. data/sig/router.rbs +43 -0
  79. data/sig/styles.rbs +25 -0
  80. data/sig/vdom.rbs +59 -0
  81. metadata +119 -8
@@ -0,0 +1,179 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Funicular::Cable Test</title>
6
+ <link rel="stylesheet" href="../test.css">
7
+ </head>
8
+ <body>
9
+ <div><a href="../index.html">Back</a></div>
10
+ <h1>Funicular::Cable Test</h1>
11
+ <p>Make sure the test server is running: <code>cd mrbgems/picoruby-funicular/demo/cable_server && ruby server.rb</code></p>
12
+
13
+ <div class="test-section">
14
+ <h3>Connection Status</h3>
15
+ <div id="status" class="status disconnected">Disconnected</div>
16
+ </div>
17
+
18
+ <div class="test-section">
19
+ <h3>Test 1: Basic Connection</h3>
20
+ <button id="btn-test1">Test Connection</button>
21
+ <div id="test1-result"></div>
22
+ </div>
23
+
24
+ <div class="test-section">
25
+ <h3>Test 2: Subscribe to Channel</h3>
26
+ <button id="btn-test2">Subscribe to ChatChannel</button>
27
+ <div id="test2-result"></div>
28
+ </div>
29
+
30
+ <div class="test-section">
31
+ <h3>Test 3: Send Message</h3>
32
+ <input type="text" id="messageInput" placeholder="Enter a message" value="Hello from Funicular!">
33
+ <button id="btn-test3">Send Message</button>
34
+ <div id="test3-result"></div>
35
+ </div>
36
+
37
+ <div class="test-section">
38
+ <h3>Test 4: Receive Messages</h3>
39
+ <div id="messages"></div>
40
+ </div>
41
+
42
+ <div class="test-section">
43
+ <h3>Test 5: Unsubscribe</h3>
44
+ <button id="btn-test5">Unsubscribe</button>
45
+ <div id="test5-result"></div>
46
+ </div>
47
+
48
+ <div class="test-section">
49
+ <h3>Test 6: Reconnection (Exponential Backoff)</h3>
50
+ <p>Stop the server, then restart it to test automatic reconnection with exponential backoff.</p>
51
+ <div id="test6-result">Waiting for reconnection events...</div>
52
+ </div>
53
+
54
+ <script src="../init.iife.js"></script>
55
+ <script type="text/ruby">
56
+ # Global variables for testing
57
+ $consumer = nil
58
+ $subscription = nil
59
+ $message_count = 0
60
+
61
+ def log_message(msg)
62
+ messages_div = JS.document.getElementById('messages')
63
+ message_div = JS.document.createElement('div')
64
+ message_div.className = 'message'
65
+ now = Time.now
66
+ message_div.textContent = "[#{now} (#{now.to_i})] #{msg}"
67
+ messages_div.appendChild(message_div)
68
+ messages_div.scrollTop = messages_div.scrollHeight
69
+ end
70
+
71
+ def update_status(connected)
72
+ status_div = JS.document.getElementById('status')
73
+ if connected
74
+ status_div.textContent = 'Connected'
75
+ status_div.className = 'status connected'
76
+ else
77
+ status_div.textContent = 'Disconnected'
78
+ status_div.className = 'status disconnected'
79
+ end
80
+ end
81
+
82
+ # Test 1: Basic Connection
83
+ btn1 = JS.document.getElementById('btn-test1')
84
+ btn1.addEventListener('click') do |event|
85
+ result_div = JS.document.getElementById('test1-result')
86
+
87
+ begin
88
+ $consumer = Funicular::Cable.create_consumer('ws://localhost:9292/cable')
89
+
90
+ # Wait a moment for connection
91
+ sleep 1
92
+ if $consumer
93
+ result_div.innerHTML = '<span class="pass">PASS: Consumer created and connected</span>'
94
+ update_status(true)
95
+ else
96
+ result_div.innerHTML = '<span class="fail">FAIL: Consumer not created</span>'
97
+ end
98
+ rescue => e
99
+ result_div.innerHTML = "<span class=\"fail\">FAIL: #{e.message}</span>"
100
+ end
101
+ end
102
+
103
+ # Test 2: Subscribe to Channel
104
+ btn2 = JS.document.getElementById('btn-test2')
105
+ btn2.addEventListener('click') do |event|
106
+ result_div = JS.document.getElementById('test2-result')
107
+
108
+ unless $consumer
109
+ result_div.innerHTML = '<span class="fail">FAIL: Run Test 1 first</span>'
110
+ next
111
+ end
112
+
113
+ begin
114
+ $subscription = $consumer.subscriptions.create(channel: 'ChatChannel', room: 'lobby') do |message|
115
+ log_message("Received: #{message.inspect}")
116
+ $message_count += 1
117
+ end
118
+
119
+ $subscription.on_connected do
120
+ result_div.innerHTML = '<span class="pass">PASS: Subscribed to ChatChannel</span>'
121
+ log_message('Subscription confirmed!')
122
+ end
123
+
124
+ $subscription.on_rejected do
125
+ result_div.innerHTML = '<span class="fail">FAIL: Subscription rejected</span>'
126
+ end
127
+ rescue => e
128
+ result_div.innerHTML = "<span class=\"fail\">FAIL: #{e.message}</span>"
129
+ end
130
+ end
131
+
132
+ # Test 3: Send Message
133
+ btn3 = JS.document.getElementById('btn-test3')
134
+ btn3.addEventListener('click') do |event|
135
+ result_div = JS.document.getElementById('test3-result')
136
+
137
+ unless $subscription
138
+ result_div.innerHTML = '<span class="fail">FAIL: Run Test 2 first</span>'
139
+ next
140
+ end
141
+
142
+ begin
143
+ input = JS.document.getElementById('messageInput')
144
+ message_text = input[:value].to_s
145
+
146
+ $subscription.perform('speak', message: message_text)
147
+
148
+ result_div.innerHTML = '<span class="pass">PASS: Message sent</span>'
149
+ log_message("Sent: #{message_text}")
150
+ rescue => e
151
+ result_div.innerHTML = "<span class=\"fail\">FAIL: #{e.message}</span>"
152
+ end
153
+ end
154
+
155
+ # Test 5: Unsubscribe
156
+ btn5 = JS.document.getElementById('btn-test5')
157
+ btn5.addEventListener('click') do |event|
158
+ result_div = JS.document.getElementById('test5-result')
159
+
160
+ unless $subscription
161
+ result_div.innerHTML = '<span class="fail">FAIL: No active subscription</span>'
162
+ next
163
+ end
164
+
165
+ begin
166
+ $subscription.unsubscribe
167
+ $subscription = nil
168
+ result_div.innerHTML = '<span class="pass">PASS: Unsubscribed</span>'
169
+ log_message('Unsubscribed from ChatChannel')
170
+ rescue => e
171
+ result_div.innerHTML = "<span class=\"fail\">FAIL: #{e.message}</span>"
172
+ end
173
+ end
174
+
175
+ puts "Funicular::Cable test page loaded"
176
+ puts "Run tests in order: Connection -> Subscribe -> Send Message -> Unsubscribe"
177
+ </script>
178
+ </body>
179
+ </html>
@@ -0,0 +1,235 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Funicular Chart.js Integration Test</title>
6
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
7
+ <style>
8
+ body { font-family: Arial, sans-serif; padding: 20px; }
9
+ .test-section { margin: 20px 0; padding: 15px; border: 1px solid #ccc; border-radius: 5px; }
10
+ .test-result { margin: 10px 0; padding: 10px; }
11
+ .test-result.pass { background-color: #d4edda; color: #155724; }
12
+ .test-result.fail { background-color: #f8d7da; color: #721c24; }
13
+ #app { margin-top: 20px; padding: 20px; border: 2px solid #007bff; border-radius: 5px; }
14
+ #chart-container { width: 600px; height: 400px; margin: 20px auto; }
15
+ button { margin: 5px; padding: 8px 16px; cursor: pointer; }
16
+ </style>
17
+ </head>
18
+ <body>
19
+ <div><a href="../index.html">Back</a></div>
20
+ <h1>Funicular Chart.js Integration Tests</h1>
21
+ <div id="test-results"></div>
22
+ <div id="app"></div>
23
+
24
+ <script src="../init.iife.js"></script>
25
+ <script type="text/ruby">
26
+ class DashboardComponent < Funicular::Component
27
+ def initialize_state
28
+ {
29
+ month_index: 0,
30
+ data_sets: [
31
+ { labels: ['Jan', 'Feb', 'Mar'], values: [10, 20, 15] },
32
+ { labels: ['Apr', 'May', 'Jun'], values: [25, 30, 28] },
33
+ { labels: ['Jul', 'Aug', 'Sep'], values: [35, 40, 38] }
34
+ ]
35
+ }
36
+ end
37
+
38
+ def component_mounted
39
+ puts "component_mounted: Initializing Chart.js"
40
+ canvas = @refs[:chart_canvas]
41
+
42
+ unless canvas
43
+ puts "ERROR: canvas ref not found!"
44
+ return
45
+ end
46
+
47
+ current_data = state.data_sets[state.month_index]
48
+
49
+ config = {
50
+ type: 'line',
51
+ data: {
52
+ labels: current_data[:labels],
53
+ datasets: [{
54
+ label: 'Sales',
55
+ data: current_data[:values],
56
+ borderColor: 'rgb(75, 192, 192)',
57
+ tension: 0.1
58
+ }]
59
+ },
60
+ options: {
61
+ responsive: true,
62
+ maintainAspectRatio: false
63
+ }
64
+ }
65
+
66
+ js_config = JS::Bridge.to_js(config)
67
+ @chart = JS.global[:Chart].new(canvas, js_config)
68
+ puts "Chart.js initialized successfully"
69
+ end
70
+
71
+ def component_updated
72
+ puts "component_updated: Updating Chart.js with data index #{state.month_index}"
73
+ update_chart
74
+ end
75
+
76
+ def component_will_unmount
77
+ puts "component_will_unmount: Destroying Chart.js"
78
+ @chart.destroy if @chart
79
+ end
80
+
81
+ def next_data_set(event)
82
+ event.preventDefault if event
83
+ new_index = (state.month_index + 1) % state.data_sets.size
84
+ puts "next_data_set: Changing index from #{state.month_index} to #{new_index}"
85
+ patch(month_index: new_index)
86
+ end
87
+
88
+ def prev_data_set(event)
89
+ event.preventDefault if event
90
+ new_index = (state.month_index - 1) % state.data_sets.size
91
+ puts "prev_data_set: Changing index from #{state.month_index} to #{new_index}"
92
+ patch(month_index: new_index)
93
+ end
94
+
95
+ def update_chart
96
+ return unless @chart
97
+
98
+ current_data = state.data_sets[state.month_index]
99
+ puts "update_chart: Setting data to #{current_data[:labels].join(', ')}"
100
+
101
+ # Convert to JS objects
102
+ new_labels = JS::Bridge.to_js(current_data[:labels])
103
+ new_data = JS::Bridge.to_js(current_data[:values])
104
+
105
+ puts "update_chart: new_labels type: #{new_labels.type}"
106
+ puts "update_chart: new_data type: #{new_data.type}"
107
+
108
+ # Update chart data
109
+ @chart[:data][:labels] = new_labels
110
+ @chart[:data][:datasets][0][:data] = new_data
111
+
112
+ # Verify the data was set
113
+ puts "update_chart: Chart labels after setting: #{@chart[:data][:labels][0]}"
114
+ puts "update_chart: Chart data after setting: #{@chart[:data][:datasets][0][:data][0]}"
115
+
116
+ # Call update() method with explicit argument to ensure method call
117
+ @chart.update("active")
118
+ puts "update_chart: Chart.js update('active') called"
119
+ end
120
+
121
+ def render
122
+ div(id: "dashboard") do
123
+ h2 { "Sales Dashboard" }
124
+ div(id: "chart-container") do
125
+ canvas(ref: :chart_canvas, id: "chart")
126
+ end
127
+ div do
128
+ button(id: "prev-btn", onclick: :prev_data_set) { "Previous Quarter" }
129
+ button(id: "next-btn", onclick: :next_data_set) { "Next Quarter" }
130
+ end
131
+ div(id: "info") do
132
+ current = state.data_sets[state.month_index]
133
+ "Current data: #{current[:labels].join(', ')}"
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ def log_test(name, passed)
140
+ results = JS.document.getElementById('test-results')
141
+ div = JS.document.createElement('div')
142
+ div.className = passed ? 'test-result pass' : 'test-result fail'
143
+ div.textContent = "#{passed ? 'PASS' : 'FAIL'}: #{name}"
144
+ results.appendChild(div)
145
+ puts "#{passed ? 'PASS' : 'FAIL'}: #{name}"
146
+ end
147
+
148
+ def run_tests
149
+ puts "Starting Chart.js integration tests..."
150
+
151
+ # Test 1: JS::Bridge.to_js with Hash
152
+ begin
153
+ hash = { a: 1, b: "test", c: true }
154
+ js_obj = JS::Bridge.to_js(hash)
155
+ test1 = js_obj[:a].to_i == 1 && js_obj[:b].to_s == "test"
156
+ log_test("JS::Bridge.to_js(Hash)", test1)
157
+ rescue => e
158
+ log_test("JS::Bridge.to_js(Hash)", false)
159
+ puts "Error: #{e.message}"
160
+ end
161
+
162
+ # Test 2: JS::Bridge.to_js with nested Hash
163
+ begin
164
+ nested = { outer: { inner: 42 } }
165
+ js_nested = JS::Bridge.to_js(nested)
166
+ test2 = js_nested[:outer][:inner].to_i == 42
167
+ log_test("JS::Bridge.to_js(nested Hash)", test2)
168
+ rescue => e
169
+ log_test("JS::Bridge.to_js(nested Hash)", false)
170
+ puts "Error: #{e.message}"
171
+ end
172
+
173
+ # Test 3: JS::Bridge.to_js with Array
174
+ begin
175
+ array = [1, 2, 3]
176
+ js_array = JS::Bridge.to_js(array)
177
+ test3 = js_array[0].to_i == 1 && js_array[2].to_i == 3
178
+ log_test("JS::Bridge.to_js(Array)", test3)
179
+ rescue => e
180
+ log_test("JS::Bridge.to_js(Array)", false)
181
+ puts "Error: #{e.message}"
182
+ end
183
+
184
+ # Test 4: JS::Bridge.to_js with Array of Hashes
185
+ begin
186
+ complex = [{ x: 10 }, { x: 20 }]
187
+ js_complex = JS::Bridge.to_js(complex)
188
+ test4 = js_complex[0][:x].to_i == 10 && js_complex[1][:x].to_i == 20
189
+ log_test("JS::Bridge.to_js(Array of Hashes)", test4)
190
+ rescue => e
191
+ log_test("JS::Bridge.to_js(Array of Hashes)", false)
192
+ puts "Error: #{e.message}"
193
+ end
194
+
195
+ # Test 5: Chart.js availability
196
+ begin
197
+ test5 = !JS.global[:Chart].nil?
198
+ log_test("Chart.js loaded", test5)
199
+ rescue => e
200
+ log_test("Chart.js loaded", false)
201
+ puts "Error: #{e.message}"
202
+ end
203
+
204
+ # Test 6: Component with Chart.js
205
+ begin
206
+ container = JS.document.getElementById('app')
207
+ dashboard = DashboardComponent.new
208
+ dashboard.mount(container)
209
+ sleep 0.1
210
+
211
+ test6 = !dashboard.instance_variable_get(:@chart).nil?
212
+ log_test("Chart.js component integration", test6)
213
+ rescue => e
214
+ log_test("Chart.js component integration", false)
215
+ puts "Error: #{e.message}"
216
+ end
217
+
218
+ # Test 7: Ref attribute
219
+ begin
220
+ test7 = !dashboard.refs[:chart_canvas].nil?
221
+ log_test("Ref attribute for canvas", test7)
222
+ rescue => e
223
+ log_test("Ref attribute for canvas", false)
224
+ puts "Error: #{e.message}"
225
+ end
226
+
227
+ puts "All tests completed!"
228
+ puts ""
229
+ puts "Manual test: Click 'Next Quarter' and 'Previous Quarter' buttons to verify chart updates"
230
+ end
231
+
232
+ run_tests
233
+ </script>
234
+ </body>
235
+ </html>
@@ -0,0 +1,201 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Funicular Component Test</title>
6
+ <style>
7
+ body { font-family: Arial, sans-serif; padding: 20px; }
8
+ .test-section { margin: 20px 0; padding: 15px; border: 1px solid #ccc; border-radius: 5px; }
9
+ .test-result { margin: 10px 0; padding: 10px; }
10
+ .test-result.pass { background-color: #d4edda; color: #155724; }
11
+ .test-result.fail { background-color: #f8d7da; color: #721c24; }
12
+ #app { margin-top: 20px; padding: 20px; border: 2px solid #007bff; border-radius: 5px; }
13
+ button { margin: 5px; padding: 8px 16px; cursor: pointer; }
14
+ </style>
15
+ </head>
16
+ <body>
17
+ <div><a href="../index.html">Back</a></div>
18
+ <h1>Funicular Component Tests</h1>
19
+ <div id="test-results"></div>
20
+ <div id="app"></div>
21
+
22
+ <script type="text/ruby">
23
+ class TestCounter < Funicular::Component
24
+ def initialize_state
25
+ { count: 0, mounted: false }
26
+ end
27
+
28
+ def component_will_mount
29
+ end
30
+
31
+ def component_mounted
32
+ patch(mounted: true)
33
+ end
34
+
35
+ def component_will_update
36
+ end
37
+
38
+ def component_updated
39
+ end
40
+
41
+ def increment(event)
42
+ event.preventDefault if event
43
+ patch(count: state.count + 1)
44
+ end
45
+
46
+ def decrement(event)
47
+ event.preventDefault if event
48
+ patch(count: state.count - 1)
49
+ end
50
+
51
+ def render
52
+ div(id: "counter-app") do
53
+ h2 { "Counter: #{state.count}" }
54
+ div(id: "status") do
55
+ "Mounted: #{state.mounted}"
56
+ end
57
+ div do
58
+ button(id: "increment-btn", onclick: :increment) { "+1" }
59
+ button(id: "decrement-btn", onclick: :decrement) { "-1" }
60
+ end
61
+ div(ref: :display, id: "display") do
62
+ "Current value: #{state.count}"
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ def log_test(name, passed)
69
+ results = JS.document.getElementById('test-results')
70
+ div = JS.document.createElement('div')
71
+ div.className = passed ? 'test-result pass' : 'test-result fail'
72
+ div.textContent = "#{passed ? 'PASS' : 'FAIL'}: #{name}"
73
+ results.appendChild(div)
74
+ puts "#{passed ? 'PASS' : 'FAIL'}: #{name}"
75
+ end
76
+
77
+ def run_tests
78
+ puts "Starting Funicular Component tests..."
79
+
80
+ # Test 1: Component creation
81
+ begin
82
+ counter = TestCounter.new
83
+ log_test("Component creation", counter.is_a?(Funicular::Component))
84
+ rescue => e
85
+ log_test("Component creation", false)
86
+ puts "Error: #{e.message}"
87
+ end
88
+
89
+ # Test 2: Initial state
90
+ begin
91
+ counter = TestCounter.new
92
+ initial_state = counter.state.count == 0
93
+ log_test("Initial state", initial_state)
94
+ rescue => e
95
+ log_test("Initial state", false)
96
+ puts "Error: #{e.message}"
97
+ end
98
+
99
+ # Test 3: Mount component
100
+ begin
101
+ container = JS.document.getElementById('app')
102
+ counter = Funicular.start(TestCounter, container: container)
103
+ sleep 0.1
104
+ mounted = JS.document.getElementById('counter-app') != nil
105
+ log_test("Component mount", mounted)
106
+ rescue => e
107
+ log_test("Component mount", false)
108
+ puts "Error: #{e.message}"
109
+ end
110
+
111
+ # Test 4: State update
112
+ begin
113
+ container = JS.document.getElementById('app')
114
+ container.innerHTML = ''
115
+ counter = Funicular.start(TestCounter, container: container)
116
+ sleep 0.1
117
+ counter.patch(count: 5)
118
+ sleep 0.1
119
+ h2_element = JS.document.querySelector('#counter-app h2')
120
+ text = h2_element.textContent.to_s
121
+ state_updated = text.include?("5")
122
+ log_test("State update", state_updated)
123
+ rescue => e
124
+ log_test("State update", false)
125
+ puts "Error: #{e.message}"
126
+ end
127
+
128
+ # Test 5: Event handler binding and execution
129
+ # Note: Manual browser clicks work correctly (verified in console logs)
130
+ # For automated testing, we test the handler logic directly
131
+ begin
132
+ container = JS.document.getElementById('app')
133
+ container.innerHTML = ''
134
+ counter = Funicular.start(TestCounter, container: container)
135
+ sleep 0.1
136
+
137
+ # Verify event handler is bound (button exists and has onclick attribute)
138
+ btn = JS.document.getElementById('increment-btn')
139
+ has_button = btn != nil
140
+
141
+ # Test handler logic by calling it directly (standard testing approach)
142
+ initial_count = counter.state.count
143
+ counter.increment(nil)
144
+ sleep 0.05
145
+
146
+ h2_element = JS.document.querySelector('#counter-app h2')
147
+ text = h2_element.textContent.to_s
148
+ handler_works = counter.state.count == initial_count + 1 && text.include?("#{initial_count + 1}")
149
+
150
+ log_test("Event handler (click)", has_button && handler_works)
151
+ rescue => e
152
+ log_test("Event handler (click)", false)
153
+ puts "Error: #{e.message}"
154
+ end
155
+
156
+ # Test 6: Ref attribute
157
+ begin
158
+ container = JS.document.getElementById('app')
159
+ container.innerHTML = ''
160
+ counter = Funicular.start(TestCounter, container: container)
161
+ sleep 0.1
162
+
163
+ ref_element = counter.refs[:display]
164
+ has_ref = ref_element != nil && ref_element.id.to_s == "display"
165
+ log_test("Ref attribute", has_ref)
166
+ rescue => e
167
+ log_test("Ref attribute", false)
168
+ puts "Error: #{e.message}"
169
+ end
170
+
171
+ # Test 7: Multiple updates
172
+ begin
173
+ container = JS.document.getElementById('app')
174
+ container.innerHTML = ''
175
+ counter = Funicular.start(TestCounter, container: container)
176
+ sleep 0.1
177
+
178
+ counter.patch(count: 10)
179
+ sleep 0.05
180
+ counter.patch(count: 20)
181
+ sleep 0.05
182
+ counter.patch(count: 30)
183
+ sleep 0.1
184
+
185
+ h2_element = JS.document.querySelector('#counter-app h2')
186
+ text = h2_element.textContent.to_s
187
+ multiple_updates = text.include?("30")
188
+ log_test("Multiple updates", multiple_updates)
189
+ rescue => e
190
+ log_test("Multiple updates", false)
191
+ puts "Error: #{e.message}"
192
+ end
193
+
194
+ puts "All tests completed!"
195
+ end
196
+
197
+ run_tests
198
+ </script>
199
+ <script src="../init.iife.js"></script>
200
+ </body>
201
+ </html>