Fix island state loss on SX navigation + cache busting
Island markers rendered during SX navigation responses had no data-sx-state attribute, so hydration found empty kwargs and path was nil in the copyright display. Now adapter-dom.sx serializes keyword args into data-sx-state on island markers, matching what adapter-html.sx does for SSR. Also fix post-swap to use parent element for outerHTML swaps in SX responses (was using detached old target). Add SX source file hashes to wasm_hash for proper browser cache busting — changing any .sx file now busts the cache. Remove stale .sxbc bytecode cache files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -713,13 +713,13 @@ let register_jit_hook env =
|
||||
Sx_ref.jit_call_hook := Some (fun f args ->
|
||||
match f with
|
||||
| Lambda l ->
|
||||
let fn_name = match l.l_name with Some n -> n | None -> "?" in
|
||||
(match l.l_compiled with
|
||||
| Some cl when not (Sx_vm.is_jit_failed cl) ->
|
||||
(* Cached bytecode — run on VM, fall back to CEK on runtime error.
|
||||
Log once per function name, then stay quiet. Don't disable. *)
|
||||
(try Some (Sx_vm.call_closure cl args cl.vm_env_ref)
|
||||
with e ->
|
||||
let fn_name = match l.l_name with Some n -> n | None -> "?" in
|
||||
if not (Hashtbl.mem _jit_warned fn_name) then begin
|
||||
Hashtbl.replace _jit_warned fn_name true;
|
||||
Printf.eprintf "[jit] %s runtime fallback to CEK: %s\n%!" fn_name (Printexc.to_string e)
|
||||
@@ -727,7 +727,6 @@ let register_jit_hook env =
|
||||
None)
|
||||
| Some _ -> None (* compile failed or disabled — CEK handles *)
|
||||
| None ->
|
||||
let fn_name = match l.l_name with Some n -> n | None -> "?" in
|
||||
if !_jit_compiling then None
|
||||
else begin
|
||||
_jit_compiling := true;
|
||||
@@ -1578,9 +1577,16 @@ let http_inject_shell_statics env static_dir sx_sxc =
|
||||
let project_dir = try Sys.getenv "SX_PROJECT_DIR" with Not_found ->
|
||||
Filename.dirname (Filename.dirname static_dir) in
|
||||
let templates_dir = project_dir ^ "/shared/sx/templates" in
|
||||
let client_libs = [
|
||||
templates_dir ^ "/cssx.sx";
|
||||
] in
|
||||
(* Client libraries: all .sx files in templates/client-libs/ *)
|
||||
let client_libs_dir = templates_dir ^ "/client-libs" in
|
||||
let extra_libs =
|
||||
if Sys.file_exists client_libs_dir && Sys.is_directory client_libs_dir then
|
||||
Array.to_list (Sys.readdir client_libs_dir)
|
||||
|> List.filter (fun f -> Filename.check_suffix f ".sx")
|
||||
|> List.sort String.compare
|
||||
|> List.map (fun f -> client_libs_dir ^ "/" ^ f)
|
||||
else [] in
|
||||
let client_libs = (templates_dir ^ "/cssx.sx") :: extra_libs in
|
||||
List.iter (fun path ->
|
||||
if Sys.file_exists path then begin
|
||||
let src = In_channel.with_open_text path In_channel.input_all in
|
||||
@@ -1614,7 +1620,20 @@ let http_inject_shell_statics env static_dir sx_sxc =
|
||||
(* Compute file hashes for cache busting *)
|
||||
let sx_js_hash = file_hash (static_dir ^ "/scripts/sx-browser.js") in
|
||||
let body_js_hash = file_hash (static_dir ^ "/scripts/body.js") in
|
||||
let wasm_hash = file_hash (static_dir ^ "/wasm/sx_browser.bc.wasm.js") in
|
||||
(* Include SX source file hashes so browser cache busts when .sx files change *)
|
||||
let sx_dir = static_dir ^ "/wasm/sx" in
|
||||
let sx_files_hash =
|
||||
if Sys.file_exists sx_dir && Sys.is_directory sx_dir then
|
||||
let entries = Sys.readdir sx_dir in
|
||||
Array.sort String.compare entries;
|
||||
let combined = Array.fold_left (fun acc f ->
|
||||
if Filename.check_suffix f ".sx" then
|
||||
acc ^ file_hash (sx_dir ^ "/" ^ f)
|
||||
else acc
|
||||
) "" entries in
|
||||
String.sub (Digest.string combined |> Digest.to_hex) 0 12
|
||||
else "" in
|
||||
let wasm_hash = file_hash (static_dir ^ "/wasm/sx_browser.bc.wasm.js") ^ sx_files_hash in
|
||||
(* Read CSS for inline injection *)
|
||||
let tw_css = read_css_file (static_dir ^ "/styles/tw.css") in
|
||||
let basics_css = read_css_file (static_dir ^ "/styles/basics.css") in
|
||||
@@ -1674,16 +1693,7 @@ let http_inject_shell_statics env static_dir sx_sxc =
|
||||
ignore (env_bind env "__shell-body-scripts" Nil);
|
||||
ignore (env_bind env "__shell-inline-css" Nil);
|
||||
ignore (env_bind env "__shell-inline-head-js" Nil);
|
||||
(* init-sx: trigger client-side render when sx-root is empty (SSR failed).
|
||||
The boot code hydrates existing islands but doesn't do fresh render.
|
||||
This script forces a render from page-sx after boot completes. *)
|
||||
ignore (env_bind env "__shell-init-sx" (String
|
||||
"document.addEventListener('sx:boot-done', function() { \
|
||||
var root = document.getElementById('sx-root'); \
|
||||
if (root && !root.innerHTML.trim() && typeof SX !== 'undefined' && SX.renderPage) { \
|
||||
SX.renderPage(); \
|
||||
} \
|
||||
});"));
|
||||
ignore (env_bind env "__shell-init-sx" Nil);
|
||||
Printf.eprintf "[sx-http] Shell statics: defs=%d hash=%s css=%d js=%s wasm=%s\n%!"
|
||||
(String.length component_defs) component_hash (String.length sx_css) sx_js_hash wasm_hash
|
||||
|
||||
@@ -1789,7 +1799,9 @@ let http_setup_page_helpers env =
|
||||
SxExpr (Printf.sprintf "(pre :class \"text-sm overflow-x-auto\" (code \"%s\"))" escaped)
|
||||
| _ -> Nil);
|
||||
(* component-source — stub *)
|
||||
bind "component-source" (fun _args -> String "")
|
||||
bind "component-source" (fun _args -> String "");
|
||||
(* handler-source — stub (returns empty, used by example pages) *)
|
||||
bind "handler-source" (fun _args -> String "")
|
||||
|
||||
let http_mode port =
|
||||
let env = make_server_env () in
|
||||
@@ -2003,12 +2015,17 @@ let http_mode port =
|
||||
in
|
||||
match work with
|
||||
| Some (fd, path, headers) ->
|
||||
(* ~10M steps ≈ 3-5 seconds of CEK evaluation *)
|
||||
Atomic.set Sx_ref._step_count 0;
|
||||
Atomic.set Sx_ref._step_limit 10_000_000;
|
||||
let response =
|
||||
try
|
||||
let is_ajax = List.exists (fun (k, _) -> k = "sx-request" || k = "hx-request") headers in
|
||||
match http_render_page env path headers with
|
||||
| Some html ->
|
||||
let resp = http_response ~content_type:"text/html; charset=utf-8" html in
|
||||
let ct = if is_ajax then "text/sx; charset=utf-8"
|
||||
else "text/html; charset=utf-8" in
|
||||
let resp = http_response ~content_type:ct html in
|
||||
if not is_ajax then Hashtbl.replace response_cache path resp;
|
||||
resp
|
||||
| None -> http_response ~status:404 "<h1>Not Found</h1>"
|
||||
@@ -2016,6 +2033,7 @@ let http_mode port =
|
||||
Printf.eprintf "[render] Error for %s: %s\n%!" path (Printexc.to_string e);
|
||||
http_response ~status:500 "<h1>Internal Server Error</h1>"
|
||||
in
|
||||
Atomic.set Sx_ref._step_limit 0;
|
||||
write_response fd response
|
||||
| None -> ()
|
||||
done
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,464 +0,0 @@
|
||||
(sxbc 1 "8dd29bf7bc354b48"
|
||||
(code
|
||||
:bytecode (1 1 0 128 0 0 5 51 3 0 128 2 0 5 51 5 0 128 4 0 5 51 7 0 128 6 0 5 51 9 0 128 8 0 5 51 11 0 128 10 0 5 51 13 0 128 12 0 5 51 15 0 128 14 0 5 51 17 0 128 16 0 5 52 19 0 0 128 18 0 5 51 21 0 128 20 0 5 51 23 0 128 22 0 5 51 25 0 128 24 0 5 51 27 0 128 26 0 5 51 29 0 128 28 0 5 51 31 0 128 30 0 5 52 19 0 0 128 32 0 5 52 19 0 0 128 33 0 5 51 35 0 128 34 0 5 51 37 0 128 36 0 5 51 39 0 128 38 0 5 51 41 0 128 40 0 5 51 43 0 128 42 0 50)
|
||||
:constants (
|
||||
"HEAD_HOIST_SELECTOR"
|
||||
"meta, title, link[rel='canonical'], script[type='application/ld+json']"
|
||||
"hoist-head-elements-full"
|
||||
(code :arity 1
|
||||
:bytecode (20 0 0 16 0 20 1 0 48 2 17 1 51 3 0 16 1 52 2 0 2 50)
|
||||
:constants (
|
||||
"dom-query-all"
|
||||
"HEAD_HOIST_SELECTOR"
|
||||
"for-each"
|
||||
(code :arity 1
|
||||
:bytecode (20 1 0 16 0 48 1 52 0 0 1 17 1 16 1 1 3 0 52 2 0 2 33 30 0 20 4 0 20 5 0 16 0 48 1 48 1 5 20 6 0 20 7 0 16 0 48 1 16 0 49 2 32 205 0 16 1 1 8 0 52 2 0 2 33 103 0 20 9 0 16 0 1 10 0 48 2 17 2 20 9 0 16 0 1 11 0 48 2 17 3 16 2 33 20 0 20 12 0 1 14 0 16 2 1 15 0 52 13 0 3 48 1 32 1 0 2 5 16 3 33 20 0 20 12 0 1 16 0 16 3 1 15 0 52 13 0 3 48 1 32 1 0 2 5 20 6 0 20 7 0 16 0 48 1 16 0 48 2 5 20 17 0 16 0 49 1 32 90 0 16 1 1 18 0 52 2 0 2 6 33 18 0 5 20 9 0 16 0 1 19 0 48 2 1 20 0 52 2 0 2 33 34 0 20 12 0 1 21 0 48 1 5 20 6 0 20 7 0 16 0 48 1 16 0 48 2 5 20 17 0 16 0 49 1 32 22 0 20 6 0 20 7 0 16 0 48 1 16 0 48 2 5 20 17 0 16 0 49 1 50)
|
||||
:constants (
|
||||
"lower"
|
||||
"dom-tag-name"
|
||||
"="
|
||||
"title"
|
||||
"set-document-title"
|
||||
"dom-text-content"
|
||||
"dom-remove-child"
|
||||
"dom-parent"
|
||||
"meta"
|
||||
"dom-get-attr"
|
||||
"name"
|
||||
"property"
|
||||
"remove-head-element"
|
||||
"str"
|
||||
"meta[name=\""
|
||||
"\"]"
|
||||
"meta[property=\""
|
||||
"dom-append-to-head"
|
||||
"link"
|
||||
"rel"
|
||||
"canonical"
|
||||
"link[rel=\"canonical\"]"))))
|
||||
"sx-mount"
|
||||
(code :arity 3
|
||||
:bytecode (20 0 0 16 0 48 1 17 3 16 3 33 90 0 20 2 0 16 3 48 1 52 1 0 1 33 42 0 20 3 0 16 1 16 2 48 2 17 4 20 4 0 16 3 1 5 0 48 2 5 20 6 0 16 3 16 4 48 2 5 20 7 0 16 3 48 1 32 1 0 2 5 20 8 0 16 3 48 1 5 20 9 0 16 3 48 1 5 20 10 0 16 3 48 1 5 20 11 0 49 0 32 1 0 2 50)
|
||||
:constants (
|
||||
"resolve-mount-target"
|
||||
"empty?"
|
||||
"dom-child-list"
|
||||
"sx-render-with-env"
|
||||
"dom-set-text-content"
|
||||
""
|
||||
"dom-append"
|
||||
"hoist-head-elements-full"
|
||||
"process-elements"
|
||||
"sx-hydrate-elements"
|
||||
"sx-hydrate-islands"
|
||||
"run-post-render-hooks"))
|
||||
"resolve-suspense"
|
||||
(code :arity 2
|
||||
:bytecode (20 0 0 2 48 1 5 20 1 0 1 3 0 16 0 1 4 0 52 2 0 3 48 1 17 2 16 2 33 93 0 20 5 0 16 1 48 1 17 3 20 6 0 2 48 1 17 4 20 7 0 16 2 1 8 0 48 2 5 51 10 0 1 2 1 4 16 3 52 9 0 2 5 20 11 0 16 2 48 1 5 20 12 0 16 2 48 1 5 20 13 0 16 2 48 1 5 20 14 0 48 0 5 20 15 0 16 2 1 16 0 1 17 0 16 0 65 1 0 49 3 32 14 0 20 18 0 1 19 0 16 0 52 2 0 2 49 1 50)
|
||||
:constants (
|
||||
"process-sx-scripts"
|
||||
"dom-query"
|
||||
"str"
|
||||
"[data-suspense=\""
|
||||
"\"]"
|
||||
"parse"
|
||||
"get-render-env"
|
||||
"dom-set-text-content"
|
||||
""
|
||||
"for-each"
|
||||
(code :arity 1 :upvalue-count 2
|
||||
:bytecode (20 0 0 18 0 20 1 0 16 0 18 1 2 48 3 49 2 50)
|
||||
:constants (
|
||||
"dom-append"
|
||||
"render-to-dom"))
|
||||
"process-elements"
|
||||
"sx-hydrate-elements"
|
||||
"sx-hydrate-islands"
|
||||
"run-post-render-hooks"
|
||||
"dom-dispatch"
|
||||
"sx:resolved"
|
||||
"id"
|
||||
"log-warn"
|
||||
"resolveSuspense: no element for id="))
|
||||
"sx-hydrate-elements"
|
||||
(code :arity 1
|
||||
:bytecode (20 0 0 16 0 6 34 6 0 5 20 1 0 48 0 1 2 0 48 2 17 1 51 4 0 16 1 52 3 0 2 50)
|
||||
:constants (
|
||||
"dom-query-all"
|
||||
"dom-body"
|
||||
"[data-sx]"
|
||||
"for-each"
|
||||
(code :arity 1
|
||||
:bytecode (20 1 0 16 0 1 2 0 48 2 52 0 0 1 33 22 0 20 3 0 16 0 1 2 0 48 2 5 20 4 0 16 0 2 49 2 32 1 0 2 50)
|
||||
:constants (
|
||||
"not"
|
||||
"is-processed?"
|
||||
"hydrated"
|
||||
"mark-processed!"
|
||||
"sx-update-element"))))
|
||||
"sx-update-element"
|
||||
(code :arity 2
|
||||
:bytecode (20 0 0 16 0 48 1 17 2 16 2 33 96 0 20 1 0 16 2 1 2 0 48 2 17 3 16 3 33 75 0 20 3 0 16 2 48 1 17 4 20 4 0 16 4 16 1 48 2 17 5 20 5 0 16 3 16 5 48 2 17 6 20 6 0 16 2 1 7 0 48 2 5 20 8 0 16 2 16 6 48 2 5 16 1 33 14 0 20 9 0 16 2 16 4 16 1 49 3 32 1 0 2 32 1 0 2 32 1 0 2 50)
|
||||
:constants (
|
||||
"resolve-mount-target"
|
||||
"dom-get-attr"
|
||||
"data-sx"
|
||||
"parse-env-attr"
|
||||
"merge-envs"
|
||||
"sx-render-with-env"
|
||||
"dom-set-text-content"
|
||||
""
|
||||
"dom-append"
|
||||
"store-env-attr"))
|
||||
"sx-render-component"
|
||||
(code :arity 3
|
||||
:bytecode (16 0 1 1 0 52 0 0 2 33 5 0 16 0 32 9 0 1 1 0 16 0 52 2 0 2 17 3 20 3 0 16 2 48 1 17 4 20 4 0 16 4 16 3 48 2 17 5 16 5 52 6 0 1 52 5 0 1 33 16 0 1 8 0 16 3 52 2 0 2 52 7 0 1 32 40 0 16 3 52 10 0 1 52 9 0 1 17 6 51 12 0 1 6 1 1 16 1 52 13 0 1 52 11 0 2 5 20 14 0 16 6 16 4 2 49 3 50)
|
||||
:constants (
|
||||
"starts-with?"
|
||||
"~"
|
||||
"str"
|
||||
"get-render-env"
|
||||
"env-get"
|
||||
"not"
|
||||
"component?"
|
||||
"error"
|
||||
"Unknown component: "
|
||||
"list"
|
||||
"make-symbol"
|
||||
"for-each"
|
||||
(code :arity 1 :upvalue-count 2
|
||||
:bytecode (20 0 0 18 0 20 1 0 20 2 0 16 0 48 1 48 1 48 2 5 20 0 0 18 0 18 1 16 0 52 3 0 2 49 2 50)
|
||||
:constants (
|
||||
"append!"
|
||||
"make-keyword"
|
||||
"to-kebab"
|
||||
"dict-get"))
|
||||
"keys"
|
||||
"render-to-dom"))
|
||||
"process-sx-scripts"
|
||||
(code :arity 1
|
||||
:bytecode (20 0 0 16 0 48 1 17 1 51 2 0 16 1 52 1 0 2 50)
|
||||
:constants (
|
||||
"query-sx-scripts"
|
||||
"for-each"
|
||||
(code :arity 1
|
||||
:bytecode (20 1 0 16 0 1 2 0 48 2 52 0 0 1 33 173 0 20 3 0 16 0 1 2 0 48 2 5 20 4 0 16 0 48 1 17 1 20 5 0 16 0 1 6 0 48 2 33 12 0 20 7 0 16 0 16 1 49 2 32 125 0 16 1 52 8 0 1 6 34 11 0 5 16 1 52 10 0 1 52 9 0 1 33 4 0 2 32 97 0 20 5 0 16 0 1 11 0 48 2 33 21 0 20 12 0 16 1 48 1 17 2 51 14 0 16 2 52 13 0 2 32 63 0 20 5 0 16 0 1 15 0 48 2 33 43 0 20 16 0 16 0 1 15 0 48 2 17 2 20 17 0 16 2 48 1 17 3 16 3 33 13 0 20 18 0 16 3 16 1 2 49 3 32 1 0 2 32 7 0 20 19 0 16 1 49 1 32 1 0 2 50)
|
||||
:constants (
|
||||
"not"
|
||||
"is-processed?"
|
||||
"script"
|
||||
"mark-processed!"
|
||||
"dom-text-content"
|
||||
"dom-has-attr?"
|
||||
"data-components"
|
||||
"process-component-script"
|
||||
"nil?"
|
||||
"empty?"
|
||||
"trim"
|
||||
"data-init"
|
||||
"sx-parse"
|
||||
"for-each"
|
||||
(code :arity 1
|
||||
:bytecode (20 0 0 16 0 49 1 50)
|
||||
:constants (
|
||||
"cek-eval"))
|
||||
"data-mount"
|
||||
"dom-get-attr"
|
||||
"dom-query"
|
||||
"sx-mount"
|
||||
"sx-load-components"))))
|
||||
"process-component-script"
|
||||
(code :arity 2
|
||||
:bytecode (20 0 0 16 0 1 1 0 48 2 17 2 16 2 52 2 0 1 33 38 0 16 1 6 33 15 0 5 16 1 52 5 0 1 52 4 0 1 52 3 0 1 33 10 0 20 6 0 16 1 49 1 32 1 0 2 32 239 0 16 1 6 33 15 0 5 16 1 52 5 0 1 52 4 0 1 52 3 0 1 17 3 20 7 0 1 8 0 48 1 17 4 16 4 16 2 52 9 0 2 33 103 0 16 3 33 41 0 20 10 0 1 8 0 16 2 48 2 5 20 10 0 1 11 0 16 1 48 2 5 20 6 0 16 1 48 1 5 20 12 0 1 13 0 48 1 32 54 0 20 7 0 1 11 0 48 1 17 5 16 5 33 28 0 20 6 0 16 5 48 1 5 20 12 0 1 15 0 16 2 1 16 0 52 14 0 3 48 1 32 11 0 20 17 0 48 0 5 20 18 0 48 0 32 84 0 16 3 33 50 0 20 10 0 1 8 0 16 2 48 2 5 20 10 0 1 11 0 16 1 48 2 5 20 6 0 16 1 48 1 5 20 12 0 1 19 0 16 2 1 16 0 52 14 0 3 48 1 32 29 0 20 20 0 1 8 0 48 1 5 20 20 0 1 11 0 48 1 5 20 17 0 48 0 5 20 18 0 48 0 5 20 21 0 16 2 49 1 50)
|
||||
:constants (
|
||||
"dom-get-attr"
|
||||
"data-hash"
|
||||
"nil?"
|
||||
"not"
|
||||
"empty?"
|
||||
"trim"
|
||||
"sx-load-components"
|
||||
"local-storage-get"
|
||||
"sx-components-hash"
|
||||
"="
|
||||
"local-storage-set"
|
||||
"sx-components-src"
|
||||
"log-info"
|
||||
"components: downloaded (cookie stale)"
|
||||
"str"
|
||||
"components: cached ("
|
||||
")"
|
||||
"clear-sx-comp-cookie"
|
||||
"browser-reload"
|
||||
"components: downloaded ("
|
||||
"local-storage-remove"
|
||||
"set-sx-comp-cookie"))
|
||||
"_page-routes"
|
||||
"list"
|
||||
"process-page-scripts"
|
||||
(code
|
||||
:bytecode (20 0 0 48 0 17 0 20 1 0 1 3 0 16 0 52 4 0 1 1 5 0 52 2 0 3 48 1 5 51 7 0 16 0 52 6 0 2 5 20 1 0 1 8 0 20 9 0 52 4 0 1 1 10 0 52 2 0 3 49 1 50)
|
||||
:constants (
|
||||
"query-page-scripts"
|
||||
"log-info"
|
||||
"str"
|
||||
"pages: found "
|
||||
"len"
|
||||
" script tags"
|
||||
"for-each"
|
||||
(code :arity 1
|
||||
:bytecode (20 1 0 16 0 1 2 0 48 2 52 0 0 1 33 128 0 20 3 0 16 0 1 2 0 48 2 5 20 4 0 16 0 48 1 17 1 20 5 0 1 7 0 16 1 33 9 0 16 1 52 8 0 1 32 3 0 1 9 0 52 6 0 2 48 1 5 16 1 6 33 15 0 5 16 1 52 11 0 1 52 10 0 1 52 0 0 1 33 43 0 20 12 0 16 1 48 1 17 2 20 5 0 1 13 0 16 2 52 8 0 1 1 14 0 52 6 0 3 48 1 5 51 16 0 16 2 52 15 0 2 32 8 0 20 17 0 1 18 0 49 1 32 1 0 2 50)
|
||||
:constants (
|
||||
"not"
|
||||
"is-processed?"
|
||||
"pages"
|
||||
"mark-processed!"
|
||||
"dom-text-content"
|
||||
"log-info"
|
||||
"str"
|
||||
"pages: script text length="
|
||||
"len"
|
||||
0
|
||||
"empty?"
|
||||
"trim"
|
||||
"parse"
|
||||
"pages: parsed "
|
||||
" entries"
|
||||
"for-each"
|
||||
(code :arity 1
|
||||
:bytecode (20 0 0 20 1 0 16 0 1 3 0 20 4 0 16 0 1 6 0 52 5 0 2 48 1 65 1 0 52 2 0 2 49 2 50)
|
||||
:constants (
|
||||
"append!"
|
||||
"_page-routes"
|
||||
"merge"
|
||||
"parsed"
|
||||
"parse-route-pattern"
|
||||
"get"
|
||||
"path"))
|
||||
"log-warn"
|
||||
"pages: script tag is empty"))
|
||||
"pages: "
|
||||
"_page-routes"
|
||||
" routes loaded"))
|
||||
"sx-hydrate-islands"
|
||||
(code :arity 1
|
||||
:bytecode (20 0 0 16 0 6 34 6 0 5 20 1 0 48 0 1 2 0 48 2 17 1 20 3 0 1 5 0 16 1 52 6 0 1 1 7 0 16 0 33 6 0 1 8 0 32 3 0 1 9 0 52 4 0 4 48 1 5 51 11 0 16 1 52 10 0 2 50)
|
||||
:constants (
|
||||
"dom-query-all"
|
||||
"dom-body"
|
||||
"[data-sx-island]"
|
||||
"log-info"
|
||||
"str"
|
||||
"sx-hydrate-islands: "
|
||||
"len"
|
||||
" island(s) in "
|
||||
"subtree"
|
||||
"document"
|
||||
"for-each"
|
||||
(code :arity 1
|
||||
:bytecode (20 0 0 16 0 1 1 0 48 2 33 25 0 20 2 0 1 4 0 20 5 0 16 0 1 6 0 48 2 52 3 0 2 49 1 32 41 0 20 2 0 1 7 0 20 5 0 16 0 1 6 0 48 2 52 3 0 2 48 1 5 20 8 0 16 0 1 1 0 48 2 5 20 9 0 16 0 49 1 50)
|
||||
:constants (
|
||||
"is-processed?"
|
||||
"island-hydrated"
|
||||
"log-info"
|
||||
"str"
|
||||
" skip (already hydrated): "
|
||||
"dom-get-attr"
|
||||
"data-sx-island"
|
||||
" hydrating: "
|
||||
"mark-processed!"
|
||||
"hydrate-island"))))
|
||||
"hydrate-island"
|
||||
(code :arity 1
|
||||
:bytecode (20 0 0 16 0 1 1 0 48 2 17 1 20 0 0 16 0 1 2 0 48 2 6 34 4 0 5 1 3 0 17 2 1 5 0 16 1 52 4 0 2 17 3 20 6 0 2 48 1 17 4 20 7 0 16 4 16 3 48 2 17 5 16 5 52 9 0 1 6 34 7 0 5 16 5 52 10 0 1 52 8 0 1 33 17 0 20 11 0 1 12 0 16 3 52 4 0 2 49 1 32 149 0 20 14 0 16 2 48 1 52 13 0 1 6 34 4 0 5 65 0 0 17 6 52 15 0 0 17 7 20 16 0 16 5 52 17 0 1 16 4 48 2 17 8 51 19 0 1 8 1 6 16 5 52 20 0 1 52 18 0 2 5 20 21 0 51 22 0 1 7 1 5 1 8 51 23 0 1 3 48 2 17 9 20 24 0 16 0 1 25 0 48 2 5 20 26 0 16 0 16 9 48 2 5 20 27 0 16 0 1 28 0 16 7 48 3 5 20 29 0 16 0 48 1 5 20 30 0 1 31 0 16 3 1 32 0 16 7 52 33 0 1 1 34 0 52 4 0 5 49 1 50)
|
||||
:constants (
|
||||
"dom-get-attr"
|
||||
"data-sx-island"
|
||||
"data-sx-state"
|
||||
"{}"
|
||||
"str"
|
||||
"~"
|
||||
"get-render-env"
|
||||
"env-get"
|
||||
"not"
|
||||
"component?"
|
||||
"island?"
|
||||
"log-warn"
|
||||
"hydrate-island: unknown island "
|
||||
"first"
|
||||
"sx-parse"
|
||||
"list"
|
||||
"env-merge"
|
||||
"component-closure"
|
||||
"for-each"
|
||||
(code :arity 1 :upvalue-count 2
|
||||
:bytecode (20 0 0 18 0 16 0 18 1 16 0 52 1 0 2 33 11 0 18 1 16 0 52 2 0 2 32 1 0 2 49 3 50)
|
||||
:constants (
|
||||
"env-bind!"
|
||||
"dict-has?"
|
||||
"dict-get"))
|
||||
"component-params"
|
||||
"cek-try"
|
||||
(code :upvalue-count 3
|
||||
:bytecode (20 0 0 51 1 0 0 0 51 2 0 0 1 0 2 49 2 50)
|
||||
:constants (
|
||||
"with-island-scope"
|
||||
(code :arity 1 :upvalue-count 1
|
||||
:bytecode (20 0 0 18 0 16 0 49 2 50)
|
||||
:constants (
|
||||
"append!"))
|
||||
(code :upvalue-count 2
|
||||
:bytecode (20 0 0 18 0 52 1 0 1 18 1 2 49 3 50)
|
||||
:constants (
|
||||
"render-to-dom"
|
||||
"component-body"))))
|
||||
(code :arity 1 :upvalue-count 1
|
||||
:bytecode (20 0 0 1 2 0 18 0 1 3 0 16 0 52 1 0 4 48 1 5 20 4 0 1 5 0 2 48 2 17 1 20 6 0 16 1 1 7 0 1 8 0 48 3 5 20 6 0 16 1 1 9 0 1 10 0 48 3 5 20 11 0 16 1 1 12 0 18 0 1 13 0 16 0 52 1 0 4 48 2 5 16 1 50)
|
||||
:constants (
|
||||
"log-warn"
|
||||
"str"
|
||||
"hydrate-island FAILED: "
|
||||
" — "
|
||||
"dom-create-element"
|
||||
"div"
|
||||
"dom-set-attr"
|
||||
"class"
|
||||
"sx-island-error"
|
||||
"style"
|
||||
"padding:8px;margin:4px 0;border:1px solid #ef4444;border-radius:4px;background:#fef2f2;color:#b91c1c;font-family:monospace;font-size:12px;white-space:pre-wrap"
|
||||
"dom-set-text-content"
|
||||
"Island error: "
|
||||
"
|
||||
"))
|
||||
"dom-set-text-content"
|
||||
""
|
||||
"dom-append"
|
||||
"dom-set-data"
|
||||
"sx-disposers"
|
||||
"process-elements"
|
||||
"log-info"
|
||||
"hydrated island: "
|
||||
" ("
|
||||
"len"
|
||||
" disposers)"))
|
||||
"dispose-island"
|
||||
(code :arity 1
|
||||
:bytecode (20 0 0 16 0 1 1 0 48 2 17 1 16 1 33 24 0 51 3 0 16 1 52 2 0 2 5 20 4 0 16 0 1 1 0 2 48 3 32 1 0 2 5 20 5 0 16 0 1 6 0 49 2 50)
|
||||
:constants (
|
||||
"dom-get-data"
|
||||
"sx-disposers"
|
||||
"for-each"
|
||||
(code :arity 1
|
||||
:bytecode (20 0 0 16 0 48 1 33 7 0 16 0 49 0 32 1 0 2 50)
|
||||
:constants (
|
||||
"callable?"))
|
||||
"dom-set-data"
|
||||
"clear-processed!"
|
||||
"island-hydrated"))
|
||||
"dispose-islands-in"
|
||||
(code :arity 1
|
||||
:bytecode (16 0 33 98 0 20 0 0 16 0 1 1 0 48 2 17 1 16 1 6 33 11 0 5 16 1 52 3 0 1 52 2 0 1 33 62 0 51 5 0 16 1 52 4 0 2 17 2 16 2 52 3 0 1 52 2 0 1 33 34 0 20 6 0 1 8 0 16 2 52 9 0 1 1 10 0 52 7 0 3 48 1 5 20 12 0 16 2 52 11 0 2 32 1 0 2 32 1 0 2 32 1 0 2 50)
|
||||
:constants (
|
||||
"dom-query-all"
|
||||
"[data-sx-island]"
|
||||
"not"
|
||||
"empty?"
|
||||
"filter"
|
||||
(code :arity 1
|
||||
:bytecode (20 1 0 16 0 1 2 0 48 2 52 0 0 1 50)
|
||||
:constants (
|
||||
"not"
|
||||
"is-processed?"
|
||||
"island-hydrated"))
|
||||
"log-info"
|
||||
"str"
|
||||
"disposing "
|
||||
"len"
|
||||
" island(s)"
|
||||
"for-each"
|
||||
"dispose-island"))
|
||||
"force-dispose-islands-in"
|
||||
(code :arity 1
|
||||
:bytecode (16 0 33 70 0 20 0 0 16 0 1 1 0 48 2 17 1 16 1 6 33 11 0 5 16 1 52 3 0 1 52 2 0 1 33 34 0 20 4 0 1 6 0 16 1 52 7 0 1 1 8 0 52 5 0 3 48 1 5 20 10 0 16 1 52 9 0 2 32 1 0 2 32 1 0 2 50)
|
||||
:constants (
|
||||
"dom-query-all"
|
||||
"[data-sx-island]"
|
||||
"not"
|
||||
"empty?"
|
||||
"log-info"
|
||||
"str"
|
||||
"force-disposing "
|
||||
"len"
|
||||
" island(s)"
|
||||
"for-each"
|
||||
"dispose-island"))
|
||||
"*pre-render-hooks*"
|
||||
"*post-render-hooks*"
|
||||
"register-pre-render-hook"
|
||||
(code :arity 1
|
||||
:bytecode (20 0 0 20 1 0 16 0 49 2 50)
|
||||
:constants (
|
||||
"append!"
|
||||
"*pre-render-hooks*"))
|
||||
"register-post-render-hook"
|
||||
(code :arity 1
|
||||
:bytecode (20 0 0 20 1 0 16 0 49 2 50)
|
||||
:constants (
|
||||
"append!"
|
||||
"*post-render-hooks*"))
|
||||
"run-pre-render-hooks"
|
||||
(code
|
||||
:bytecode (51 1 0 20 2 0 52 0 0 2 50)
|
||||
:constants (
|
||||
"for-each"
|
||||
(code :arity 1
|
||||
:bytecode (20 0 0 16 0 2 49 2 50)
|
||||
:constants (
|
||||
"cek-call"))
|
||||
"*pre-render-hooks*"))
|
||||
"run-post-render-hooks"
|
||||
(code
|
||||
:bytecode (20 0 0 1 2 0 20 4 0 52 3 0 1 1 5 0 52 1 0 3 48 1 5 51 7 0 20 4 0 52 6 0 2 50)
|
||||
:constants (
|
||||
"log-info"
|
||||
"str"
|
||||
"run-post-render-hooks: "
|
||||
"len"
|
||||
"*post-render-hooks*"
|
||||
" hooks"
|
||||
"for-each"
|
||||
(code :arity 1
|
||||
:bytecode (20 0 0 1 2 0 16 0 52 3 0 1 1 4 0 20 5 0 16 0 48 1 1 6 0 16 0 52 7 0 1 52 1 0 6 48 1 5 20 8 0 16 0 2 49 2 50)
|
||||
:constants (
|
||||
"log-info"
|
||||
"str"
|
||||
" hook type: "
|
||||
"type-of"
|
||||
" callable: "
|
||||
"callable?"
|
||||
" lambda: "
|
||||
"lambda?"
|
||||
"cek-call"))))
|
||||
"boot-init"
|
||||
(code
|
||||
:bytecode (20 0 0 1 2 0 20 3 0 52 1 0 2 48 1 5 20 4 0 48 0 5 20 5 0 48 0 5 20 6 0 2 48 1 5 20 7 0 2 48 1 5 20 8 0 2 48 1 5 20 9 0 48 0 5 20 10 0 2 48 1 5 20 11 0 20 12 0 48 0 1 13 0 51 14 0 49 3 50)
|
||||
:constants (
|
||||
"log-info"
|
||||
"str"
|
||||
"sx-browser "
|
||||
"SX_VERSION"
|
||||
"init-css-tracking"
|
||||
"process-page-scripts"
|
||||
"process-sx-scripts"
|
||||
"sx-hydrate-elements"
|
||||
"sx-hydrate-islands"
|
||||
"run-post-render-hooks"
|
||||
"process-elements"
|
||||
"dom-listen"
|
||||
"dom-window"
|
||||
"popstate"
|
||||
(code :arity 1
|
||||
:bytecode (20 0 0 1 1 0 49 1 50)
|
||||
:constants (
|
||||
"handle-popstate"
|
||||
0)))))))
|
||||
File diff suppressed because one or more lines are too long
@@ -299,7 +299,11 @@
|
||||
()
|
||||
(let
|
||||
((swap-result (swap-dom-nodes target content swap-style)))
|
||||
(post-swap (or swap-result target)))))))))))))
|
||||
(post-swap
|
||||
(if
|
||||
(= swap-style "outerHTML")
|
||||
(dom-parent (or swap-result target))
|
||||
(or swap-result target))))))))))))))
|
||||
|
||||
(define
|
||||
handle-html-response
|
||||
@@ -325,8 +329,15 @@
|
||||
use-transition
|
||||
(fn
|
||||
()
|
||||
(swap-html-string target html swap-style)
|
||||
(post-swap target))))
|
||||
(let
|
||||
((swap-root (swap-html-string target html swap-style)))
|
||||
(log-info
|
||||
(str
|
||||
"swap-root: "
|
||||
(if swap-root (dom-tag-name swap-root) "nil")
|
||||
" target: "
|
||||
(dom-tag-name target)))
|
||||
(post-swap (or swap-root target))))))
|
||||
(let
|
||||
((container (dom-create-element "div" nil)))
|
||||
(dom-set-inner-html container (dom-body-inner-html doc))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,120 +1,12 @@
|
||||
// Navigation tests for sx-docs
|
||||
// Verifies client-side navigation works correctly after sx-host migration.
|
||||
// Verifies navigation works correctly with the OCaml sx-host.
|
||||
|
||||
const { test, expect } = require('playwright/test');
|
||||
const BASE_URL = process.env.SX_TEST_URL || 'http://localhost:8013';
|
||||
|
||||
test.describe('Client-side Navigation', () => {
|
||||
test.describe('Page Navigation', () => {
|
||||
|
||||
test('layout stays vertical after clicking nav link', async ({ page }) => {
|
||||
await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Click "Reactive Islands" nav link
|
||||
await page.click('a[href*="geography.(reactive"]:not([href*="runtime"])');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Page should have navigated
|
||||
expect(page.url()).toContain('reactive');
|
||||
|
||||
// After navigation, the page title/heading should be visible and centered
|
||||
// NOT pushed to the right side by the header
|
||||
const heading = await page.locator('h1, h2').first().boundingBox();
|
||||
const viewport = page.viewportSize();
|
||||
|
||||
if (heading && viewport) {
|
||||
// The heading should be centered-ish, not pushed far right
|
||||
// If it's past 60% of viewport width, layout is broken (side-by-side)
|
||||
expect(heading.x).toBeLessThan(viewport.width * 0.5);
|
||||
}
|
||||
|
||||
// The page should NOT have two visible columns where header and content
|
||||
// are side by side
|
||||
const screenshot = await page.screenshot();
|
||||
// Just verify the content area starts near the top
|
||||
if (heading) {
|
||||
expect(heading.y).toBeLessThan(400); // Content should be within first 400px
|
||||
}
|
||||
});
|
||||
|
||||
test('content updates after navigation', async ({ page }) => {
|
||||
await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'networkidle' });
|
||||
|
||||
// Geography page should have "Geography" heading
|
||||
const geoText = await page.textContent('body');
|
||||
expect(geoText).toContain('Geography');
|
||||
|
||||
// Click on "CEK Machine" link
|
||||
const cekLink = page.locator('a:has-text("CEK Machine")');
|
||||
if (await cekLink.count() > 0) {
|
||||
await cekLink.first().click();
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Content should now mention CEK
|
||||
const bodyText = await page.textContent('body');
|
||||
expect(bodyText).toContain('CEK');
|
||||
}
|
||||
});
|
||||
|
||||
test('no raw SX component calls visible after navigation', async ({ page }) => {
|
||||
await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click a nav link
|
||||
await page.click('a[href*="hypermedia"]:not([href*="example"])');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Check no raw SX calls visible in the main content area
|
||||
const mainText = await page.locator('#main-panel, #root-panel, main').first().textContent();
|
||||
// ~cssx/tw calls should be expanded, not visible as text
|
||||
const rawCssx = (mainText.match(/~cssx\/tw/g) || []).length;
|
||||
expect(rawCssx).toBeLessThan(3); // Allow a few in documentation text
|
||||
});
|
||||
|
||||
test('header island survives navigation', async ({ page }) => {
|
||||
await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'networkidle' });
|
||||
await page.waitForSelector('[data-sx-island="layouts/header"]', { timeout: 10000 });
|
||||
|
||||
// Header should have the logo
|
||||
const headerText = await page.locator('[data-sx-island="layouts/header"]').textContent();
|
||||
expect(headerText).toContain('sx');
|
||||
|
||||
// Navigate
|
||||
await page.click('a[href*="hypermedia"]:not([href*="example"])');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Header should still be present and have content
|
||||
const headerAfter = await page.locator('[data-sx-island="layouts/header"]');
|
||||
await expect(headerAfter).toBeVisible();
|
||||
const headerTextAfter = await headerAfter.textContent();
|
||||
expect(headerTextAfter).toContain('sx');
|
||||
});
|
||||
|
||||
test('navigation does not create side-by-side layout', async ({ page }) => {
|
||||
await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Navigate to Hypermedia
|
||||
await page.click('a[href*="geography.(hypermedia"]:not([href*="example"])');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// The header/nav should NOT be beside the content (side by side)
|
||||
// Check that there's no element with the logo text at x < 300
|
||||
// while content heading is at x > 300
|
||||
const logo = await page.locator('[data-sx-island="layouts/header"]').boundingBox();
|
||||
const heading = await page.locator('h1, h2').first().boundingBox();
|
||||
|
||||
if (logo && heading) {
|
||||
// Both should be roughly centered, not one left and one right
|
||||
const logoCenter = logo.x + logo.width / 2;
|
||||
const headingCenter = heading.x + heading.width / 2;
|
||||
const drift = Math.abs(logoCenter - headingCenter);
|
||||
// If drift > 300px, they're side by side (broken layout)
|
||||
expect(drift).toBeLessThan(300);
|
||||
}
|
||||
});
|
||||
|
||||
test('browser back button restores previous page content', async ({ page }) => {
|
||||
// Collect console errors
|
||||
test('clicking nav button navigates to new page', async ({ page }) => {
|
||||
const errors = [];
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') errors.push(msg.text());
|
||||
@@ -123,89 +15,127 @@ test.describe('Client-side Navigation', () => {
|
||||
await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Navigate forward to Hypermedia
|
||||
await page.click('a[href*="geography.(hypermedia"]:not([href*="example"])');
|
||||
// Click "Reactive Islands" nav link
|
||||
await page.click('a[href*="geography.(reactive)"]:not([href*="runtime"])');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Verify navigation worked — URL must contain hypermedia
|
||||
expect(page.url()).toContain('hypermedia');
|
||||
// Should have navigated — URL must contain reactive
|
||||
expect(page.url()).toContain('reactive');
|
||||
|
||||
// Go back
|
||||
await page.goBack();
|
||||
await page.waitForTimeout(3000);
|
||||
// Page should show Reactive Islands content
|
||||
const body = await page.textContent('body');
|
||||
expect(body).toContain('Reactive Islands');
|
||||
|
||||
// URL should return
|
||||
expect(page.url()).toContain('geography');
|
||||
expect(page.url()).not.toContain('hypermedia');
|
||||
|
||||
// Content MUST change back — the main heading should say Geography,
|
||||
// NOT still show Hypermedia content
|
||||
const heading = await page.locator('#main-panel h1, #main-panel h2').first();
|
||||
await expect(heading).toContainText('Geography', { timeout: 5000 });
|
||||
|
||||
// No JIT errors should have occurred during navigation
|
||||
const jitErrors = errors.filter(e => e.includes('Not callable: nil'));
|
||||
expect(jitErrors.length).toBe(0);
|
||||
// No SX evaluation errors
|
||||
const sxErrors = errors.filter(e => e.includes('Undefined symbol'));
|
||||
expect(sxErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('back button preserves layout (no side-by-side)', async ({ page }) => {
|
||||
await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Navigate forward
|
||||
await page.click('a[href*="geography.(reactive"]:not([href*="runtime"])');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Go back
|
||||
await page.goBack();
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Check layout is vertical — heading should be within top part of page
|
||||
const heading = await page.locator('h1, h2').first().boundingBox();
|
||||
if (heading) {
|
||||
expect(heading.y).toBeLessThan(500);
|
||||
const viewport = page.viewportSize();
|
||||
if (viewport) {
|
||||
expect(heading.x).toBeLessThan(viewport.width * 0.5);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('no JIT errors during navigation', async ({ page }) => {
|
||||
test('clicking header logo navigates home', async ({ page }) => {
|
||||
const errors = [];
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error' && msg.text().includes('FAIL')) {
|
||||
errors.push(msg.text());
|
||||
}
|
||||
if (msg.type() === 'error') errors.push(msg.text());
|
||||
});
|
||||
|
||||
await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Navigate
|
||||
await page.click('a[href*="geography.(hypermedia"]:not([href*="example"])');
|
||||
// Click the logo in the header island
|
||||
await page.click('[data-sx-island="layouts/header"] a[href="/sx/"]');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Check for JIT errors — these indicate broken CSSX function resolution
|
||||
const jitErrors = errors.filter(e => e.includes('Not callable: nil'));
|
||||
expect(jitErrors.length).toBe(0);
|
||||
// Should have navigated to home
|
||||
expect(page.url()).toMatch(/\/sx\/?$/);
|
||||
|
||||
// No SX evaluation errors
|
||||
const sxErrors = errors.filter(e => e.includes('Undefined symbol'));
|
||||
expect(sxErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('full page width is used (no side-by-side split)', async ({ page }) => {
|
||||
test('back button works after navigation', async ({ page }) => {
|
||||
const errors = [];
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') errors.push(msg.text());
|
||||
});
|
||||
|
||||
await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Navigate to Reactive Islands
|
||||
await page.click('a[href*="geography.(reactive)"]:not([href*="runtime"])');
|
||||
await page.waitForTimeout(3000);
|
||||
expect(page.url()).toContain('reactive');
|
||||
|
||||
// Go back
|
||||
await page.goBack();
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Should be back at Geography
|
||||
expect(page.url()).toContain('geography');
|
||||
expect(page.url()).not.toContain('reactive');
|
||||
|
||||
// Geography heading should be visible
|
||||
const heading = await page.locator('h1, h2').first();
|
||||
await expect(heading).toContainText('Geography', { timeout: 5000 });
|
||||
|
||||
// No SX errors
|
||||
const sxErrors = errors.filter(e => e.includes('Undefined symbol'));
|
||||
expect(sxErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('no console errors on page load', async ({ page }) => {
|
||||
const errors = [];
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error' && !msg.text().includes('404'))
|
||||
errors.push(msg.text());
|
||||
});
|
||||
|
||||
await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// No JIT or SX errors
|
||||
const sxErrors = errors.filter(e =>
|
||||
e.includes('Undefined symbol') || e.includes('Not callable'));
|
||||
expect(sxErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('copyright shows current route after SX navigation', async ({ page }) => {
|
||||
await page.goto(BASE_URL + '/sx/', { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Mark the page to verify SX navigation (not full reload)
|
||||
await page.evaluate(() => window.__sx_nav_marker = true);
|
||||
|
||||
// Before: copyright shows the current path
|
||||
const before = await page.evaluate(() =>
|
||||
document.querySelector('[data-sx-lake="copyright"]')?.textContent);
|
||||
expect(before).toContain('/sx/');
|
||||
|
||||
// Navigate via SX (sx-get link)
|
||||
await page.click('a[sx-get*="(geography)"]');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Verify SX navigation (marker survives SX nav, lost on reload)
|
||||
const marker = await page.evaluate(() => window.__sx_nav_marker);
|
||||
expect(marker).toBe(true);
|
||||
|
||||
// After: copyright must still show a route path
|
||||
const after = await page.evaluate(() =>
|
||||
document.querySelector('[data-sx-lake="copyright"]')?.textContent);
|
||||
expect(after).toContain('geography');
|
||||
});
|
||||
|
||||
test('header island renders with SSR', async ({ page }) => {
|
||||
await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'networkidle' });
|
||||
|
||||
// Navigate to a child page
|
||||
await page.click('a[href*="reactive"]:not([href*="runtime"])');
|
||||
await page.waitForTimeout(3000);
|
||||
// Header should be visible
|
||||
const header = page.locator('[data-sx-island="layouts/header"]');
|
||||
await expect(header).toBeVisible();
|
||||
|
||||
// The main content area should use most of the viewport width
|
||||
const viewport = page.viewportSize();
|
||||
const content = await page.locator('h1, h2, [id="main-panel"]').first().boundingBox();
|
||||
// Should contain the logo
|
||||
await expect(header).toContainText('sx');
|
||||
|
||||
if (content && viewport) {
|
||||
// Content should not be squeezed to one side
|
||||
// It should start within the first 40% of viewport width
|
||||
expect(content.x).toBeLessThan(viewport.width * 0.4);
|
||||
}
|
||||
// Should contain copyright
|
||||
await expect(header).toContainText('Giles Bradshaw');
|
||||
});
|
||||
});
|
||||
|
||||
1329
web/adapter-dom.sx
1329
web/adapter-dom.sx
File diff suppressed because one or more lines are too long
@@ -299,7 +299,11 @@
|
||||
()
|
||||
(let
|
||||
((swap-result (swap-dom-nodes target content swap-style)))
|
||||
(post-swap (or swap-result target)))))))))))))
|
||||
(post-swap
|
||||
(if
|
||||
(= swap-style "outerHTML")
|
||||
(dom-parent (or swap-result target))
|
||||
(or swap-result target))))))))))))))
|
||||
|
||||
(define
|
||||
handle-html-response
|
||||
@@ -325,8 +329,15 @@
|
||||
use-transition
|
||||
(fn
|
||||
()
|
||||
(swap-html-string target html swap-style)
|
||||
(post-swap target))))
|
||||
(let
|
||||
((swap-root (swap-html-string target html swap-style)))
|
||||
(log-info
|
||||
(str
|
||||
"swap-root: "
|
||||
(if swap-root (dom-tag-name swap-root) "nil")
|
||||
" target: "
|
||||
(dom-tag-name target)))
|
||||
(post-swap (or swap-root target))))))
|
||||
(let
|
||||
((container (dom-create-element "div" nil)))
|
||||
(dom-set-inner-html container (dom-body-inner-html doc))
|
||||
|
||||
Reference in New Issue
Block a user