diff --git a/shared/static/wasm/sx/boot.sx b/shared/static/wasm/sx/boot.sx index 408f6f9d..9b24e00b 100644 --- a/shared/static/wasm/sx/boot.sx +++ b/shared/static/wasm/sx/boot.sx @@ -431,4 +431,10 @@ (sx-hydrate-islands nil) (run-post-render-hooks) (process-elements nil) - (dom-listen (dom-window) "popstate" (fn (e) (handle-popstate 0)))))) + (dom-listen (dom-window) "popstate" (fn (e) (handle-popstate 0))) + (dom-set-attr + (host-get (dom-document) "documentElement") + "data-sx-ready" + "true") + (dom-dispatch (dom-document) "sx:ready" nil) + (log-info "sx:ready")))) diff --git a/shared/static/wasm/sx/boot.sxbc.json b/shared/static/wasm/sx/boot.sxbc.json index f771a183..e34637c4 100644 --- a/shared/static/wasm/sx/boot.sxbc.json +++ b/shared/static/wasm/sx/boot.sxbc.json @@ -1 +1 @@ -{"magic":"SXBC","version":1,"hash":"a6d8d388e0d3dc9c","module":{"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":[{"t":"s","v":"HEAD_HOIST_SELECTOR"},{"t":"s","v":"meta, title, link[rel='canonical'], script[type='application/ld+json']"},{"t":"s","v":"hoist-head-elements-full"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,20,2,0,48,2,17,1,51,4,0,20,5,0,52,3,0,2,50],"constants":[{"t":"s","v":"dom-query-all"},{"t":"s","v":"root"},{"t":"s","v":"HEAD_HOIST_SELECTOR"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,1,0,20,2,0,48,1,52,0,0,1,17,1,20,4,0,1,5,0,52,3,0,2,33,33,0,20,6,0,20,7,0,20,2,0,48,1,48,1,5,20,8,0,20,9,0,20,2,0,48,1,20,2,0,49,2,32,223,0,20,4,0,1,10,0,52,3,0,2,33,112,0,20,11,0,20,2,0,1,12,0,48,2,17,2,20,11,0,20,2,0,1,13,0,48,2,17,3,20,12,0,33,21,0,20,14,0,1,16,0,20,12,0,1,17,0,52,15,0,3,48,1,32,1,0,2,5,20,18,0,33,21,0,20,14,0,1,19,0,20,18,0,1,17,0,52,15,0,3,48,1,32,1,0,2,5,20,8,0,20,9,0,20,2,0,48,1,20,2,0,48,2,5,20,20,0,20,2,0,49,1,32,98,0,20,4,0,1,21,0,52,3,0,2,6,33,19,0,5,20,11,0,20,2,0,1,22,0,48,2,1,23,0,52,3,0,2,33,37,0,20,14,0,1,24,0,48,1,5,20,8,0,20,9,0,20,2,0,48,1,20,2,0,48,2,5,20,20,0,20,2,0,49,1,32,25,0,20,8,0,20,9,0,20,2,0,48,1,20,2,0,48,2,5,20,20,0,20,2,0,49,1,50],"constants":[{"t":"s","v":"lower"},{"t":"s","v":"dom-tag-name"},{"t":"s","v":"el"},{"t":"s","v":"="},{"t":"s","v":"tag"},{"t":"s","v":"title"},{"t":"s","v":"set-document-title"},{"t":"s","v":"dom-text-content"},{"t":"s","v":"dom-remove-child"},{"t":"s","v":"dom-parent"},{"t":"s","v":"meta"},{"t":"s","v":"dom-get-attr"},{"t":"s","v":"name"},{"t":"s","v":"property"},{"t":"s","v":"remove-head-element"},{"t":"s","v":"str"},{"t":"s","v":"meta[name=\""},{"t":"s","v":"\"]"},{"t":"s","v":"prop"},{"t":"s","v":"meta[property=\""},{"t":"s","v":"dom-append-to-head"},{"t":"s","v":"link"},{"t":"s","v":"rel"},{"t":"s","v":"canonical"},{"t":"s","v":"link[rel=\"canonical\"]"}]}},{"t":"s","v":"els"}]}},{"t":"s","v":"sx-mount"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,48,1,17,3,20,2,0,33,100,0,20,4,0,20,2,0,48,1,52,3,0,1,33,48,0,20,5,0,20,6,0,20,7,0,48,2,17,4,20,8,0,20,2,0,1,9,0,48,2,5,20,10,0,20,2,0,20,11,0,48,2,5,20,12,0,20,2,0,48,1,32,1,0,2,5,20,13,0,20,2,0,48,1,5,20,14,0,20,2,0,48,1,5,20,15,0,20,2,0,48,1,5,20,16,0,49,0,32,1,0,2,50],"constants":[{"t":"s","v":"resolve-mount-target"},{"t":"s","v":"target"},{"t":"s","v":"el"},{"t":"s","v":"empty?"},{"t":"s","v":"dom-child-list"},{"t":"s","v":"sx-render-with-env"},{"t":"s","v":"source"},{"t":"s","v":"extra-env"},{"t":"s","v":"dom-set-text-content"},{"t":"s","v":""},{"t":"s","v":"dom-append"},{"t":"s","v":"node"},{"t":"s","v":"hoist-head-elements-full"},{"t":"s","v":"process-elements"},{"t":"s","v":"sx-hydrate-elements"},{"t":"s","v":"sx-hydrate-islands"},{"t":"s","v":"run-post-render-hooks"}]}},{"t":"s","v":"resolve-suspense"},{"t":"code","v":{"bytecode":[20,0,0,2,48,1,5,20,1,0,1,3,0,20,4,0,1,5,0,52,2,0,3,48,1,17,2,20,6,0,33,97,0,20,7,0,20,8,0,48,1,17,3,20,9,0,2,48,1,17,4,20,10,0,20,6,0,1,11,0,48,2,5,51,13,0,20,14,0,52,12,0,2,5,20,15,0,20,6,0,48,1,5,20,16,0,20,6,0,48,1,5,20,17,0,20,6,0,48,1,5,20,18,0,48,0,5,20,19,0,20,6,0,1,20,0,1,4,0,20,4,0,65,1,0,49,3,32,15,0,20,21,0,1,22,0,20,4,0,52,2,0,2,49,1,50],"constants":[{"t":"s","v":"process-sx-scripts"},{"t":"s","v":"dom-query"},{"t":"s","v":"str"},{"t":"s","v":"[data-suspense=\""},{"t":"s","v":"id"},{"t":"s","v":"\"]"},{"t":"s","v":"el"},{"t":"s","v":"parse"},{"t":"s","v":"sx"},{"t":"s","v":"get-render-env"},{"t":"s","v":"dom-set-text-content"},{"t":"s","v":""},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,20,2,0,20,3,0,20,4,0,2,48,3,49,2,50],"constants":[{"t":"s","v":"dom-append"},{"t":"s","v":"el"},{"t":"s","v":"render-to-dom"},{"t":"s","v":"expr"},{"t":"s","v":"env"}]}},{"t":"s","v":"exprs"},{"t":"s","v":"process-elements"},{"t":"s","v":"sx-hydrate-elements"},{"t":"s","v":"sx-hydrate-islands"},{"t":"s","v":"run-post-render-hooks"},{"t":"s","v":"dom-dispatch"},{"t":"s","v":"sx:resolved"},{"t":"s","v":"log-warn"},{"t":"s","v":"resolveSuspense: no element for id="}]}},{"t":"s","v":"sx-hydrate-elements"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,6,34,6,0,5,20,2,0,48,0,1,3,0,48,2,17,1,51,5,0,20,6,0,52,4,0,2,50],"constants":[{"t":"s","v":"dom-query-all"},{"t":"s","v":"root"},{"t":"s","v":"dom-body"},{"t":"s","v":"[data-sx]"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,1,0,20,2,0,1,3,0,48,2,52,0,0,1,33,24,0,20,4,0,20,2,0,1,3,0,48,2,5,20,5,0,20,2,0,2,49,2,32,1,0,2,50],"constants":[{"t":"s","v":"not"},{"t":"s","v":"is-processed?"},{"t":"s","v":"el"},{"t":"s","v":"hydrated"},{"t":"s","v":"mark-processed!"},{"t":"s","v":"sx-update-element"}]}},{"t":"s","v":"els"}]}},{"t":"s","v":"sx-update-element"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,48,1,17,2,20,2,0,33,110,0,20,3,0,20,2,0,1,4,0,48,2,17,3,20,5,0,33,87,0,20,6,0,20,2,0,48,1,17,4,20,7,0,20,8,0,20,9,0,48,2,17,5,20,10,0,20,5,0,20,11,0,48,2,17,6,20,12,0,20,2,0,1,13,0,48,2,5,20,14,0,20,2,0,20,15,0,48,2,5,20,9,0,33,17,0,20,16,0,20,2,0,20,8,0,20,9,0,49,3,32,1,0,2,32,1,0,2,32,1,0,2,50],"constants":[{"t":"s","v":"resolve-mount-target"},{"t":"s","v":"el"},{"t":"s","v":"target"},{"t":"s","v":"dom-get-attr"},{"t":"s","v":"data-sx"},{"t":"s","v":"source"},{"t":"s","v":"parse-env-attr"},{"t":"s","v":"merge-envs"},{"t":"s","v":"base-env"},{"t":"s","v":"new-env"},{"t":"s","v":"sx-render-with-env"},{"t":"s","v":"env"},{"t":"s","v":"dom-set-text-content"},{"t":"s","v":""},{"t":"s","v":"dom-append"},{"t":"s","v":"node"},{"t":"s","v":"store-env-attr"}]}},{"t":"s","v":"sx-render-component"},{"t":"code","v":{"bytecode":[20,1,0,1,2,0,52,0,0,2,33,6,0,20,1,0,32,10,0,1,2,0,20,1,0,52,3,0,2,17,3,20,4,0,20,5,0,48,1,17,4,20,6,0,20,7,0,20,8,0,48,2,17,5,20,11,0,52,10,0,1,52,9,0,1,33,17,0,1,13,0,20,8,0,52,3,0,2,52,12,0,1,32,40,0,20,8,0,52,15,0,1,52,14,0,1,17,6,51,17,0,20,19,0,52,18,0,1,52,16,0,2,5,20,20,0,20,21,0,20,7,0,2,49,3,50],"constants":[{"t":"s","v":"starts-with?"},{"t":"s","v":"name"},{"t":"s","v":"~"},{"t":"s","v":"str"},{"t":"s","v":"get-render-env"},{"t":"s","v":"extra-env"},{"t":"s","v":"env-get"},{"t":"s","v":"env"},{"t":"s","v":"full-name"},{"t":"s","v":"not"},{"t":"s","v":"component?"},{"t":"s","v":"comp"},{"t":"s","v":"error"},{"t":"s","v":"Unknown component: "},{"t":"s","v":"list"},{"t":"s","v":"make-symbol"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,20,2,0,20,3,0,20,4,0,48,1,48,1,48,2,5,20,0,0,20,1,0,20,6,0,20,4,0,52,5,0,2,49,2,50],"constants":[{"t":"s","v":"append!"},{"t":"s","v":"call-expr"},{"t":"s","v":"make-keyword"},{"t":"s","v":"to-kebab"},{"t":"s","v":"k"},{"t":"s","v":"dict-get"},{"t":"s","v":"kwargs"}]}},{"t":"s","v":"keys"},{"t":"s","v":"kwargs"},{"t":"s","v":"render-to-dom"},{"t":"s","v":"call-expr"}]}},{"t":"s","v":"process-sx-scripts"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,48,1,17,1,51,3,0,20,4,0,52,2,0,2,50],"constants":[{"t":"s","v":"query-sx-scripts"},{"t":"s","v":"root"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,1,0,20,2,0,1,3,0,48,2,52,0,0,1,33,190,0,20,4,0,20,2,0,1,3,0,48,2,5,20,5,0,20,2,0,48,1,17,1,20,6,0,20,2,0,1,7,0,48,2,33,14,0,20,8,0,20,2,0,20,9,0,49,2,32,137,0,20,9,0,52,10,0,1,6,34,12,0,5,20,9,0,52,12,0,1,52,11,0,1,33,4,0,2,32,107,0,20,6,0,20,2,0,1,13,0,48,2,33,23,0,20,14,0,20,9,0,48,1,17,2,51,16,0,20,17,0,52,15,0,2,32,70,0,20,6,0,20,2,0,1,18,0,48,2,33,48,0,20,19,0,20,2,0,1,18,0,48,2,17,2,20,20,0,20,21,0,48,1,17,3,20,22,0,33,15,0,20,23,0,20,22,0,20,9,0,2,49,3,32,1,0,2,32,8,0,20,24,0,20,9,0,49,1,32,1,0,2,50],"constants":[{"t":"s","v":"not"},{"t":"s","v":"is-processed?"},{"t":"s","v":"s"},{"t":"s","v":"script"},{"t":"s","v":"mark-processed!"},{"t":"s","v":"dom-text-content"},{"t":"s","v":"dom-has-attr?"},{"t":"s","v":"data-components"},{"t":"s","v":"process-component-script"},{"t":"s","v":"text"},{"t":"s","v":"nil?"},{"t":"s","v":"empty?"},{"t":"s","v":"trim"},{"t":"s","v":"data-init"},{"t":"s","v":"sx-parse"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,49,1,50],"constants":[{"t":"s","v":"cek-eval"},{"t":"s","v":"expr"}]}},{"t":"s","v":"exprs"},{"t":"s","v":"data-mount"},{"t":"s","v":"dom-get-attr"},{"t":"s","v":"dom-query"},{"t":"s","v":"mount-sel"},{"t":"s","v":"target"},{"t":"s","v":"sx-mount"},{"t":"s","v":"sx-load-components"}]}},{"t":"s","v":"scripts"}]}},{"t":"s","v":"process-component-script"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,1,2,0,48,2,17,2,20,4,0,52,3,0,1,33,41,0,20,5,0,6,33,16,0,5,20,5,0,52,8,0,1,52,7,0,1,52,6,0,1,33,11,0,20,9,0,20,5,0,49,1,32,1,0,2,32,0,1,20,5,0,6,33,16,0,5,20,5,0,52,8,0,1,52,7,0,1,52,6,0,1,17,3,20,10,0,1,11,0,48,1,17,4,20,13,0,20,4,0,52,12,0,2,33,110,0,20,14,0,33,44,0,20,15,0,1,11,0,20,4,0,48,2,5,20,15,0,1,16,0,20,5,0,48,2,5,20,9,0,20,5,0,48,1,5,20,17,0,1,18,0,48,1,32,57,0,20,10,0,1,16,0,48,1,17,5,20,19,0,33,30,0,20,9,0,20,19,0,48,1,5,20,17,0,1,21,0,20,4,0,1,22,0,52,20,0,3,48,1,32,11,0,20,23,0,48,0,5,20,24,0,48,0,32,89,0,20,14,0,33,54,0,20,15,0,1,11,0,20,4,0,48,2,5,20,15,0,1,16,0,20,5,0,48,2,5,20,9,0,20,5,0,48,1,5,20,17,0,1,25,0,20,4,0,1,22,0,52,20,0,3,48,1,32,29,0,20,26,0,1,11,0,48,1,5,20,26,0,1,16,0,48,1,5,20,23,0,48,0,5,20,24,0,48,0,5,20,27,0,20,4,0,49,1,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"script"},{"t":"s","v":"data-hash"},{"t":"s","v":"nil?"},{"t":"s","v":"hash"},{"t":"s","v":"text"},{"t":"s","v":"not"},{"t":"s","v":"empty?"},{"t":"s","v":"trim"},{"t":"s","v":"sx-load-components"},{"t":"s","v":"local-storage-get"},{"t":"s","v":"sx-components-hash"},{"t":"s","v":"="},{"t":"s","v":"cached-hash"},{"t":"s","v":"has-inline"},{"t":"s","v":"local-storage-set"},{"t":"s","v":"sx-components-src"},{"t":"s","v":"log-info"},{"t":"s","v":"components: downloaded (cookie stale)"},{"t":"s","v":"cached"},{"t":"s","v":"str"},{"t":"s","v":"components: cached ("},{"t":"s","v":")"},{"t":"s","v":"clear-sx-comp-cookie"},{"t":"s","v":"browser-reload"},{"t":"s","v":"components: downloaded ("},{"t":"s","v":"local-storage-remove"},{"t":"s","v":"set-sx-comp-cookie"}]}},{"t":"s","v":"_page-routes"},{"t":"s","v":"list"},{"t":"s","v":"process-page-scripts"},{"t":"code","v":{"bytecode":[20,0,0,48,0,17,0,20,1,0,1,3,0,20,5,0,52,4,0,1,1,6,0,52,2,0,3,48,1,5,51,8,0,20,5,0,52,7,0,2,5,20,1,0,1,9,0,20,10,0,52,4,0,1,1,11,0,52,2,0,3,49,1,50],"constants":[{"t":"s","v":"query-page-scripts"},{"t":"s","v":"log-info"},{"t":"s","v":"str"},{"t":"s","v":"pages: found "},{"t":"s","v":"len"},{"t":"s","v":"scripts"},{"t":"s","v":" script tags"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,1,0,20,2,0,1,3,0,48,2,52,0,0,1,33,137,0,20,4,0,20,2,0,1,3,0,48,2,5,20,5,0,20,2,0,48,1,17,1,20,6,0,1,8,0,20,9,0,33,10,0,20,9,0,52,10,0,1,32,3,0,1,11,0,52,7,0,2,48,1,5,20,9,0,6,33,16,0,5,20,9,0,52,13,0,1,52,12,0,1,52,0,0,1,33,46,0,20,14,0,20,9,0,48,1,17,2,20,6,0,1,15,0,20,3,0,52,10,0,1,1,16,0,52,7,0,3,48,1,5,51,18,0,20,3,0,52,17,0,2,32,8,0,20,19,0,1,20,0,49,1,32,1,0,2,50],"constants":[{"t":"s","v":"not"},{"t":"s","v":"is-processed?"},{"t":"s","v":"s"},{"t":"s","v":"pages"},{"t":"s","v":"mark-processed!"},{"t":"s","v":"dom-text-content"},{"t":"s","v":"log-info"},{"t":"s","v":"str"},{"t":"s","v":"pages: script text length="},{"t":"s","v":"text"},{"t":"s","v":"len"},{"t":"n","v":0},{"t":"s","v":"empty?"},{"t":"s","v":"trim"},{"t":"s","v":"parse"},{"t":"s","v":"pages: parsed "},{"t":"s","v":" entries"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,20,3,0,1,4,0,20,5,0,20,3,0,1,7,0,52,6,0,2,48,1,65,1,0,52,2,0,2,49,2,50],"constants":[{"t":"s","v":"append!"},{"t":"s","v":"_page-routes"},{"t":"s","v":"merge"},{"t":"s","v":"page"},{"t":"s","v":"parsed"},{"t":"s","v":"parse-route-pattern"},{"t":"s","v":"get"},{"t":"s","v":"path"}]}},{"t":"s","v":"log-warn"},{"t":"s","v":"pages: script tag is empty"}]}},{"t":"s","v":"pages: "},{"t":"s","v":"_page-routes"},{"t":"s","v":" routes loaded"}]}},{"t":"s","v":"sx-hydrate-islands"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,6,34,6,0,5,20,2,0,48,0,1,3,0,48,2,17,1,20,4,0,1,6,0,20,8,0,52,7,0,1,1,9,0,20,1,0,33,6,0,1,10,0,32,3,0,1,11,0,52,5,0,4,48,1,5,51,13,0,20,8,0,52,12,0,2,50],"constants":[{"t":"s","v":"dom-query-all"},{"t":"s","v":"root"},{"t":"s","v":"dom-body"},{"t":"s","v":"[data-sx-island]"},{"t":"s","v":"log-info"},{"t":"s","v":"str"},{"t":"s","v":"sx-hydrate-islands: "},{"t":"s","v":"len"},{"t":"s","v":"els"},{"t":"s","v":" island(s) in "},{"t":"s","v":"subtree"},{"t":"s","v":"document"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,1,2,0,48,2,33,26,0,20,3,0,1,5,0,20,6,0,20,1,0,1,7,0,48,2,52,4,0,2,49,1,32,44,0,20,3,0,1,8,0,20,6,0,20,1,0,1,7,0,48,2,52,4,0,2,48,1,5,20,9,0,20,1,0,1,2,0,48,2,5,20,10,0,20,1,0,49,1,50],"constants":[{"t":"s","v":"is-processed?"},{"t":"s","v":"el"},{"t":"s","v":"island-hydrated"},{"t":"s","v":"log-info"},{"t":"s","v":"str"},{"t":"s","v":" skip (already hydrated): "},{"t":"s","v":"dom-get-attr"},{"t":"s","v":"data-sx-island"},{"t":"s","v":" hydrating: "},{"t":"s","v":"mark-processed!"},{"t":"s","v":"hydrate-island"}]}}]}},{"t":"s","v":"hydrate-island"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,1,2,0,48,2,17,1,20,0,0,20,1,0,1,3,0,48,2,6,34,4,0,5,1,4,0,17,2,1,6,0,20,7,0,52,5,0,2,17,3,20,8,0,2,48,1,17,4,20,9,0,20,10,0,20,11,0,48,2,17,5,20,14,0,52,13,0,1,6,34,8,0,5,20,14,0,52,15,0,1,52,12,0,1,33,18,0,20,16,0,1,17,0,20,11,0,52,5,0,2,49,1,32,149,0,20,19,0,20,20,0,48,1,52,18,0,1,6,34,4,0,5,65,0,0,17,6,52,21,0,0,17,7,20,22,0,20,14,0,52,23,0,1,20,10,0,48,2,17,8,51,25,0,20,14,0,52,26,0,1,52,24,0,2,5,20,27,0,51,28,0,51,29,0,48,2,17,9,20,30,0,20,1,0,1,31,0,48,2,5,20,32,0,20,1,0,20,33,0,48,2,5,20,34,0,20,1,0,1,35,0,20,36,0,48,3,5,20,37,0,20,1,0,48,1,5,20,38,0,1,39,0,20,11,0,1,40,0,20,36,0,52,41,0,1,1,42,0,52,5,0,5,49,1,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"el"},{"t":"s","v":"data-sx-island"},{"t":"s","v":"data-sx-state"},{"t":"s","v":"{}"},{"t":"s","v":"str"},{"t":"s","v":"~"},{"t":"s","v":"name"},{"t":"s","v":"get-render-env"},{"t":"s","v":"env-get"},{"t":"s","v":"env"},{"t":"s","v":"comp-name"},{"t":"s","v":"not"},{"t":"s","v":"component?"},{"t":"s","v":"comp"},{"t":"s","v":"island?"},{"t":"s","v":"log-warn"},{"t":"s","v":"hydrate-island: unknown island "},{"t":"s","v":"first"},{"t":"s","v":"sx-parse"},{"t":"s","v":"state-sx"},{"t":"s","v":"list"},{"t":"s","v":"env-merge"},{"t":"s","v":"component-closure"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,20,2,0,20,4,0,20,2,0,52,3,0,2,33,13,0,20,4,0,20,2,0,52,5,0,2,32,1,0,2,49,3,50],"constants":[{"t":"s","v":"env-bind!"},{"t":"s","v":"local"},{"t":"s","v":"p"},{"t":"s","v":"dict-has?"},{"t":"s","v":"kwargs"},{"t":"s","v":"dict-get"}]}},{"t":"s","v":"component-params"},{"t":"s","v":"cek-try"},{"t":"code","v":{"bytecode":[20,0,0,51,1,0,51,2,0,49,2,50],"constants":[{"t":"s","v":"with-island-scope"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,20,2,0,49,2,50],"constants":[{"t":"s","v":"append!"},{"t":"s","v":"disposers"},{"t":"s","v":"disposable"}]}},{"t":"code","v":{"bytecode":[20,0,0,20,2,0,52,1,0,1,20,3,0,2,49,3,50],"constants":[{"t":"s","v":"render-to-dom"},{"t":"s","v":"component-body"},{"t":"s","v":"comp"},{"t":"s","v":"local"}]}}]}},{"t":"code","v":{"bytecode":[20,0,0,1,2,0,20,3,0,1,4,0,20,5,0,52,1,0,4,48,1,5,20,6,0,1,7,0,2,48,2,17,1,20,8,0,20,9,0,1,10,0,1,11,0,48,3,5,20,8,0,20,9,0,1,12,0,1,13,0,48,3,5,20,14,0,20,9,0,1,15,0,20,3,0,1,16,0,20,5,0,52,1,0,4,48,2,5,20,9,0,50],"constants":[{"t":"s","v":"log-warn"},{"t":"s","v":"str"},{"t":"s","v":"hydrate-island FAILED: "},{"t":"s","v":"comp-name"},{"t":"s","v":" — "},{"t":"s","v":"err"},{"t":"s","v":"dom-create-element"},{"t":"s","v":"div"},{"t":"s","v":"dom-set-attr"},{"t":"s","v":"error-el"},{"t":"s","v":"class"},{"t":"s","v":"sx-island-error"},{"t":"s","v":"style"},{"t":"s","v":"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"},{"t":"s","v":"dom-set-text-content"},{"t":"s","v":"Island error: "},{"t":"s","v":"\n"}]}},{"t":"s","v":"dom-set-text-content"},{"t":"s","v":""},{"t":"s","v":"dom-append"},{"t":"s","v":"body-dom"},{"t":"s","v":"dom-set-data"},{"t":"s","v":"sx-disposers"},{"t":"s","v":"disposers"},{"t":"s","v":"process-elements"},{"t":"s","v":"log-info"},{"t":"s","v":"hydrated island: "},{"t":"s","v":" ("},{"t":"s","v":"len"},{"t":"s","v":" disposers)"}]}},{"t":"s","v":"dispose-island"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,1,2,0,48,2,17,1,20,3,0,33,26,0,51,5,0,20,3,0,52,4,0,2,5,20,6,0,20,1,0,1,2,0,2,48,3,32,1,0,2,5,20,7,0,20,1,0,1,8,0,49,2,50],"constants":[{"t":"s","v":"dom-get-data"},{"t":"s","v":"el"},{"t":"s","v":"sx-disposers"},{"t":"s","v":"disposers"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,48,1,33,8,0,20,1,0,49,0,32,1,0,2,50],"constants":[{"t":"s","v":"callable?"},{"t":"s","v":"d"}]}},{"t":"s","v":"dom-set-data"},{"t":"s","v":"clear-processed!"},{"t":"s","v":"island-hydrated"}]}},{"t":"s","v":"dispose-islands-in"},{"t":"code","v":{"bytecode":[20,0,0,33,105,0,20,1,0,20,0,0,1,2,0,48,2,17,1,20,3,0,6,33,12,0,5,20,3,0,52,5,0,1,52,4,0,1,33,66,0,51,7,0,20,3,0,52,6,0,2,17,2,20,8,0,52,5,0,1,52,4,0,1,33,36,0,20,9,0,1,11,0,20,8,0,52,12,0,1,1,13,0,52,10,0,3,48,1,5,20,15,0,20,8,0,52,14,0,2,32,1,0,2,32,1,0,2,32,1,0,2,50],"constants":[{"t":"s","v":"root"},{"t":"s","v":"dom-query-all"},{"t":"s","v":"[data-sx-island]"},{"t":"s","v":"islands"},{"t":"s","v":"not"},{"t":"s","v":"empty?"},{"t":"s","v":"filter"},{"t":"code","v":{"bytecode":[20,1,0,20,2,0,1,3,0,48,2,52,0,0,1,50],"constants":[{"t":"s","v":"not"},{"t":"s","v":"is-processed?"},{"t":"s","v":"el"},{"t":"s","v":"island-hydrated"}]}},{"t":"s","v":"to-dispose"},{"t":"s","v":"log-info"},{"t":"s","v":"str"},{"t":"s","v":"disposing "},{"t":"s","v":"len"},{"t":"s","v":" island(s)"},{"t":"s","v":"for-each"},{"t":"s","v":"dispose-island"}]}},{"t":"s","v":"force-dispose-islands-in"},{"t":"code","v":{"bytecode":[20,0,0,33,75,0,20,1,0,20,0,0,1,2,0,48,2,17,1,20,3,0,6,33,12,0,5,20,3,0,52,5,0,1,52,4,0,1,33,36,0,20,6,0,1,8,0,20,3,0,52,9,0,1,1,10,0,52,7,0,3,48,1,5,20,12,0,20,3,0,52,11,0,2,32,1,0,2,32,1,0,2,50],"constants":[{"t":"s","v":"root"},{"t":"s","v":"dom-query-all"},{"t":"s","v":"[data-sx-island]"},{"t":"s","v":"islands"},{"t":"s","v":"not"},{"t":"s","v":"empty?"},{"t":"s","v":"log-info"},{"t":"s","v":"str"},{"t":"s","v":"force-disposing "},{"t":"s","v":"len"},{"t":"s","v":" island(s)"},{"t":"s","v":"for-each"},{"t":"s","v":"dispose-island"}]}},{"t":"s","v":"*pre-render-hooks*"},{"t":"s","v":"*post-render-hooks*"},{"t":"s","v":"register-pre-render-hook"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,20,2,0,49,2,50],"constants":[{"t":"s","v":"append!"},{"t":"s","v":"*pre-render-hooks*"},{"t":"s","v":"hook-fn"}]}},{"t":"s","v":"register-post-render-hook"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,20,2,0,49,2,50],"constants":[{"t":"s","v":"append!"},{"t":"s","v":"*post-render-hooks*"},{"t":"s","v":"hook-fn"}]}},{"t":"s","v":"run-pre-render-hooks"},{"t":"code","v":{"bytecode":[51,1,0,20,2,0,52,0,0,2,50],"constants":[{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,2,49,2,50],"constants":[{"t":"s","v":"cek-call"},{"t":"s","v":"hook"}]}},{"t":"s","v":"*pre-render-hooks*"}]}},{"t":"s","v":"run-post-render-hooks"},{"t":"code","v":{"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":[{"t":"s","v":"log-info"},{"t":"s","v":"str"},{"t":"s","v":"run-post-render-hooks: "},{"t":"s","v":"len"},{"t":"s","v":"*post-render-hooks*"},{"t":"s","v":" hooks"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,1,2,0,20,4,0,52,3,0,1,1,5,0,20,6,0,20,4,0,48,1,1,7,0,20,4,0,52,8,0,1,52,1,0,6,48,1,5,20,9,0,20,4,0,2,49,2,50],"constants":[{"t":"s","v":"log-info"},{"t":"s","v":"str"},{"t":"s","v":" hook type: "},{"t":"s","v":"type-of"},{"t":"s","v":"hook"},{"t":"s","v":" callable: "},{"t":"s","v":"callable?"},{"t":"s","v":" lambda: "},{"t":"s","v":"lambda?"},{"t":"s","v":"cek-call"}]}}]}},{"t":"s","v":"boot-init"},{"t":"code","v":{"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":[{"t":"s","v":"log-info"},{"t":"s","v":"str"},{"t":"s","v":"sx-browser "},{"t":"s","v":"SX_VERSION"},{"t":"s","v":"init-css-tracking"},{"t":"s","v":"process-page-scripts"},{"t":"s","v":"process-sx-scripts"},{"t":"s","v":"sx-hydrate-elements"},{"t":"s","v":"sx-hydrate-islands"},{"t":"s","v":"run-post-render-hooks"},{"t":"s","v":"process-elements"},{"t":"s","v":"dom-listen"},{"t":"s","v":"dom-window"},{"t":"s","v":"popstate"},{"t":"code","v":{"bytecode":[20,0,0,1,1,0,49,1,50],"constants":[{"t":"s","v":"handle-popstate"},{"t":"n","v":0}]}}]}}]}} \ No newline at end of file +{"magic":"SXBC","version":1,"hash":"bc78815bb4f83787","module":{"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":[{"t":"s","v":"HEAD_HOIST_SELECTOR"},{"t":"s","v":"meta, title, link[rel='canonical'], script[type='application/ld+json']"},{"t":"s","v":"hoist-head-elements-full"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,20,2,0,48,2,17,1,51,4,0,20,5,0,52,3,0,2,50],"constants":[{"t":"s","v":"dom-query-all"},{"t":"s","v":"root"},{"t":"s","v":"HEAD_HOIST_SELECTOR"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,1,0,20,2,0,48,1,52,0,0,1,17,1,20,4,0,1,5,0,52,3,0,2,33,33,0,20,6,0,20,7,0,20,2,0,48,1,48,1,5,20,8,0,20,9,0,20,2,0,48,1,20,2,0,49,2,32,223,0,20,4,0,1,10,0,52,3,0,2,33,112,0,20,11,0,20,2,0,1,12,0,48,2,17,2,20,11,0,20,2,0,1,13,0,48,2,17,3,20,12,0,33,21,0,20,14,0,1,16,0,20,12,0,1,17,0,52,15,0,3,48,1,32,1,0,2,5,20,18,0,33,21,0,20,14,0,1,19,0,20,18,0,1,17,0,52,15,0,3,48,1,32,1,0,2,5,20,8,0,20,9,0,20,2,0,48,1,20,2,0,48,2,5,20,20,0,20,2,0,49,1,32,98,0,20,4,0,1,21,0,52,3,0,2,6,33,19,0,5,20,11,0,20,2,0,1,22,0,48,2,1,23,0,52,3,0,2,33,37,0,20,14,0,1,24,0,48,1,5,20,8,0,20,9,0,20,2,0,48,1,20,2,0,48,2,5,20,20,0,20,2,0,49,1,32,25,0,20,8,0,20,9,0,20,2,0,48,1,20,2,0,48,2,5,20,20,0,20,2,0,49,1,50],"constants":[{"t":"s","v":"lower"},{"t":"s","v":"dom-tag-name"},{"t":"s","v":"el"},{"t":"s","v":"="},{"t":"s","v":"tag"},{"t":"s","v":"title"},{"t":"s","v":"set-document-title"},{"t":"s","v":"dom-text-content"},{"t":"s","v":"dom-remove-child"},{"t":"s","v":"dom-parent"},{"t":"s","v":"meta"},{"t":"s","v":"dom-get-attr"},{"t":"s","v":"name"},{"t":"s","v":"property"},{"t":"s","v":"remove-head-element"},{"t":"s","v":"str"},{"t":"s","v":"meta[name=\""},{"t":"s","v":"\"]"},{"t":"s","v":"prop"},{"t":"s","v":"meta[property=\""},{"t":"s","v":"dom-append-to-head"},{"t":"s","v":"link"},{"t":"s","v":"rel"},{"t":"s","v":"canonical"},{"t":"s","v":"link[rel=\"canonical\"]"}]}},{"t":"s","v":"els"}]}},{"t":"s","v":"sx-mount"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,48,1,17,3,20,2,0,33,100,0,20,4,0,20,2,0,48,1,52,3,0,1,33,48,0,20,5,0,20,6,0,20,7,0,48,2,17,4,20,8,0,20,2,0,1,9,0,48,2,5,20,10,0,20,2,0,20,11,0,48,2,5,20,12,0,20,2,0,48,1,32,1,0,2,5,20,13,0,20,2,0,48,1,5,20,14,0,20,2,0,48,1,5,20,15,0,20,2,0,48,1,5,20,16,0,49,0,32,1,0,2,50],"constants":[{"t":"s","v":"resolve-mount-target"},{"t":"s","v":"target"},{"t":"s","v":"el"},{"t":"s","v":"empty?"},{"t":"s","v":"dom-child-list"},{"t":"s","v":"sx-render-with-env"},{"t":"s","v":"source"},{"t":"s","v":"extra-env"},{"t":"s","v":"dom-set-text-content"},{"t":"s","v":""},{"t":"s","v":"dom-append"},{"t":"s","v":"node"},{"t":"s","v":"hoist-head-elements-full"},{"t":"s","v":"process-elements"},{"t":"s","v":"sx-hydrate-elements"},{"t":"s","v":"sx-hydrate-islands"},{"t":"s","v":"run-post-render-hooks"}]}},{"t":"s","v":"resolve-suspense"},{"t":"code","v":{"bytecode":[20,0,0,2,48,1,5,20,1,0,1,3,0,20,4,0,1,5,0,52,2,0,3,48,1,17,2,20,6,0,33,97,0,20,7,0,20,8,0,48,1,17,3,20,9,0,2,48,1,17,4,20,10,0,20,6,0,1,11,0,48,2,5,51,13,0,20,14,0,52,12,0,2,5,20,15,0,20,6,0,48,1,5,20,16,0,20,6,0,48,1,5,20,17,0,20,6,0,48,1,5,20,18,0,48,0,5,20,19,0,20,6,0,1,20,0,1,4,0,20,4,0,65,1,0,49,3,32,15,0,20,21,0,1,22,0,20,4,0,52,2,0,2,49,1,50],"constants":[{"t":"s","v":"process-sx-scripts"},{"t":"s","v":"dom-query"},{"t":"s","v":"str"},{"t":"s","v":"[data-suspense=\""},{"t":"s","v":"id"},{"t":"s","v":"\"]"},{"t":"s","v":"el"},{"t":"s","v":"parse"},{"t":"s","v":"sx"},{"t":"s","v":"get-render-env"},{"t":"s","v":"dom-set-text-content"},{"t":"s","v":""},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,20,2,0,20,3,0,20,4,0,2,48,3,49,2,50],"constants":[{"t":"s","v":"dom-append"},{"t":"s","v":"el"},{"t":"s","v":"render-to-dom"},{"t":"s","v":"expr"},{"t":"s","v":"env"}]}},{"t":"s","v":"exprs"},{"t":"s","v":"process-elements"},{"t":"s","v":"sx-hydrate-elements"},{"t":"s","v":"sx-hydrate-islands"},{"t":"s","v":"run-post-render-hooks"},{"t":"s","v":"dom-dispatch"},{"t":"s","v":"sx:resolved"},{"t":"s","v":"log-warn"},{"t":"s","v":"resolveSuspense: no element for id="}]}},{"t":"s","v":"sx-hydrate-elements"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,6,34,6,0,5,20,2,0,48,0,1,3,0,48,2,17,1,51,5,0,20,6,0,52,4,0,2,50],"constants":[{"t":"s","v":"dom-query-all"},{"t":"s","v":"root"},{"t":"s","v":"dom-body"},{"t":"s","v":"[data-sx]"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,1,0,20,2,0,1,3,0,48,2,52,0,0,1,33,24,0,20,4,0,20,2,0,1,3,0,48,2,5,20,5,0,20,2,0,2,49,2,32,1,0,2,50],"constants":[{"t":"s","v":"not"},{"t":"s","v":"is-processed?"},{"t":"s","v":"el"},{"t":"s","v":"hydrated"},{"t":"s","v":"mark-processed!"},{"t":"s","v":"sx-update-element"}]}},{"t":"s","v":"els"}]}},{"t":"s","v":"sx-update-element"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,48,1,17,2,20,2,0,33,110,0,20,3,0,20,2,0,1,4,0,48,2,17,3,20,5,0,33,87,0,20,6,0,20,2,0,48,1,17,4,20,7,0,20,8,0,20,9,0,48,2,17,5,20,10,0,20,5,0,20,11,0,48,2,17,6,20,12,0,20,2,0,1,13,0,48,2,5,20,14,0,20,2,0,20,15,0,48,2,5,20,9,0,33,17,0,20,16,0,20,2,0,20,8,0,20,9,0,49,3,32,1,0,2,32,1,0,2,32,1,0,2,50],"constants":[{"t":"s","v":"resolve-mount-target"},{"t":"s","v":"el"},{"t":"s","v":"target"},{"t":"s","v":"dom-get-attr"},{"t":"s","v":"data-sx"},{"t":"s","v":"source"},{"t":"s","v":"parse-env-attr"},{"t":"s","v":"merge-envs"},{"t":"s","v":"base-env"},{"t":"s","v":"new-env"},{"t":"s","v":"sx-render-with-env"},{"t":"s","v":"env"},{"t":"s","v":"dom-set-text-content"},{"t":"s","v":""},{"t":"s","v":"dom-append"},{"t":"s","v":"node"},{"t":"s","v":"store-env-attr"}]}},{"t":"s","v":"sx-render-component"},{"t":"code","v":{"bytecode":[20,1,0,1,2,0,52,0,0,2,33,6,0,20,1,0,32,10,0,1,2,0,20,1,0,52,3,0,2,17,3,20,4,0,20,5,0,48,1,17,4,20,6,0,20,7,0,20,8,0,48,2,17,5,20,11,0,52,10,0,1,52,9,0,1,33,17,0,1,13,0,20,8,0,52,3,0,2,52,12,0,1,32,40,0,20,8,0,52,15,0,1,52,14,0,1,17,6,51,17,0,20,19,0,52,18,0,1,52,16,0,2,5,20,20,0,20,21,0,20,7,0,2,49,3,50],"constants":[{"t":"s","v":"starts-with?"},{"t":"s","v":"name"},{"t":"s","v":"~"},{"t":"s","v":"str"},{"t":"s","v":"get-render-env"},{"t":"s","v":"extra-env"},{"t":"s","v":"env-get"},{"t":"s","v":"env"},{"t":"s","v":"full-name"},{"t":"s","v":"not"},{"t":"s","v":"component?"},{"t":"s","v":"comp"},{"t":"s","v":"error"},{"t":"s","v":"Unknown component: "},{"t":"s","v":"list"},{"t":"s","v":"make-symbol"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,20,2,0,20,3,0,20,4,0,48,1,48,1,48,2,5,20,0,0,20,1,0,20,6,0,20,4,0,52,5,0,2,49,2,50],"constants":[{"t":"s","v":"append!"},{"t":"s","v":"call-expr"},{"t":"s","v":"make-keyword"},{"t":"s","v":"to-kebab"},{"t":"s","v":"k"},{"t":"s","v":"dict-get"},{"t":"s","v":"kwargs"}]}},{"t":"s","v":"keys"},{"t":"s","v":"kwargs"},{"t":"s","v":"render-to-dom"},{"t":"s","v":"call-expr"}]}},{"t":"s","v":"process-sx-scripts"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,48,1,17,1,51,3,0,20,4,0,52,2,0,2,50],"constants":[{"t":"s","v":"query-sx-scripts"},{"t":"s","v":"root"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,1,0,20,2,0,1,3,0,48,2,52,0,0,1,33,190,0,20,4,0,20,2,0,1,3,0,48,2,5,20,5,0,20,2,0,48,1,17,1,20,6,0,20,2,0,1,7,0,48,2,33,14,0,20,8,0,20,2,0,20,9,0,49,2,32,137,0,20,9,0,52,10,0,1,6,34,12,0,5,20,9,0,52,12,0,1,52,11,0,1,33,4,0,2,32,107,0,20,6,0,20,2,0,1,13,0,48,2,33,23,0,20,14,0,20,9,0,48,1,17,2,51,16,0,20,17,0,52,15,0,2,32,70,0,20,6,0,20,2,0,1,18,0,48,2,33,48,0,20,19,0,20,2,0,1,18,0,48,2,17,2,20,20,0,20,21,0,48,1,17,3,20,22,0,33,15,0,20,23,0,20,22,0,20,9,0,2,49,3,32,1,0,2,32,8,0,20,24,0,20,9,0,49,1,32,1,0,2,50],"constants":[{"t":"s","v":"not"},{"t":"s","v":"is-processed?"},{"t":"s","v":"s"},{"t":"s","v":"script"},{"t":"s","v":"mark-processed!"},{"t":"s","v":"dom-text-content"},{"t":"s","v":"dom-has-attr?"},{"t":"s","v":"data-components"},{"t":"s","v":"process-component-script"},{"t":"s","v":"text"},{"t":"s","v":"nil?"},{"t":"s","v":"empty?"},{"t":"s","v":"trim"},{"t":"s","v":"data-init"},{"t":"s","v":"sx-parse"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,49,1,50],"constants":[{"t":"s","v":"cek-eval"},{"t":"s","v":"expr"}]}},{"t":"s","v":"exprs"},{"t":"s","v":"data-mount"},{"t":"s","v":"dom-get-attr"},{"t":"s","v":"dom-query"},{"t":"s","v":"mount-sel"},{"t":"s","v":"target"},{"t":"s","v":"sx-mount"},{"t":"s","v":"sx-load-components"}]}},{"t":"s","v":"scripts"}]}},{"t":"s","v":"process-component-script"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,1,2,0,48,2,17,2,20,4,0,52,3,0,1,33,41,0,20,5,0,6,33,16,0,5,20,5,0,52,8,0,1,52,7,0,1,52,6,0,1,33,11,0,20,9,0,20,5,0,49,1,32,1,0,2,32,0,1,20,5,0,6,33,16,0,5,20,5,0,52,8,0,1,52,7,0,1,52,6,0,1,17,3,20,10,0,1,11,0,48,1,17,4,20,13,0,20,4,0,52,12,0,2,33,110,0,20,14,0,33,44,0,20,15,0,1,11,0,20,4,0,48,2,5,20,15,0,1,16,0,20,5,0,48,2,5,20,9,0,20,5,0,48,1,5,20,17,0,1,18,0,48,1,32,57,0,20,10,0,1,16,0,48,1,17,5,20,19,0,33,30,0,20,9,0,20,19,0,48,1,5,20,17,0,1,21,0,20,4,0,1,22,0,52,20,0,3,48,1,32,11,0,20,23,0,48,0,5,20,24,0,48,0,32,89,0,20,14,0,33,54,0,20,15,0,1,11,0,20,4,0,48,2,5,20,15,0,1,16,0,20,5,0,48,2,5,20,9,0,20,5,0,48,1,5,20,17,0,1,25,0,20,4,0,1,22,0,52,20,0,3,48,1,32,29,0,20,26,0,1,11,0,48,1,5,20,26,0,1,16,0,48,1,5,20,23,0,48,0,5,20,24,0,48,0,5,20,27,0,20,4,0,49,1,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"script"},{"t":"s","v":"data-hash"},{"t":"s","v":"nil?"},{"t":"s","v":"hash"},{"t":"s","v":"text"},{"t":"s","v":"not"},{"t":"s","v":"empty?"},{"t":"s","v":"trim"},{"t":"s","v":"sx-load-components"},{"t":"s","v":"local-storage-get"},{"t":"s","v":"sx-components-hash"},{"t":"s","v":"="},{"t":"s","v":"cached-hash"},{"t":"s","v":"has-inline"},{"t":"s","v":"local-storage-set"},{"t":"s","v":"sx-components-src"},{"t":"s","v":"log-info"},{"t":"s","v":"components: downloaded (cookie stale)"},{"t":"s","v":"cached"},{"t":"s","v":"str"},{"t":"s","v":"components: cached ("},{"t":"s","v":")"},{"t":"s","v":"clear-sx-comp-cookie"},{"t":"s","v":"browser-reload"},{"t":"s","v":"components: downloaded ("},{"t":"s","v":"local-storage-remove"},{"t":"s","v":"set-sx-comp-cookie"}]}},{"t":"s","v":"_page-routes"},{"t":"s","v":"list"},{"t":"s","v":"process-page-scripts"},{"t":"code","v":{"bytecode":[20,0,0,48,0,17,0,20,1,0,1,3,0,20,5,0,52,4,0,1,1,6,0,52,2,0,3,48,1,5,51,8,0,20,5,0,52,7,0,2,5,20,1,0,1,9,0,20,10,0,52,4,0,1,1,11,0,52,2,0,3,49,1,50],"constants":[{"t":"s","v":"query-page-scripts"},{"t":"s","v":"log-info"},{"t":"s","v":"str"},{"t":"s","v":"pages: found "},{"t":"s","v":"len"},{"t":"s","v":"scripts"},{"t":"s","v":" script tags"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,1,0,20,2,0,1,3,0,48,2,52,0,0,1,33,137,0,20,4,0,20,2,0,1,3,0,48,2,5,20,5,0,20,2,0,48,1,17,1,20,6,0,1,8,0,20,9,0,33,10,0,20,9,0,52,10,0,1,32,3,0,1,11,0,52,7,0,2,48,1,5,20,9,0,6,33,16,0,5,20,9,0,52,13,0,1,52,12,0,1,52,0,0,1,33,46,0,20,14,0,20,9,0,48,1,17,2,20,6,0,1,15,0,20,3,0,52,10,0,1,1,16,0,52,7,0,3,48,1,5,51,18,0,20,3,0,52,17,0,2,32,8,0,20,19,0,1,20,0,49,1,32,1,0,2,50],"constants":[{"t":"s","v":"not"},{"t":"s","v":"is-processed?"},{"t":"s","v":"s"},{"t":"s","v":"pages"},{"t":"s","v":"mark-processed!"},{"t":"s","v":"dom-text-content"},{"t":"s","v":"log-info"},{"t":"s","v":"str"},{"t":"s","v":"pages: script text length="},{"t":"s","v":"text"},{"t":"s","v":"len"},{"t":"n","v":0},{"t":"s","v":"empty?"},{"t":"s","v":"trim"},{"t":"s","v":"parse"},{"t":"s","v":"pages: parsed "},{"t":"s","v":" entries"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,20,3,0,1,4,0,20,5,0,20,3,0,1,7,0,52,6,0,2,48,1,65,1,0,52,2,0,2,49,2,50],"constants":[{"t":"s","v":"append!"},{"t":"s","v":"_page-routes"},{"t":"s","v":"merge"},{"t":"s","v":"page"},{"t":"s","v":"parsed"},{"t":"s","v":"parse-route-pattern"},{"t":"s","v":"get"},{"t":"s","v":"path"}]}},{"t":"s","v":"log-warn"},{"t":"s","v":"pages: script tag is empty"}]}},{"t":"s","v":"pages: "},{"t":"s","v":"_page-routes"},{"t":"s","v":" routes loaded"}]}},{"t":"s","v":"sx-hydrate-islands"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,6,34,6,0,5,20,2,0,48,0,1,3,0,48,2,17,1,20,4,0,1,6,0,20,8,0,52,7,0,1,1,9,0,20,1,0,33,6,0,1,10,0,32,3,0,1,11,0,52,5,0,4,48,1,5,51,13,0,20,8,0,52,12,0,2,50],"constants":[{"t":"s","v":"dom-query-all"},{"t":"s","v":"root"},{"t":"s","v":"dom-body"},{"t":"s","v":"[data-sx-island]"},{"t":"s","v":"log-info"},{"t":"s","v":"str"},{"t":"s","v":"sx-hydrate-islands: "},{"t":"s","v":"len"},{"t":"s","v":"els"},{"t":"s","v":" island(s) in "},{"t":"s","v":"subtree"},{"t":"s","v":"document"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,1,2,0,48,2,33,26,0,20,3,0,1,5,0,20,6,0,20,1,0,1,7,0,48,2,52,4,0,2,49,1,32,44,0,20,3,0,1,8,0,20,6,0,20,1,0,1,7,0,48,2,52,4,0,2,48,1,5,20,9,0,20,1,0,1,2,0,48,2,5,20,10,0,20,1,0,49,1,50],"constants":[{"t":"s","v":"is-processed?"},{"t":"s","v":"el"},{"t":"s","v":"island-hydrated"},{"t":"s","v":"log-info"},{"t":"s","v":"str"},{"t":"s","v":" skip (already hydrated): "},{"t":"s","v":"dom-get-attr"},{"t":"s","v":"data-sx-island"},{"t":"s","v":" hydrating: "},{"t":"s","v":"mark-processed!"},{"t":"s","v":"hydrate-island"}]}}]}},{"t":"s","v":"hydrate-island"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,1,2,0,48,2,17,1,20,0,0,20,1,0,1,3,0,48,2,6,34,4,0,5,1,4,0,17,2,1,6,0,20,7,0,52,5,0,2,17,3,20,8,0,2,48,1,17,4,20,9,0,20,10,0,20,11,0,48,2,17,5,20,14,0,52,13,0,1,6,34,8,0,5,20,14,0,52,15,0,1,52,12,0,1,33,18,0,20,16,0,1,17,0,20,11,0,52,5,0,2,49,1,32,149,0,20,19,0,20,20,0,48,1,52,18,0,1,6,34,4,0,5,65,0,0,17,6,52,21,0,0,17,7,20,22,0,20,14,0,52,23,0,1,20,10,0,48,2,17,8,51,25,0,20,14,0,52,26,0,1,52,24,0,2,5,20,27,0,51,28,0,51,29,0,48,2,17,9,20,30,0,20,1,0,1,31,0,48,2,5,20,32,0,20,1,0,20,33,0,48,2,5,20,34,0,20,1,0,1,35,0,20,36,0,48,3,5,20,37,0,20,1,0,48,1,5,20,38,0,1,39,0,20,11,0,1,40,0,20,36,0,52,41,0,1,1,42,0,52,5,0,5,49,1,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"el"},{"t":"s","v":"data-sx-island"},{"t":"s","v":"data-sx-state"},{"t":"s","v":"{}"},{"t":"s","v":"str"},{"t":"s","v":"~"},{"t":"s","v":"name"},{"t":"s","v":"get-render-env"},{"t":"s","v":"env-get"},{"t":"s","v":"env"},{"t":"s","v":"comp-name"},{"t":"s","v":"not"},{"t":"s","v":"component?"},{"t":"s","v":"comp"},{"t":"s","v":"island?"},{"t":"s","v":"log-warn"},{"t":"s","v":"hydrate-island: unknown island "},{"t":"s","v":"first"},{"t":"s","v":"sx-parse"},{"t":"s","v":"state-sx"},{"t":"s","v":"list"},{"t":"s","v":"env-merge"},{"t":"s","v":"component-closure"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,20,2,0,20,4,0,20,2,0,52,3,0,2,33,13,0,20,4,0,20,2,0,52,5,0,2,32,1,0,2,49,3,50],"constants":[{"t":"s","v":"env-bind!"},{"t":"s","v":"local"},{"t":"s","v":"p"},{"t":"s","v":"dict-has?"},{"t":"s","v":"kwargs"},{"t":"s","v":"dict-get"}]}},{"t":"s","v":"component-params"},{"t":"s","v":"cek-try"},{"t":"code","v":{"bytecode":[20,0,0,51,1,0,51,2,0,49,2,50],"constants":[{"t":"s","v":"with-island-scope"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,20,2,0,49,2,50],"constants":[{"t":"s","v":"append!"},{"t":"s","v":"disposers"},{"t":"s","v":"disposable"}]}},{"t":"code","v":{"bytecode":[20,0,0,20,2,0,52,1,0,1,20,3,0,2,49,3,50],"constants":[{"t":"s","v":"render-to-dom"},{"t":"s","v":"component-body"},{"t":"s","v":"comp"},{"t":"s","v":"local"}]}}]}},{"t":"code","v":{"bytecode":[20,0,0,1,2,0,20,3,0,1,4,0,20,5,0,52,1,0,4,48,1,5,20,6,0,1,7,0,2,48,2,17,1,20,8,0,20,9,0,1,10,0,1,11,0,48,3,5,20,8,0,20,9,0,1,12,0,1,13,0,48,3,5,20,14,0,20,9,0,1,15,0,20,3,0,1,16,0,20,5,0,52,1,0,4,48,2,5,20,9,0,50],"constants":[{"t":"s","v":"log-warn"},{"t":"s","v":"str"},{"t":"s","v":"hydrate-island FAILED: "},{"t":"s","v":"comp-name"},{"t":"s","v":" — "},{"t":"s","v":"err"},{"t":"s","v":"dom-create-element"},{"t":"s","v":"div"},{"t":"s","v":"dom-set-attr"},{"t":"s","v":"error-el"},{"t":"s","v":"class"},{"t":"s","v":"sx-island-error"},{"t":"s","v":"style"},{"t":"s","v":"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"},{"t":"s","v":"dom-set-text-content"},{"t":"s","v":"Island error: "},{"t":"s","v":"\n"}]}},{"t":"s","v":"dom-set-text-content"},{"t":"s","v":""},{"t":"s","v":"dom-append"},{"t":"s","v":"body-dom"},{"t":"s","v":"dom-set-data"},{"t":"s","v":"sx-disposers"},{"t":"s","v":"disposers"},{"t":"s","v":"process-elements"},{"t":"s","v":"log-info"},{"t":"s","v":"hydrated island: "},{"t":"s","v":" ("},{"t":"s","v":"len"},{"t":"s","v":" disposers)"}]}},{"t":"s","v":"dispose-island"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,1,2,0,48,2,17,1,20,3,0,33,26,0,51,5,0,20,3,0,52,4,0,2,5,20,6,0,20,1,0,1,2,0,2,48,3,32,1,0,2,5,20,7,0,20,1,0,1,8,0,49,2,50],"constants":[{"t":"s","v":"dom-get-data"},{"t":"s","v":"el"},{"t":"s","v":"sx-disposers"},{"t":"s","v":"disposers"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,48,1,33,8,0,20,1,0,49,0,32,1,0,2,50],"constants":[{"t":"s","v":"callable?"},{"t":"s","v":"d"}]}},{"t":"s","v":"dom-set-data"},{"t":"s","v":"clear-processed!"},{"t":"s","v":"island-hydrated"}]}},{"t":"s","v":"dispose-islands-in"},{"t":"code","v":{"bytecode":[20,0,0,33,105,0,20,1,0,20,0,0,1,2,0,48,2,17,1,20,3,0,6,33,12,0,5,20,3,0,52,5,0,1,52,4,0,1,33,66,0,51,7,0,20,3,0,52,6,0,2,17,2,20,8,0,52,5,0,1,52,4,0,1,33,36,0,20,9,0,1,11,0,20,8,0,52,12,0,1,1,13,0,52,10,0,3,48,1,5,20,15,0,20,8,0,52,14,0,2,32,1,0,2,32,1,0,2,32,1,0,2,50],"constants":[{"t":"s","v":"root"},{"t":"s","v":"dom-query-all"},{"t":"s","v":"[data-sx-island]"},{"t":"s","v":"islands"},{"t":"s","v":"not"},{"t":"s","v":"empty?"},{"t":"s","v":"filter"},{"t":"code","v":{"bytecode":[20,1,0,20,2,0,1,3,0,48,2,52,0,0,1,50],"constants":[{"t":"s","v":"not"},{"t":"s","v":"is-processed?"},{"t":"s","v":"el"},{"t":"s","v":"island-hydrated"}]}},{"t":"s","v":"to-dispose"},{"t":"s","v":"log-info"},{"t":"s","v":"str"},{"t":"s","v":"disposing "},{"t":"s","v":"len"},{"t":"s","v":" island(s)"},{"t":"s","v":"for-each"},{"t":"s","v":"dispose-island"}]}},{"t":"s","v":"force-dispose-islands-in"},{"t":"code","v":{"bytecode":[20,0,0,33,75,0,20,1,0,20,0,0,1,2,0,48,2,17,1,20,3,0,6,33,12,0,5,20,3,0,52,5,0,1,52,4,0,1,33,36,0,20,6,0,1,8,0,20,3,0,52,9,0,1,1,10,0,52,7,0,3,48,1,5,20,12,0,20,3,0,52,11,0,2,32,1,0,2,32,1,0,2,50],"constants":[{"t":"s","v":"root"},{"t":"s","v":"dom-query-all"},{"t":"s","v":"[data-sx-island]"},{"t":"s","v":"islands"},{"t":"s","v":"not"},{"t":"s","v":"empty?"},{"t":"s","v":"log-info"},{"t":"s","v":"str"},{"t":"s","v":"force-disposing "},{"t":"s","v":"len"},{"t":"s","v":" island(s)"},{"t":"s","v":"for-each"},{"t":"s","v":"dispose-island"}]}},{"t":"s","v":"*pre-render-hooks*"},{"t":"s","v":"*post-render-hooks*"},{"t":"s","v":"register-pre-render-hook"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,20,2,0,49,2,50],"constants":[{"t":"s","v":"append!"},{"t":"s","v":"*pre-render-hooks*"},{"t":"s","v":"hook-fn"}]}},{"t":"s","v":"register-post-render-hook"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,20,2,0,49,2,50],"constants":[{"t":"s","v":"append!"},{"t":"s","v":"*post-render-hooks*"},{"t":"s","v":"hook-fn"}]}},{"t":"s","v":"run-pre-render-hooks"},{"t":"code","v":{"bytecode":[51,1,0,20,2,0,52,0,0,2,50],"constants":[{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,2,49,2,50],"constants":[{"t":"s","v":"cek-call"},{"t":"s","v":"hook"}]}},{"t":"s","v":"*pre-render-hooks*"}]}},{"t":"s","v":"run-post-render-hooks"},{"t":"code","v":{"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":[{"t":"s","v":"log-info"},{"t":"s","v":"str"},{"t":"s","v":"run-post-render-hooks: "},{"t":"s","v":"len"},{"t":"s","v":"*post-render-hooks*"},{"t":"s","v":" hooks"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,1,2,0,20,4,0,52,3,0,1,1,5,0,20,6,0,20,4,0,48,1,1,7,0,20,4,0,52,8,0,1,52,1,0,6,48,1,5,20,9,0,20,4,0,2,49,2,50],"constants":[{"t":"s","v":"log-info"},{"t":"s","v":"str"},{"t":"s","v":" hook type: "},{"t":"s","v":"type-of"},{"t":"s","v":"hook"},{"t":"s","v":" callable: "},{"t":"s","v":"callable?"},{"t":"s","v":" lambda: "},{"t":"s","v":"lambda?"},{"t":"s","v":"cek-call"}]}}]}},{"t":"s","v":"boot-init"},{"t":"code","v":{"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,48,3,5,20,15,0,20,16,0,20,17,0,48,0,1,18,0,48,2,1,19,0,1,20,0,48,3,5,20,21,0,20,17,0,48,0,1,22,0,2,48,3,5,20,0,0,1,22,0,49,1,50],"constants":[{"t":"s","v":"log-info"},{"t":"s","v":"str"},{"t":"s","v":"sx-browser "},{"t":"s","v":"SX_VERSION"},{"t":"s","v":"init-css-tracking"},{"t":"s","v":"process-page-scripts"},{"t":"s","v":"process-sx-scripts"},{"t":"s","v":"sx-hydrate-elements"},{"t":"s","v":"sx-hydrate-islands"},{"t":"s","v":"run-post-render-hooks"},{"t":"s","v":"process-elements"},{"t":"s","v":"dom-listen"},{"t":"s","v":"dom-window"},{"t":"s","v":"popstate"},{"t":"code","v":{"bytecode":[20,0,0,1,1,0,49,1,50],"constants":[{"t":"s","v":"handle-popstate"},{"t":"n","v":0}]}},{"t":"s","v":"dom-set-attr"},{"t":"s","v":"host-get"},{"t":"s","v":"dom-document"},{"t":"s","v":"documentElement"},{"t":"s","v":"data-sx-ready"},{"t":"s","v":"true"},{"t":"s","v":"dom-dispatch"},{"t":"s","v":"sx:ready"}]}}]}} \ No newline at end of file diff --git a/sx/tests/test_demos.py b/sx/tests/test_demos.py index 0a7c7d4d..e04c3156 100644 --- a/sx/tests/test_demos.py +++ b/sx/tests/test_demos.py @@ -36,13 +36,10 @@ def nav(page: Page, path: str): js_errors: list[str] = [] page.on("pageerror", lambda err: js_errors.append(str(err))) - page.goto(f"{BASE}/sx/{path}", wait_until="networkidle") + page.goto(f"{BASE}/sx/{path}", wait_until="domcontentloaded") - # Poll briefly for JS errors — pageerror fires async during networkidle - for _ in range(10): - if js_errors: - break - page.wait_for_timeout(100) + # Wait for SX runtime hydration to complete + page.wait_for_selector("html[data-sx-ready]", timeout=15000) # Fail fast on JS errors — don't wait for content that will never appear if js_errors: diff --git a/tests/playwright/demo-interactions.spec.js b/tests/playwright/demo-interactions.spec.js index 1fd9c08b..08e5c6e3 100644 --- a/tests/playwright/demo-interactions.spec.js +++ b/tests/playwright/demo-interactions.spec.js @@ -1,16 +1,9 @@ // @ts-check /** * Demo interaction tests — verify every demo actually functions. - * Each test is isolated (fresh page.goto) for reliability. - * Server cache keeps page loads fast. */ const { test, expect } = require('playwright/test'); -const BASE_URL = process.env.SX_TEST_URL || 'http://localhost:8013'; - -async function loadDemo(page, path) { - await page.goto(BASE_URL + '/sx/' + path, { waitUntil: 'networkidle', timeout: 15000 }); - await page.waitForTimeout(500); -} +const { loadPage } = require('./helpers'); function island(page, pattern) { return page.locator(`[data-sx-island*="${pattern}"]`); @@ -32,7 +25,7 @@ async function assertNoClassLeak(page, scope) { test.describe('Reactive island interactions', () => { test('counter: + and − change count and doubled', async ({ page }) => { - await loadDemo(page, '(geography.(reactive.(examples.counter)))'); + await loadPage(page, '(geography.(reactive.(examples.counter)))'); const el = island(page, 'counter'); await expect(el).toBeVisible({ timeout: 10000 }); const buttons = el.locator('button'); @@ -52,7 +45,7 @@ test.describe('Reactive island interactions', () => { }); test('temperature: +/− change celsius and fahrenheit', async ({ page }) => { - await loadDemo(page, '(geography.(reactive.(examples.temperature)))'); + await loadPage(page, '(geography.(reactive.(examples.temperature)))'); const el = island(page, 'temperature'); await expect(el).toBeVisible({ timeout: 10000 }); const buttons = el.locator('button'); @@ -65,7 +58,7 @@ test.describe('Reactive island interactions', () => { }); test('stopwatch: start shows elapsed time', async ({ page }) => { - await loadDemo(page, '(geography.(reactive.(examples.stopwatch)))'); + await loadPage(page, '(geography.(reactive.(examples.stopwatch)))'); const el = island(page, 'stopwatch'); await expect(el).toBeVisible({ timeout: 10000 }); const textBefore = await el.textContent(); @@ -75,7 +68,7 @@ test.describe('Reactive island interactions', () => { }); test('input-binding: typing updates live preview', async ({ page }) => { - await loadDemo(page, '(geography.(reactive.(examples.input-binding)))'); + await loadPage(page, '(geography.(reactive.(examples.input-binding)))'); const el = island(page, 'input-binding'); await expect(el).toBeVisible({ timeout: 10000 }); await el.locator('input').first().fill('playwright test'); @@ -84,7 +77,7 @@ test.describe('Reactive island interactions', () => { }); test('dynamic-class: toggle changes element styling', async ({ page }) => { - await loadDemo(page, '(geography.(reactive.(examples.dynamic-class)))'); + await loadPage(page, '(geography.(reactive.(examples.dynamic-class)))'); const el = island(page, 'dynamic-class'); await expect(el).toBeVisible({ timeout: 10000 }); const htmlBefore = await el.innerHTML(); @@ -94,7 +87,7 @@ test.describe('Reactive island interactions', () => { }); test('reactive-list: add button increases items', async ({ page }) => { - await loadDemo(page, '(geography.(reactive.(examples.reactive-list)))'); + await loadPage(page, '(geography.(reactive.(examples.reactive-list)))'); const el = island(page, 'reactive-list'); await expect(el).toBeVisible({ timeout: 10000 }); const textBefore = await el.textContent(); @@ -104,7 +97,7 @@ test.describe('Reactive island interactions', () => { }); test('stores: writer and reader share state', async ({ page }) => { - await loadDemo(page, '(geography.(reactive.(examples.stores)))'); + await loadPage(page, '(geography.(reactive.(examples.stores)))'); const writer = island(page, 'store-writer'); const reader = island(page, 'store-reader'); await expect(writer).toBeVisible({ timeout: 10000 }); @@ -114,7 +107,7 @@ test.describe('Reactive island interactions', () => { }); test('refs: focus button focuses input', async ({ page }) => { - await loadDemo(page, '(geography.(reactive.(examples.refs)))'); + await loadPage(page, '(geography.(reactive.(examples.refs)))'); const el = island(page, 'refs'); await expect(el).toBeVisible({ timeout: 10000 }); const textBefore = await el.textContent(); @@ -126,7 +119,7 @@ test.describe('Reactive island interactions', () => { }); test('portal: button toggles portal content', async ({ page }) => { - await loadDemo(page, '(geography.(reactive.(examples.portal)))'); + await loadPage(page, '(geography.(reactive.(examples.portal)))'); const el = island(page, 'portal'); await expect(el).toBeVisible({ timeout: 10000 }); const before = await page.locator('#portal-root').innerHTML(); @@ -136,7 +129,7 @@ test.describe('Reactive island interactions', () => { }); test('imperative: button triggers DOM manipulation', async ({ page }) => { - await loadDemo(page, '(geography.(reactive.(examples.imperative)))'); + await loadPage(page, '(geography.(reactive.(examples.imperative)))'); const el = island(page, 'imperative'); await expect(el).toBeVisible({ timeout: 10000 }); const textBefore = await el.textContent(); @@ -146,7 +139,7 @@ test.describe('Reactive island interactions', () => { }); test('error-boundary: trigger shows boundary message', async ({ page }) => { - await loadDemo(page, '(geography.(reactive.(examples.error-boundary)))'); + await loadPage(page, '(geography.(reactive.(examples.error-boundary)))'); const el = island(page, 'error-boundary'); await expect(el).toBeVisible({ timeout: 10000 }); const btn = el.locator('button').filter({ hasText: /error|trigger|throw/i }).first(); @@ -158,7 +151,7 @@ test.describe('Reactive island interactions', () => { }); test('event-bridge: sender triggers receiver', async ({ page }) => { - await loadDemo(page, '(geography.(reactive.(examples.event-bridge-demo)))'); + await loadPage(page, '(geography.(reactive.(examples.event-bridge-demo)))'); const el = island(page, 'event-bridge'); await expect(el).toBeVisible({ timeout: 10000 }); const textBefore = await el.textContent(); @@ -168,11 +161,10 @@ test.describe('Reactive island interactions', () => { }); test('resource: shows loading then resolved data', async ({ page }) => { - await loadDemo(page, '(geography.(reactive.(examples.resource)))'); + await loadPage(page, '(geography.(reactive.(examples.resource)))'); const el = island(page, 'resource'); await expect(el).toBeVisible({ timeout: 10000 }); - await page.waitForTimeout(2000); - expect(await el.textContent()).toContain('Ada'); + await expect(el).toContainText('Ada', { timeout: 5000 }); }); }); @@ -184,7 +176,7 @@ test.describe('Reactive island interactions', () => { test.describe('Marshes interactions', () => { test('hypermedia-feeds: reactive +/− works', async ({ page }) => { - await loadDemo(page, '(geography.(marshes.hypermedia-feeds))'); + await loadPage(page, '(geography.(marshes.hypermedia-feeds))'); const el = island(page, 'marsh-product'); await expect(el).toBeVisible({ timeout: 10000 }); const plusBtn = el.locator('button:has-text("+")').first(); @@ -198,7 +190,7 @@ test.describe('Marshes interactions', () => { }); test('on-settle: settle evaluates after swap', async ({ page }) => { - await loadDemo(page, '(geography.(marshes.on-settle))'); + await loadPage(page, '(geography.(marshes.on-settle))'); const el = island(page, 'marsh-settle'); await expect(el).toBeVisible({ timeout: 10000 }); const btn = el.locator('button').first(); @@ -212,7 +204,7 @@ test.describe('Marshes interactions', () => { }); test('server-signals: server writes to client signal', async ({ page }) => { - await loadDemo(page, '(geography.(marshes.server-signals))'); + await loadPage(page, '(geography.(marshes.server-signals))'); const writer = island(page, 'marsh-store-writer'); const reader = island(page, 'marsh-store-reader'); await expect(writer).toBeVisible({ timeout: 10000 }); @@ -220,7 +212,7 @@ test.describe('Marshes interactions', () => { }); test('view-transform: view toggle changes rendering', async ({ page }) => { - await loadDemo(page, '(geography.(marshes.view-transform))'); + await loadPage(page, '(geography.(marshes.view-transform))'); const el = island(page, 'marsh-view-transform'); await expect(el).toBeVisible({ timeout: 10000 }); const viewBtns = el.locator('button'); @@ -246,8 +238,7 @@ test.describe('Server health', () => { const demos = ['counter', 'temperature', 'stopwatch', 'input-binding', 'dynamic-class', 'reactive-list', 'stores', 'resource']; for (const demo of demos) { - await page.goto(BASE_URL + `/sx/(geography.(reactive.(examples.${demo})))`, { waitUntil: 'networkidle', timeout: 15000 }); - await page.waitForTimeout(500); + await loadPage(page, `(geography.(reactive.(examples.${demo})))`); } const real = errors.filter(e => !e.includes('net::ERR') && !e.includes('fetch')); expect(real).toEqual([]); @@ -259,8 +250,7 @@ test.describe('Server health', () => { const pages = ['hypermedia-feeds', 'on-settle', 'server-signals', 'signal-triggers', 'view-transform']; for (const p of pages) { - await page.goto(BASE_URL + `/sx/(geography.(marshes.${p}))`, { waitUntil: 'networkidle', timeout: 15000 }); - await page.waitForTimeout(500); + await loadPage(page, `(geography.(marshes.${p}))`); } const real = errors.filter(e => !e.includes('net::ERR') && !e.includes('fetch')); expect(real).toEqual([]); diff --git a/tests/playwright/geography-demos.spec.js b/tests/playwright/geography-demos.spec.js index c9796829..dc826e74 100644 --- a/tests/playwright/geography-demos.spec.js +++ b/tests/playwright/geography-demos.spec.js @@ -1,23 +1,13 @@ // @ts-check /** * Geography demos — comprehensive page load + interaction tests. - * Each test does a fresh page.goto() for isolation, but cached server - * responses (pre-warmed) keep these fast (~0.5s each vs ~2s uncached). */ const { test, expect } = require('playwright/test'); -const BASE_URL = process.env.SX_TEST_URL || 'http://localhost:8013'; +const { loadPage } = require('./helpers'); // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- -async function loadPage(page, path) { - await page.goto(BASE_URL + '/sx/' + path, { waitUntil: 'networkidle', timeout: 15000 }); - await page.waitForTimeout(500); - const root = page.locator('#sx-root'); - await expect(root).toBeVisible({ timeout: 10000 }); - return root; -} - async function expectIsland(page, pattern) { const island = page.locator(`[data-sx-island*="${pattern}"]`); await expect(island).toBeVisible({ timeout: 8000 }); @@ -44,7 +34,8 @@ test.describe('Geography sections', () => { for (const [path, text] of sections) { test(`${path} loads`, async ({ page }) => { - const root = await loadPage(page, path); + await loadPage(page, path); + const root = page.locator('#sx-root'); expect(await root.textContent()).toContain(text); }); } @@ -153,7 +144,6 @@ test('counter → temperature → counter: all stay reactive', async ({ page }) const tempLink = page.locator('a[href*="temperature"]').first(); if (await tempLink.count() > 0) { await tempLink.click(); - await page.waitForTimeout(2000); el = await expectIsland(page, 'temperature'); buttons = el.locator('button'); if (await buttons.count() >= 2) { @@ -168,7 +158,6 @@ test('counter → temperature → counter: all stay reactive', async ({ page }) const counterLink = page.locator('a[href*="counter"]').first(); if (await counterLink.count() > 0) { await counterLink.click(); - await page.waitForTimeout(2000); el = await expectIsland(page, 'counter'); buttons = el.locator('button'); before = await el.textContent(); diff --git a/tests/playwright/handler-responses.spec.js b/tests/playwright/handler-responses.spec.js index b8e4a9a8..3d6dd454 100644 --- a/tests/playwright/handler-responses.spec.js +++ b/tests/playwright/handler-responses.spec.js @@ -1,11 +1,10 @@ const { test, expect } = require('playwright/test'); -const BASE_URL = process.env.SX_TEST_URL || 'http://localhost:8013'; +const { loadPage } = require('./helpers'); test.describe('Handler responses render correctly', () => { test('bulk-update: deactivate renders proper HTML attributes', async ({ page }) => { - await page.goto(BASE_URL + '/sx/(geography.(hypermedia.(example.bulk-update)))', { waitUntil: 'networkidle' }); - await page.waitForTimeout(2000); + await loadPage(page, '(geography.(hypermedia.(example.bulk-update)))'); // Check first row, note its status const firstCheckbox = page.locator('input[type="checkbox"]').first(); @@ -13,7 +12,7 @@ test.describe('Handler responses render correctly', () => { // Click Deactivate await page.locator('button:has-text("Deactivate")').first().click(); - await page.waitForTimeout(3000); + await page.waitForTimeout(2000); // The table should still have proper HTML — no raw "class" text visible const tableText = await page.locator('table').first().textContent(); @@ -28,8 +27,7 @@ test.describe('Handler responses render correctly', () => { }); test('delete-row: response renders with proper HTML classes', async ({ page }) => { - await page.goto(BASE_URL + '/sx/(geography.(hypermedia.(example.delete-row)))', { waitUntil: 'networkidle' }); - await page.waitForTimeout(2000); + await loadPage(page, '(geography.(hypermedia.(example.delete-row)))'); // Table should have proper class attrs, not text const tableText = await page.locator('table').first().textContent(); @@ -38,7 +36,7 @@ test.describe('Handler responses render correctly', () => { // Click delete and check response doesn't corrupt await page.locator('button:has-text("Delete")').first().click(); - await page.waitForTimeout(3000); + await page.waitForTimeout(2000); const afterText = await page.locator('table').first().textContent(); expect(afterText).not.toContain('classpx'); @@ -46,14 +44,13 @@ test.describe('Handler responses render correctly', () => { }); test('click-to-load: loaded rows have proper HTML', async ({ page }) => { - await page.goto(BASE_URL + '/sx/(geography.(hypermedia.(example.click-to-load)))', { waitUntil: 'networkidle' }); - await page.waitForTimeout(2000); + await loadPage(page, '(geography.(hypermedia.(example.click-to-load)))'); const rowsBefore = await page.locator('table tbody tr').count(); const loadBtn = page.locator('button:has-text("Load More")').first(); if (await loadBtn.count() > 0) { await loadBtn.click(); - await page.waitForTimeout(3000); + await page.waitForTimeout(2000); const rowsAfter = await page.locator('table tbody tr').count(); expect(rowsAfter).toBeGreaterThan(rowsBefore); @@ -66,12 +63,11 @@ test.describe('Handler responses render correctly', () => { }); test('active-search: results render as proper HTML', async ({ page }) => { - await page.goto(BASE_URL + '/sx/(geography.(hypermedia.(example.active-search)))', { waitUntil: 'networkidle' }); - await page.waitForTimeout(2000); + await loadPage(page, '(geography.(hypermedia.(example.active-search)))'); const input = page.locator('input[placeholder*="earch"], input[name="q"]').first(); await input.fill('python'); - await page.waitForTimeout(3000); + await page.waitForTimeout(2000); const results = page.locator('#search-results'); const text = await results.textContent(); @@ -81,8 +77,7 @@ test.describe('Handler responses render correctly', () => { }); test('form-submission: response renders as HTML not SX text', async ({ page }) => { - await page.goto(BASE_URL + '/sx/(geography.(hypermedia.(example.form-submission)))', { waitUntil: 'networkidle' }); - await page.waitForTimeout(2000); + await loadPage(page, '(geography.(hypermedia.(example.form-submission)))'); const form = page.locator('form').first(); const inputs = form.locator('input[type="text"], input:not([type])'); @@ -92,7 +87,7 @@ test.describe('Handler responses render correctly', () => { const submit = form.locator('button[type="submit"], button').first(); await submit.click(); - await page.waitForTimeout(3000); + await page.waitForTimeout(2000); // Response should not have raw SX class text const root = page.locator('#sx-root'); @@ -102,13 +97,12 @@ test.describe('Handler responses render correctly', () => { }); test('edit-row: edited row renders with proper classes', async ({ page }) => { - await page.goto(BASE_URL + '/sx/(geography.(hypermedia.(example.edit-row)))', { waitUntil: 'networkidle' }); - await page.waitForTimeout(2000); + await loadPage(page, '(geography.(hypermedia.(example.edit-row)))'); const editBtn = page.locator('button:has-text("Edit")').first(); if (await editBtn.count() > 0) { await editBtn.click(); - await page.waitForTimeout(2000); + await page.waitForTimeout(1000); // Edit form or inline edit should not show raw class text const root = page.locator('#sx-root'); @@ -118,14 +112,13 @@ test.describe('Handler responses render correctly', () => { }); test('tabs: tab content renders as proper HTML', async ({ page }) => { - await page.goto(BASE_URL + '/sx/(geography.(hypermedia.(example.tabs)))', { waitUntil: 'networkidle' }); - await page.waitForTimeout(2000); + await loadPage(page, '(geography.(hypermedia.(example.tabs)))'); // Click a tab const tabs = page.locator('[sx-get*="tab"]'); if (await tabs.count() >= 2) { await tabs.nth(1).click(); - await page.waitForTimeout(2000); + await page.waitForTimeout(1000); const root = page.locator('#sx-root'); const text = await root.textContent(); diff --git a/tests/playwright/helpers.js b/tests/playwright/helpers.js new file mode 100644 index 00000000..b35cf043 --- /dev/null +++ b/tests/playwright/helpers.js @@ -0,0 +1,22 @@ +// Shared helpers for Playwright tests + +const BASE_URL = process.env.SX_TEST_URL || 'http://localhost:8013'; + +/** + * Wait for the SX runtime to finish hydration. + * boot-init sets data-sx-ready="true" on after all islands are hydrated. + */ +async function waitForSxReady(page, timeout = 15000) { + await page.waitForSelector('html[data-sx-ready]', { timeout }); +} + +/** + * Navigate to an SX page and wait for hydration to complete. + * Replaces the old pattern of networkidle + arbitrary sleep. + */ +async function loadPage(page, path, timeout = 15000) { + await page.goto(BASE_URL + '/sx/' + path, { waitUntil: 'domcontentloaded', timeout }); + await waitForSxReady(page); +} + +module.exports = { BASE_URL, waitForSxReady, loadPage }; diff --git a/tests/playwright/isomorphic.spec.js b/tests/playwright/isomorphic.spec.js index fbbcd18b..d83db591 100644 --- a/tests/playwright/isomorphic.spec.js +++ b/tests/playwright/isomorphic.spec.js @@ -1,7 +1,7 @@ // @ts-check const { test, expect } = require('playwright/test'); +const { BASE_URL, waitForSxReady } = require('./helpers'); -const BASE_URL = process.env.SX_TEST_URL || 'http://localhost:8013'; const TEST_PAGE = '/sx/(etc.(philosophy.wittgenstein))'; /** @@ -97,8 +97,8 @@ test.describe('Isomorphic SSR', () => { // Get client DOM (with JS) const jsContext = await browser.newContext({ javaScriptEnabled: true }); const jsPage = await jsContext.newPage(); - await jsPage.goto(BASE_URL + TEST_PAGE, { waitUntil: 'networkidle' }); - await jsPage.waitForTimeout(2000); // wait for hydration + await jsPage.goto(BASE_URL + TEST_PAGE, { waitUntil: 'domcontentloaded' }); + await waitForSxReady(jsPage); const clientStructure = await getSxRootStructure(jsPage); const clientText = await getSxRootText(jsPage); await jsContext.close(); @@ -137,9 +137,11 @@ test.describe('Isomorphic SSR', () => { }); test('islands hydrate and reactive signals work', async ({ page }) => { - await page.goto(BASE_URL + '/sx/', { waitUntil: 'networkidle' }); - await page.waitForSelector('[data-sx-island="layouts/header"]', { timeout: 15000 }); - await page.waitForSelector('[data-sx-island="home/stepper"]', { timeout: 5000 }); + await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' }); + await waitForSxReady(page); + + await expect(page.locator('[data-sx-island="layouts/header"]')).toBeVisible({ timeout: 5000 }); + await expect(page.locator('[data-sx-island="home/stepper"]')).toBeVisible({ timeout: 5000 }); // Stepper buttons change the count const stepper = page.locator('[data-sx-island="home/stepper"]'); @@ -159,8 +161,8 @@ test.describe('Isomorphic SSR', () => { }); test('navigation links have valid URLs (no [object Object])', async ({ page }) => { - await page.goto(BASE_URL + '/sx/', { waitUntil: 'networkidle' }); - await page.waitForTimeout(1000); + await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' }); + await waitForSxReady(page); // Check all nav links for [object Object] — regression for FFI primitive overrides const brokenLinks = await page.evaluate(() => { @@ -177,11 +179,8 @@ test.describe('Isomorphic SSR', () => { }); test('navigation preserves header island state', async ({ page }) => { - await page.goto(BASE_URL + '/sx/', { waitUntil: 'networkidle' }); - - // Wait for header island to hydrate - await page.waitForSelector('[data-sx-island="layouts/header"]', { timeout: 15000 }); - await page.waitForTimeout(1000); + await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' }); + await waitForSxReady(page); // Click "reactive" to change colour const reactive = page.locator('[data-sx-island="layouts/header"]').getByText('reactive'); @@ -195,7 +194,7 @@ test.describe('Isomorphic SSR', () => { // Navigate via SPA link const geoLink = page.locator('a[sx-get*="geography"]').first(); await geoLink.click(); - await page.waitForTimeout(2000); + await expect(page).toHaveURL(/geography/, { timeout: 5000 }); // Colour should be preserved (def-store keeps signals across re-hydration) const colourAfter = await reactive.evaluate(el => el.style.color); diff --git a/tests/playwright/navigation.spec.js b/tests/playwright/navigation.spec.js index df074126..5af76882 100644 --- a/tests/playwright/navigation.spec.js +++ b/tests/playwright/navigation.spec.js @@ -2,7 +2,7 @@ // 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'; +const { BASE_URL, waitForSxReady, loadPage } = require('./helpers'); test.describe('Page Navigation', () => { @@ -12,15 +12,11 @@ test.describe('Page Navigation', () => { if (msg.type() === 'error') errors.push(msg.text()); }); - await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'networkidle' }); - await page.waitForTimeout(2000); + await loadPage(page, '(geography)'); // Click "Reactive Islands" nav link await page.click('a[href*="geography.(reactive)"]:not([href*="runtime"])'); - await page.waitForTimeout(3000); - - // Should have navigated — URL must contain reactive - expect(page.url()).toContain('reactive'); + await expect(page).toHaveURL(/reactive/, { timeout: 5000 }); // Page should show Reactive Islands content const body = await page.textContent('body'); @@ -37,15 +33,11 @@ test.describe('Page Navigation', () => { if (msg.type() === 'error') errors.push(msg.text()); }); - await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'networkidle' }); - await page.waitForTimeout(2000); + await loadPage(page, '(geography)'); // Click the logo in the header island await page.click('[data-sx-island="layouts/header"] a[href="/sx/"]'); - await page.waitForTimeout(3000); - - // Should have navigated to home - expect(page.url()).toMatch(/\/sx\/?$/); + await expect(page).toHaveURL(/\/sx\/?$/, { timeout: 5000 }); // No SX evaluation errors const sxErrors = errors.filter(e => e.includes('Undefined symbol')); @@ -58,21 +50,16 @@ test.describe('Page Navigation', () => { if (msg.type() === 'error') errors.push(msg.text()); }); - await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'networkidle' }); - await page.waitForTimeout(2000); + await loadPage(page, '(geography)'); // Navigate to Reactive Islands await page.click('a[href*="geography.(reactive)"]:not([href*="runtime"])'); - await page.waitForTimeout(3000); - expect(page.url()).toContain('reactive'); + await expect(page).toHaveURL(/reactive/, { timeout: 5000 }); // 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'); + await expect(page).toHaveURL(/geography/, { timeout: 5000 }); + await expect(page).not.toHaveURL(/reactive/); // Geography heading should be visible const heading = await page.locator('h1, h2').first(); @@ -90,8 +77,7 @@ test.describe('Page Navigation', () => { errors.push(msg.text()); }); - await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'networkidle' }); - await page.waitForTimeout(3000); + await loadPage(page, '(geography)'); // No JIT or SX errors const sxErrors = errors.filter(e => @@ -100,8 +86,7 @@ test.describe('Page Navigation', () => { }); test('copyright shows current route after SX navigation', async ({ page }) => { - await page.goto(BASE_URL + '/sx/', { waitUntil: 'networkidle' }); - await page.waitForTimeout(3000); + await loadPage(page, ''); // Mark the page to verify SX navigation (not full reload) await page.evaluate(() => window.__sx_nav_marker = true); @@ -113,7 +98,7 @@ test.describe('Page Navigation', () => { // Navigate via SX (sx-get link) await page.click('a[sx-get*="(geography)"]'); - await page.waitForTimeout(3000); + await expect(page).toHaveURL(/geography/, { timeout: 5000 }); // Verify SX navigation (marker survives SX nav, lost on reload) const marker = await page.evaluate(() => window.__sx_nav_marker); @@ -126,8 +111,7 @@ test.describe('Page Navigation', () => { }); test('stepper persists index across navigation', async ({ page }) => { - await page.goto(BASE_URL + '/sx/', { waitUntil: 'networkidle' }); - await page.waitForTimeout(3000); + await loadPage(page, ''); // Get the initial stepper index const getIndex = () => page.evaluate(() => { @@ -144,21 +128,21 @@ test.describe('Page Navigation', () => { const btns = document.querySelectorAll('[data-sx-island="home/stepper"] button'); if (btns.length >= 2) btns[1].click(); // next button }); - await page.waitForTimeout(500); + await page.waitForTimeout(300); const advanced = await getIndex(); expect(advanced).toBe(initial + 1); // Navigate away await page.click('a[sx-get*="(geography)"]'); - await page.waitForTimeout(2000); + await expect(page).toHaveURL(/geography/, { timeout: 5000 }); // Navigate back home await page.evaluate(() => { const link = document.querySelector('a[sx-get*="/sx/"]'); if (link) link.click(); }); - await page.waitForTimeout(3000); + await expect(page).toHaveURL(/\/sx\/?$/, { timeout: 5000 }); // Stepper should still show the advanced index const afterNav = await getIndex(); @@ -166,7 +150,7 @@ test.describe('Page Navigation', () => { }); test('header island renders with SSR', async ({ page }) => { - await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'networkidle' }); + await loadPage(page, '(geography)'); // Header should be visible const header = page.locator('[data-sx-island="layouts/header"]'); diff --git a/tests/playwright/reactive-nav.spec.js b/tests/playwright/reactive-nav.spec.js index 75267482..f12df0ca 100644 --- a/tests/playwright/reactive-nav.spec.js +++ b/tests/playwright/reactive-nav.spec.js @@ -1,11 +1,12 @@ // @ts-check const { test, expect } = require('playwright/test'); -const BASE_URL = process.env.SX_TEST_URL || 'http://localhost:8013'; +const { BASE_URL, waitForSxReady } = require('./helpers'); test.describe('Reactive Island Navigation', () => { test('counter island works on direct load', async ({ page }) => { - await page.goto(BASE_URL + '/sx/(geography.(reactive.(examples.counter)))', { waitUntil: 'networkidle' }); + await page.goto(BASE_URL + '/sx/(geography.(reactive.(examples.counter)))', { waitUntil: 'domcontentloaded' }); + await waitForSxReady(page); const island = page.locator('[data-sx-island*="counter"]'); await expect(island).toBeVisible({ timeout: 10000 }); @@ -20,7 +21,8 @@ test.describe('Reactive Island Navigation', () => { }); test('temperature island works on direct load', async ({ page }) => { - await page.goto(BASE_URL + '/sx/(geography.(reactive.(examples.temperature)))', { waitUntil: 'networkidle' }); + await page.goto(BASE_URL + '/sx/(geography.(reactive.(examples.temperature)))', { waitUntil: 'domcontentloaded' }); + await waitForSxReady(page); const island = page.locator('[data-sx-island*="temperature"]'); await expect(island).toBeVisible({ timeout: 10000 }); @@ -32,7 +34,8 @@ test.describe('Reactive Island Navigation', () => { }); test('counter → temperature: temperature island is reactive after SPA nav', async ({ page }) => { - await page.goto(BASE_URL + '/sx/(geography.(reactive.(examples.counter)))', { waitUntil: 'networkidle' }); + await page.goto(BASE_URL + '/sx/(geography.(reactive.(examples.counter)))', { waitUntil: 'domcontentloaded' }); + await waitForSxReady(page); const counter = page.locator('[data-sx-island*="counter"]'); await expect(counter).toBeVisible({ timeout: 10000 }); @@ -42,7 +45,8 @@ test.describe('Reactive Island Navigation', () => { if (await tempLink.count() > 0) { await tempLink.click(); } else { - await page.goto(BASE_URL + '/sx/(geography.(reactive.(examples.temperature)))', { waitUntil: 'networkidle' }); + await page.goto(BASE_URL + '/sx/(geography.(reactive.(examples.temperature)))', { waitUntil: 'domcontentloaded' }); + await waitForSxReady(page); } const tempIsland = page.locator('[data-sx-island*="temperature"]'); @@ -58,7 +62,8 @@ test.describe('Reactive Island Navigation', () => { }); test('temperature → counter: counter island is reactive after SPA nav', async ({ page }) => { - await page.goto(BASE_URL + '/sx/(geography.(reactive.(examples.temperature)))', { waitUntil: 'networkidle' }); + await page.goto(BASE_URL + '/sx/(geography.(reactive.(examples.temperature)))', { waitUntil: 'domcontentloaded' }); + await waitForSxReady(page); const temp = page.locator('[data-sx-island*="temperature"]'); await expect(temp).toBeVisible({ timeout: 10000 }); @@ -67,7 +72,8 @@ test.describe('Reactive Island Navigation', () => { if (await counterLink.count() > 0) { await counterLink.click(); } else { - await page.goto(BASE_URL + '/sx/(geography.(reactive.(examples.counter)))', { waitUntil: 'networkidle' }); + await page.goto(BASE_URL + '/sx/(geography.(reactive.(examples.counter)))', { waitUntil: 'domcontentloaded' }); + await waitForSxReady(page); } const counter = page.locator('[data-sx-island*="counter"]'); @@ -86,11 +92,14 @@ test.describe('Reactive Island Navigation', () => { const errors = []; page.on('pageerror', err => errors.push(err.message)); - await page.goto(BASE_URL + '/sx/(geography.(reactive.(examples.counter)))', { waitUntil: 'networkidle' }); + await page.goto(BASE_URL + '/sx/(geography.(reactive.(examples.counter)))', { waitUntil: 'domcontentloaded' }); + await waitForSxReady(page); const link = page.locator('a[href*="temperature"]').first(); - if (await link.count() > 0) await link.click(); - await page.waitForTimeout(2000); + if (await link.count() > 0) { + await link.click(); + await expect(page).toHaveURL(/temperature/, { timeout: 5000 }); + } const real = errors.filter(e => !e.includes('Failed to fetch') && !e.includes('net::ERR') diff --git a/tests/playwright/stepper.spec.js b/tests/playwright/stepper.spec.js index 106786bd..90131ef4 100644 --- a/tests/playwright/stepper.spec.js +++ b/tests/playwright/stepper.spec.js @@ -1,9 +1,8 @@ const { test, expect } = require('playwright/test'); -const BASE_URL = process.env.SX_TEST_URL || 'http://localhost:8013'; +const { loadPage } = require('./helpers'); test('home page stepper: no raw SX component calls visible', async ({ page }) => { - await page.goto(BASE_URL + '/sx/', { waitUntil: 'networkidle' }); - await page.waitForTimeout(5000); + await loadPage(page, ''); const stepper = page.locator('[data-sx-island="home/stepper"]'); await expect(stepper).toBeVisible({ timeout: 10000 }); @@ -23,7 +22,7 @@ test('home page stepper: no raw SX component calls visible', async ({ page }) => await expect(buttons).toHaveCount(2); const textBefore = await stepper.textContent(); await buttons.last().click(); - await page.waitForTimeout(500); + await page.waitForTimeout(300); const textAfter = await stepper.textContent(); expect(textAfter).not.toBe(textBefore); }); diff --git a/web/boot.sx b/web/boot.sx index 408f6f9d..9b24e00b 100644 --- a/web/boot.sx +++ b/web/boot.sx @@ -431,4 +431,10 @@ (sx-hydrate-islands nil) (run-post-render-hooks) (process-elements nil) - (dom-listen (dom-window) "popstate" (fn (e) (handle-popstate 0)))))) + (dom-listen (dom-window) "popstate" (fn (e) (handle-popstate 0))) + (dom-set-attr + (host-get (dom-document) "documentElement") + "data-sx-ready" + "true") + (dom-dispatch (dom-document) "sx:ready" nil) + (log-info "sx:ready"))))