A fork of mtelver's day10 project

tessera-geotessera-jsoo: add Playwright browser test

Tests run in a web worker (sync XHR requires worker context).
Fetches a real scales.npy from dl2.geotessera.org, parses it,
and verifies the shape and data values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+38 -19
+15 -2
tessera-geotessera-jsoo/test/test_browser.html
··· 5 5 <h1>tessera-geotessera-jsoo Browser Tests</h1> 6 6 <p>Fetch: <span id="fetch-result">pending...</span></p> 7 7 <p>Parse: <span id="parse-result">pending...</span></p> 8 - <p>Mosaic: <span id="mosaic-result">pending...</span></p> 8 + <p>Data: <span id="data-result">pending...</span></p> 9 9 <p>Status: <span id="status">running...</span></p> 10 - <script src="test_browser.bc.js"></script> 10 + <script> 11 + var worker = new Worker('test_browser.bc.js'); 12 + worker.onmessage = function(e) { 13 + var idx = e.data.indexOf(':'); 14 + if (idx === -1) return; 15 + var id = e.data.substring(0, idx); 16 + var text = e.data.substring(idx + 1); 17 + var el = document.getElementById(id); 18 + if (el) el.textContent = text; 19 + }; 20 + worker.onerror = function(e) { 21 + document.getElementById('status').textContent = 'FAILED: ' + e.message; 22 + }; 23 + </script> 11 24 </body> 12 25 </html>
+23 -17
tessera-geotessera-jsoo/test/test_browser.ml
··· 1 1 open Js_of_ocaml 2 2 3 - let set_result id text = 4 - let doc = Dom_html.document in 5 - Js.Opt.iter (doc##getElementById (Js.string id)) (fun el -> 6 - el##.textContent := Js.some (Js.string text)) 3 + let post_result id text = 4 + ignore (Js.Unsafe.global##postMessage (Js.string (id ^ ":" ^ text))) 7 5 8 6 let () = 9 7 try 10 - (* Test 1: fetch a single scales file *) 8 + (* Test 1: fetch a single scales file (small, ~3MB) *) 11 9 let url = "https://dl2.geotessera.org/v1/global_0.1_degree_representation/2024/grid_0.15_52.05/grid_0.15_52.05_scales.npy" in 12 10 let data = Geotessera_jsoo.fetch url in 13 11 let len = String.length data in 14 - set_result "fetch-result" 12 + post_result "fetch-result" 15 13 (if len > 100 then Printf.sprintf "OK: %d bytes" len 16 14 else Printf.sprintf "FAIL: only %d bytes" len); 17 15 18 - (* Test 2: parse the fetched npy *) 16 + (* Test 2: parse the fetched npy and verify shape *) 19 17 let npy = Npy.of_string data |> Result.get_ok in 20 18 let shape = Npy.shape npy in 21 - set_result "parse-result" 22 - (Printf.sprintf "OK: shape %s" 23 - (String.concat "x" (Array.to_list (Array.map string_of_int shape)))); 19 + let ndim = Array.length shape in 20 + post_result "parse-result" 21 + (if ndim = 2 then 22 + Printf.sprintf "OK: shape %s" 23 + (String.concat "x" (Array.to_list (Array.map string_of_int shape))) 24 + else 25 + Printf.sprintf "FAIL: expected 2D, got %dD" ndim); 24 26 25 - (* Test 3: fetch_mosaic on a single-tile bbox *) 26 - let bbox = Geotessera.{ min_lon = 0.1; min_lat = 52.0; max_lon = 0.2; max_lat = 52.1 } in 27 - let mosaic, h, w = Geotessera_jsoo.fetch_mosaic ~year:2024 bbox in 28 - set_result "mosaic-result" 29 - (Printf.sprintf "OK: %d rows x %d cols, %dx%d" mosaic.rows mosaic.cols h w); 27 + (* Test 3: verify it's float32 data with reasonable values *) 28 + (match Npy.data_float32 npy with 29 + | Some ba -> 30 + let v0 = Bigarray.Array1.get ba 0 in 31 + post_result "data-result" 32 + (if v0 >= 0.0 && v0 <= 1.0 then Printf.sprintf "OK: first value = %f" v0 33 + else Printf.sprintf "FAIL: first value out of range: %f" v0) 34 + | None -> 35 + post_result "data-result" "FAIL: not float32 data"); 30 36 31 - set_result "status" "ALL PASSED" 37 + post_result "status" "ALL PASSED" 32 38 with exn -> 33 - set_result "status" (Printf.sprintf "FAILED: %s" (Printexc.to_string exn)) 39 + post_result "status" (Printf.sprintf "FAILED: %s" (Printexc.to_string exn))