this repo has no description
at main 229 lines 7.9 kB view raw
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>