this repo has no description
1<!DOCTYPE html>
2<html>
3<head>
4 <meta charset="utf-8">
5 <title>Widget Protocol Test</title>
6 <style>
7 body { font-family: monospace; max-width: 800px; margin: 2em auto; }
8 #log { background: #f5f5f5; padding: 1em; white-space: pre-wrap; max-height: 400px; overflow-y: auto; }
9 .widget-container { border: 1px solid #ccc; padding: 1em; margin: 1em 0; }
10 .widget-container h3 { margin-top: 0; color: #666; }
11 </style>
12</head>
13<body>
14 <h1>Widget Protocol Test</h1>
15 <div id="widgets"></div>
16 <h2>Log</h2>
17 <div id="log"></div>
18
19 <script type="module">
20 import { OcamlWorker } from './ocaml-worker.js';
21
22 const logEl = document.getElementById('log');
23 const widgetsEl = document.getElementById('widgets');
24 let worker;
25
26 function log(msg) {
27 logEl.textContent += msg + '\n';
28 logEl.scrollTop = logEl.scrollHeight;
29 }
30
31 // --- View.node JSON renderer ---
32 function renderNode(node) {
33 if (node.t === 'txt') {
34 return document.createTextNode(node.v);
35 }
36 if (node.t === 'el') {
37 const el = document.createElement(node.tag);
38 for (const attr of (node.a || [])) {
39 switch (attr.t) {
40 case 'prop':
41 el.setAttribute(attr.k, attr.v);
42 break;
43 case 'style':
44 el.style[attr.k] = attr.v;
45 break;
46 case 'cls':
47 el.classList.add(attr.v);
48 break;
49 case 'handler':
50 el.addEventListener(attr.ev, () => {
51 const isInput = ['INPUT', 'SELECT', 'TEXTAREA'].includes(el.tagName);
52 const value = isInput ? el.value : null;
53 const widgetId = el.closest('[data-widget-id]')?.dataset.widgetId;
54 if (widgetId) {
55 log('Event: widget=' + widgetId + ' handler=' + attr.id + ' value=' + value);
56 worker.sendWidgetEvent(widgetId, attr.id, attr.ev, value);
57 }
58 });
59 break;
60 }
61 }
62 for (const child of (node.c || [])) {
63 el.appendChild(renderNode(child));
64 }
65 return el;
66 }
67 return document.createTextNode('');
68 }
69
70 function renderWidget(widgetId, viewJson) {
71 let container = document.getElementById('widget-' + widgetId);
72 if (!container) {
73 container = document.createElement('div');
74 container.id = 'widget-' + widgetId;
75 container.className = 'widget-container';
76 container.dataset.widgetId = widgetId;
77 container.innerHTML = '<h3>' + widgetId + '</h3>';
78 widgetsEl.appendChild(container);
79 }
80 const heading = container.querySelector('h3');
81 container.innerHTML = '';
82 container.appendChild(heading);
83 container.dataset.widgetId = widgetId;
84 container.appendChild(renderNode(viewJson));
85 }
86
87 async function run() {
88 log('Creating worker...');
89 worker = new OcamlWorker('worker.bc.js', {
90 onWidgetUpdate: (msg) => {
91 log('WidgetUpdate: id=' + msg.widget_id);
92 renderWidget(msg.widget_id, msg.view);
93 },
94 onWidgetClear: (msg) => {
95 log('WidgetClear: id=' + msg.widget_id);
96 const container = document.getElementById('widget-' + msg.widget_id);
97 if (container) container.remove();
98 },
99 onOutputAt: (msg) => {
100 if (msg.caml_ppf) log('OutputAt: ' + msg.caml_ppf);
101 },
102 });
103
104 log('Initializing...');
105 await worker.init({
106 findlib_requires: [],
107 findlib_index: null,
108 });
109 log('Worker ready.');
110
111 // Helper to log eval results with errors
112 function logResult(label, r) {
113 if (r.caml_ppf) log(label + ': ' + r.caml_ppf);
114 if (r.stderr) log('STDERR: ' + r.stderr);
115 }
116
117 // Diagnostic: check module availability
118 log('\n--- Diagnostic: module availability ---');
119 const d1 = await worker.eval('module W = Widget;;');
120 logResult('Widget', d1);
121 const d2 = await worker.eval('let _ = Widget.View.Text "test";;');
122 logResult('Widget.View', d2);
123
124 // Test 1: Static widget
125 log('\n--- Test 1: Static widget ---');
126 const r1 = await worker.eval(
127 'Widget.display ~id:"hello" ~handlers:[] ' +
128 '(Widget.View.Element { tag = "div"; attrs = []; ' +
129 'children = [Widget.View.Text "Hello from OCaml!"] });;'
130 );
131 logResult('Eval', r1);
132
133 // Test 2: Interactive counter with Note
134 log('\n--- Test 2: Interactive counter ---');
135 const r2 = await worker.eval([
136 'let inc_e, send_inc = Note.E.create ();;',
137 'let dec_e, send_dec = Note.E.create ();;',
138 'let count =',
139 ' let delta = Note.E.select [',
140 ' Note.E.map (fun () n -> n + 1) inc_e;',
141 ' Note.E.map (fun () n -> n - 1) dec_e;',
142 ' ] in',
143 ' Note.S.accum 0 delta;;',
144 '',
145 'let counter_view n =',
146 ' let open Widget.View in',
147 ' Element { tag = "div"; attrs = [Class "counter"]; children = [',
148 ' Element { tag = "button";',
149 ' attrs = [Handler ("click", "dec")];',
150 ' children = [Text "-"] };',
151 ' Element { tag = "span";',
152 ' attrs = [Style ("margin", "0 1em")];',
153 ' children = [Text (string_of_int n)] };',
154 ' Element { tag = "button";',
155 ' attrs = [Handler ("click", "inc")];',
156 ' children = [Text "+"] };',
157 ' ] };;',
158 '',
159 'Widget.display ~id:"counter"',
160 ' ~handlers:[',
161 ' "inc", (fun _ -> send_inc ());',
162 ' "dec", (fun _ -> send_dec ());',
163 ' ]',
164 ' (counter_view 0);;',
165 '',
166 'let _logr = Note.S.log',
167 ' (Note.S.map counter_view count)',
168 ' (Widget.update ~id:"counter");;',
169 'Note.Logr.hold _logr;;',
170 ].join('\n'));
171 logResult('Eval', r2);
172
173 // Test 3: Slider with cross-cell signal
174 log('\n--- Test 3: Slider ---');
175 await worker.eval([
176 'let x_e, send_x = Note.E.create ();;',
177 'let x = Note.S.hold 50 x_e;;',
178 '',
179 'let slider_view v =',
180 ' let open Widget.View in',
181 ' Element { tag = "div"; attrs = []; children = [',
182 ' Element { tag = "label"; attrs = [];',
183 ' children = [Text (Printf.sprintf "X: %d" v)] };',
184 ' Element { tag = "input"; attrs = [',
185 ' Property ("type", "range");',
186 ' Property ("min", "0");',
187 ' Property ("max", "100");',
188 ' Property ("value", string_of_int v);',
189 ' Handler ("input", "x");',
190 ' ]; children = [] };',
191 ' ] };;',
192 '',
193 'Widget.display ~id:"slider"',
194 ' ~handlers:["x", (fun v ->',
195 ' send_x (int_of_string (Option.get v)))]',
196 ' (slider_view 50);;',
197 '',
198 'let _logr2 = Note.S.log',
199 ' (Note.S.map slider_view x)',
200 ' (Widget.update ~id:"slider");;',
201 'Note.Logr.hold _logr2;;',
202 ].join('\n'));
203
204 // Test 3b: Cross-cell derived widget
205 log('\n--- Test 3b: Derived widget (uses x from cell above) ---');
206 await worker.eval([
207 'let doubled_view v =',
208 ' let open Widget.View in',
209 ' Element { tag = "div"; attrs = []; children = [',
210 ' Text (Printf.sprintf "2x = %d" (v * 2))',
211 ' ] };;',
212 '',
213 'Widget.display ~id:"doubled"',
214 ' ~handlers:[]',
215 ' (doubled_view (Note.S.value x));;',
216 '',
217 'let _logr3 = Note.S.log',
218 ' (Note.S.map doubled_view x)',
219 ' (Widget.update ~id:"doubled");;',
220 'Note.Logr.hold _logr3;;',
221 ].join('\n'));
222
223 log('\nAll tests dispatched. Interact with widgets above.');
224 }
225
226 run().catch(e => log('Error: ' + e.message));
227 </script>
228</body>
229</html>