Compare commits
6 Commits
17b5acb71f
...
623529d3be
| Author | SHA1 | Date | |
|---|---|---|---|
| 623529d3be | |||
| 0f63216adc | |||
| ecd89270c0 | |||
| 092da5b819 | |||
| 40bf4c38f1 | |||
| b46bef2808 |
@@ -2356,6 +2356,25 @@
|
||||
((= head (quote live-no-op)) nil)
|
||||
((= head (quote when-feat-no-op)) nil)
|
||||
((= head (quote bind-feat)) nil)
|
||||
((= head (quote socket))
|
||||
(let
|
||||
((name-path (nth ast 1))
|
||||
(url (nth ast 2))
|
||||
(timeout (nth ast 3))
|
||||
(on-message (nth ast 4)))
|
||||
(let
|
||||
((handler-sx (if (and (list? on-message) (= (first on-message) (quote on-message))) (list (quote fn) (list (quote event)) (hs-to-sx (nth on-message 2))) nil)))
|
||||
(let
|
||||
((json? (if (and (list? on-message) (= (first on-message) (quote on-message))) (nth on-message 1) false)))
|
||||
(list
|
||||
(quote hs-socket-register!)
|
||||
(cons
|
||||
(quote list)
|
||||
(map (fn (seg) seg) name-path))
|
||||
(hs-to-sx url)
|
||||
(hs-to-sx timeout)
|
||||
handler-sx
|
||||
json?)))))
|
||||
((= head (quote on)) (emit-on ast))
|
||||
((= head (quote when-changes))
|
||||
(let
|
||||
|
||||
@@ -3178,6 +3178,35 @@
|
||||
(match-kw "end")
|
||||
(list (quote bind-feat) lhs rhs)))
|
||||
(true (do (match-kw "end") (list (quote bind-feat) lhs nil)))))))
|
||||
(define
|
||||
parse-socket-feat
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((first-seg (tp-val)))
|
||||
(do
|
||||
(adv!)
|
||||
(define
|
||||
collect-dots!
|
||||
(fn
|
||||
(acc)
|
||||
(if
|
||||
(= (tp-type) "class")
|
||||
(let
|
||||
((seg (tp-val)))
|
||||
(do (adv!) (collect-dots! (append acc (list seg)))))
|
||||
acc)))
|
||||
(let
|
||||
((name-path (collect-dots! (list first-seg))))
|
||||
(let
|
||||
((url (parse-arith (parse-poss (parse-atom)))))
|
||||
(let
|
||||
((timeout (if (match-kw "with") (do (when (and (or (= (tp-type) "ident") (= (tp-type) "keyword")) (= (tp-val) "timeout")) (adv!)) (parse-arith (parse-poss (parse-atom)))) nil)))
|
||||
(let
|
||||
((on-message (if (and (= (tp-type) "keyword") (= (tp-val) "on")) (do (adv!) (when (and (or (= (tp-type) "ident") (= (tp-type) "keyword")) (= (tp-val) "message")) (do (adv!) (let ((json? (if (match-kw "as") (do (when (and (or (= (tp-type) "ident") (= (tp-type) "keyword")) (= (tp-val) "JSON")) (adv!)) true) false))) (let ((body (parse-cmd-list))) (list (quote on-message) json? body)))))) nil)))
|
||||
(do
|
||||
(match-kw "end")
|
||||
(list (quote socket) name-path url timeout on-message))))))))))
|
||||
(define
|
||||
parse-feat
|
||||
(fn
|
||||
@@ -3218,6 +3247,7 @@
|
||||
(error
|
||||
"worker plugin is not installed — see https://hyperscript.org/features/worker"))
|
||||
((= val "bind") (do (adv!) (parse-bind-feat)))
|
||||
((= val "socket") (do (adv!) (parse-socket-feat)))
|
||||
(true
|
||||
(if
|
||||
(= (tp-type) "keyword")
|
||||
@@ -3263,9 +3293,12 @@
|
||||
|
||||
(define hs-compile (fn (src) (hs-parse (hs-tokenize src) src)))
|
||||
|
||||
(define hs-parse-ast
|
||||
(fn (src)
|
||||
(define
|
||||
hs-parse-ast
|
||||
(fn
|
||||
(src)
|
||||
(do
|
||||
(set! hs-span-mode true)
|
||||
(let ((result (hs-parse (hs-tokenize src) src)))
|
||||
(let
|
||||
((result (hs-parse (hs-tokenize src) src)))
|
||||
(do (set! hs-span-mode false) result)))))
|
||||
|
||||
@@ -1210,7 +1210,14 @@
|
||||
((= type-name "Array") (if (list? value) value (list value)))
|
||||
((= type-name "HTML")
|
||||
(cond
|
||||
((list? value) (join "" (map (fn (x) (str x)) value)))
|
||||
((list? value)
|
||||
(join
|
||||
""
|
||||
(map
|
||||
(fn
|
||||
(x)
|
||||
(if (hs-element? x) (host-get x "outerHTML") (str x)))
|
||||
value)))
|
||||
((hs-element? value) (host-get value "outerHTML"))
|
||||
(true (str value))))
|
||||
((= type-name "JSON")
|
||||
@@ -1261,7 +1268,25 @@
|
||||
((factor (pow 10 digits)))
|
||||
(str (/ (floor (+ (* num factor) 0.5)) factor))))))
|
||||
((= type-name "Selector") (str value))
|
||||
((= type-name "Fragment") value)
|
||||
((= type-name "Fragment")
|
||||
(let
|
||||
((frag (host-call (dom-document) "createDocumentFragment")))
|
||||
(do
|
||||
(for-each
|
||||
(fn
|
||||
(item)
|
||||
(if
|
||||
(hs-element? item)
|
||||
(dom-append frag item)
|
||||
(let
|
||||
((tmp (dom-create-element "div")))
|
||||
(do
|
||||
(dom-set-inner-html tmp (str item))
|
||||
(for-each
|
||||
(fn (k) (dom-append frag k))
|
||||
(host-get tmp "children"))))))
|
||||
(if (list? value) value (list value)))
|
||||
frag)))
|
||||
((= type-name "Values") (hs-as-values value))
|
||||
((= type-name "Keys")
|
||||
(if
|
||||
@@ -3109,3 +3134,98 @@
|
||||
(define hs-token-value (fn (tok) (dict-get tok :value)))
|
||||
|
||||
(define hs-token-op? (fn (tok) (dict-get tok :op)))
|
||||
|
||||
(define
|
||||
hs-try-json-parse
|
||||
(fn (data) (if (string? data) (guard (_e nil) (json-parse data)) nil)))
|
||||
|
||||
(define
|
||||
hs-socket-normalise-url
|
||||
(fn
|
||||
(url)
|
||||
(if
|
||||
(or (starts-with? url "ws://") (starts-with? url "wss://"))
|
||||
url
|
||||
(let
|
||||
((proto (host-get (host-global "location") "protocol"))
|
||||
(host-str (host-get (host-global "location") "host")))
|
||||
(let
|
||||
((scheme (if (= proto "https:") "wss://" "ws://")))
|
||||
(str scheme host-str url))))))
|
||||
|
||||
(define
|
||||
hs-socket-bind-name!
|
||||
(fn
|
||||
(name-path wrapper)
|
||||
(let
|
||||
((win (host-global "window")))
|
||||
(if
|
||||
(= (len name-path) 1)
|
||||
(host-set! win (first name-path) wrapper)
|
||||
(do
|
||||
(when
|
||||
(nil? (host-get win (first name-path)))
|
||||
(host-set! win (first name-path) (host-new "Object")))
|
||||
(host-set!
|
||||
(host-get win (first name-path))
|
||||
(nth name-path 1)
|
||||
wrapper))))))
|
||||
|
||||
(define
|
||||
hs-socket-resolve-rpc!
|
||||
(fn
|
||||
(wrapper data)
|
||||
(let
|
||||
((iid (host-get data "iid")))
|
||||
(when
|
||||
(not (nil? iid))
|
||||
(let
|
||||
((pending (host-get wrapper "_pending")))
|
||||
(when
|
||||
(not (nil? pending))
|
||||
(let
|
||||
((entry (host-get pending iid)))
|
||||
(when
|
||||
(not (nil? entry))
|
||||
(host-set! pending iid nil)
|
||||
(if
|
||||
(not (nil? (host-get data "throw")))
|
||||
(host-call-fn
|
||||
(host-get entry "reject")
|
||||
(list (host-get data "throw")))
|
||||
(host-call-fn
|
||||
(host-get entry "resolve")
|
||||
(list (host-get data "return"))))))))))))
|
||||
|
||||
(define
|
||||
hs-socket-register!
|
||||
(fn
|
||||
(name-path url timeout on-message-handler json?)
|
||||
(let
|
||||
((norm-url (hs-socket-normalise-url url)))
|
||||
(let
|
||||
((wrapper (host-new "Object")))
|
||||
(do
|
||||
(host-set! wrapper "_url" norm-url)
|
||||
(host-set! wrapper "_timeout" (if (nil? timeout) 0 timeout))
|
||||
(host-set! wrapper "_pending" (host-new "Object"))
|
||||
(host-set! wrapper "_closed" false)
|
||||
(let
|
||||
((ws (host-new "WebSocket" norm-url)))
|
||||
(do
|
||||
(host-set! wrapper "_ws" ws)
|
||||
(let
|
||||
((msg-handler (host-callback (fn (evt) (do (let ((parsed (hs-try-json-parse (host-get evt "data")))) (when (and (not (nil? parsed)) (not (nil? (host-get parsed "iid")))) (hs-socket-resolve-rpc! wrapper parsed))) (when (not (nil? on-message-handler)) (if json? (let ((data (hs-try-json-parse (host-get evt "data")))) (when (not (nil? data)) (on-message-handler data))) (on-message-handler evt))))))))
|
||||
(do
|
||||
(host-set! ws "onmessage" msg-handler)
|
||||
(host-set! wrapper "_onmessage_handler" msg-handler)
|
||||
(host-set!
|
||||
ws
|
||||
"onclose"
|
||||
(host-callback
|
||||
(fn (e) (host-set! wrapper "_closed" true))))
|
||||
(host-call-fn
|
||||
(host-global "_hsSetupSocket")
|
||||
(list wrapper))
|
||||
(hs-socket-bind-name! name-path wrapper)
|
||||
wrapper)))))))))
|
||||
|
||||
@@ -4,10 +4,10 @@ Live tally for `plans/hs-conformance-to-100.md`. Update after every cluster comm
|
||||
|
||||
```
|
||||
Baseline: 1213/1496 (81.1%)
|
||||
Merged: 1377/1496 (92.0%) delta +164
|
||||
Merged: 1403/1496 (93.8%) delta +190
|
||||
Worktree: all landed
|
||||
Target: 1496/1496 (100.0%)
|
||||
Remaining: ~120 tests (clusters 17/29(partial)/33/34 partial)
|
||||
Remaining: ~89 tests
|
||||
```
|
||||
|
||||
## Cluster ledger
|
||||
@@ -72,7 +72,7 @@ Remaining: ~120 tests (clusters 17/29(partial)/33/34 partial)
|
||||
|
||||
| # | Cluster | Status | Design doc |
|
||||
|---|---------|--------|------------|
|
||||
| 36 | WebSocket + `socket` + RPC proxy | design-done | `plans/designs/e36-websocket.md` |
|
||||
| 36 | WebSocket + `socket` + RPC proxy | done | +16 | (pending) |
|
||||
| 37 | Tokenizer-as-API | done | +17 | 54b54f4e |
|
||||
| 38 | SourceInfo API | design-done | `plans/designs/e38-sourceinfo.md` |
|
||||
| 39 | WebWorker plugin | design-done | `plans/designs/e39-webworker.md` |
|
||||
@@ -99,7 +99,7 @@ Defer until A–D drain. Estimated ~25 recoverable tests.
|
||||
| B | 7 | 0 | 0 | 0 | 0 | — | 7 |
|
||||
| C | 4 | 1 | 0 | 0 | 0 | — | 5 |
|
||||
| D | 2 | 2 | 0 | 0 | 1 | — | 5 |
|
||||
| E | 2 | 0 | 0 | 0 | 0 | 3 | 5 |
|
||||
| E | 3 | 0 | 0 | 0 | 0 | 2 | 5 |
|
||||
| F | — | — | — | ~10 | — | — | ~10 |
|
||||
|
||||
## Maintenance
|
||||
|
||||
@@ -1120,7 +1120,13 @@
|
||||
(hs-activate! _el-input)
|
||||
))
|
||||
(deftest "unsupported element: bind to plain div errors"
|
||||
(error "SKIP (untranslated): unsupported element: bind to plain div errors"))
|
||||
(hs-cleanup!)
|
||||
(let ((_el (dom-create-element "div")))
|
||||
(dom-set-attr _el "_" "bind $nope to me")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(assert (nil? (host-get (host-global "window") "$nope"))))
|
||||
)
|
||||
(deftest "variable drives class: setting variable adds/removes class"
|
||||
(hs-cleanup!)
|
||||
(let ((_el-div (dom-create-element "div")))
|
||||
@@ -3759,7 +3765,25 @@
|
||||
))
|
||||
)
|
||||
(deftest "converts a NodeList into HTML"
|
||||
(error "SKIP (untranslated): converts a NodeList into HTML"))
|
||||
(let ((_frag (host-call (dom-document) "createDocumentFragment")))
|
||||
(let ((_d (dom-create-element "div")))
|
||||
(do
|
||||
(host-set! _d "id" "first")
|
||||
(host-set! _d "innerText" "With Text")
|
||||
(dom-append _frag _d)
|
||||
(let ((_span (dom-create-element "span")))
|
||||
(do
|
||||
(host-set! _span "id" "second")
|
||||
(dom-append _frag _span)
|
||||
(let ((_i (dom-create-element "i")))
|
||||
(do
|
||||
(host-set! _i "id" "third")
|
||||
(dom-append _frag _i)
|
||||
(let ((_nodeList (host-get _frag "childNodes")))
|
||||
(assert=
|
||||
(eval-hs-locals "nodeList as HTML" (list (list (quote nodeList) _nodeList)))
|
||||
"<div id=\"first\">With Text</div><span id=\"second\"></span><i id=\"third\"></i>")))))))))
|
||||
)
|
||||
(deftest "converts a complete form into Values"
|
||||
(let ((_node (dom-create-element "form")))
|
||||
(dom-set-inner-html _node "<div><span><b> Catches elements nested deeply within the DOM tree <input name=\"firstName\" value=\"John\"><br> <input name=\"lastName\" value=\"Connor\"><br> <input name=\"phone\" value=\"555-1212\"> </b></span></div> Works with Textareas <textarea name=\"aboutMe\">It began on a warm summer day in 1969...</textarea> Works with Single Select Boxes <select name=\"animal\"> <option value=\"dog\" selected>Doggo</option> <option value=\"cat\">Kitteh</option> <option value=\"raccoon\">Trash Panda</option> <option value=\"possum\">Sleepy Boi</option> </select> Works with Multi-Select Boxes <select name=\"spiritAnimal\" multiple> <option value=\"dog\" selected>Doggo</option> <option value=\"cat\">Kitteh</option> <option value=\"raccoon\" selected>Trash Panda</option> <option value=\"possum\">Sleepy Boi</option> </select> Works with Radio Buttons <input type=\"radio\" name=\"coolOrNaw\" value=\"Cool\" checked> <input type=\"radio\" name=\"coolOrNaw\" value=\"Naw Bruh\"> Works with Checkboxes <input type=\"checkbox\" name=\"gender\" value=\"Male\" checked> <input type=\"checkbox\" name=\"gender\" value=\"Female\" checked> <input type=\"checkbox\" name=\"gender\" value=\"Other\" checked>")
|
||||
@@ -3858,7 +3882,14 @@
|
||||
(assert= (eval-hs "[1,2,2,3,3] as Unique") (list 1 2 3))
|
||||
)
|
||||
(deftest "converts arrays into fragments"
|
||||
(error "SKIP (untranslated): converts arrays into fragments"))
|
||||
(let ((_p (dom-create-element "p")))
|
||||
(let ((_arr (list _p "<p></p>")))
|
||||
(let ((_r (eval-hs-locals "value as Fragment" (list (list (quote value) _arr)))))
|
||||
(do
|
||||
(assert= (len (host-get _r "children")) 2)
|
||||
(assert= (host-get (nth (host-get _r "children") 0) "tagName") "P")
|
||||
(assert= (host-get (nth (host-get _r "children") 1) "tagName") "P")))))
|
||||
)
|
||||
(deftest "converts checkboxes into a Value correctly"
|
||||
(let ((_node (dom-create-element "form")))
|
||||
(dom-set-inner-html _node "<div> <input type=\"checkbox\" name=\"gender\" value=\"Male\" checked> <input type=\"checkbox\" name=\"gender\" value=\"Female\" checked> <input type=\"checkbox\" name=\"gender\" value=\"Other\" checked> </div>")
|
||||
@@ -3869,7 +3900,12 @@
|
||||
))
|
||||
)
|
||||
(deftest "converts elements into fragments"
|
||||
(error "SKIP (untranslated): converts elements into fragments"))
|
||||
(let ((_p (dom-create-element "p")))
|
||||
(let ((_r (eval-hs-locals "value as Fragment" (list (list (quote value) _p)))))
|
||||
(do
|
||||
(assert= (len (host-get _r "children")) 1)
|
||||
(assert= (host-get (first (host-get _r "children")) "tagName") "P"))))
|
||||
)
|
||||
(deftest "converts multiple selects into a Value correctly"
|
||||
(let ((_node (dom-create-element "form")))
|
||||
(dom-set-inner-html _node "<select name=\"animal\" multiple> <option value=\"dog\" selected>Doggo</option> <option value=\"cat\">Kitteh</option> <option value=\"raccoon\" selected>Trash Panda</option> <option value=\"possum\">Sleepy Boi</option> </select>")
|
||||
@@ -3922,7 +3958,11 @@
|
||||
(assert= (host-get (eval-hs "'{\"foo\":\"bar\"}' as Object") "foo") "bar")
|
||||
)
|
||||
(deftest "converts strings into fragments"
|
||||
(error "SKIP (untranslated): converts strings into fragments"))
|
||||
(let ((_r (eval-hs-locals "value as Fragment" (list (list (quote value) "<p></p>")))))
|
||||
(do
|
||||
(assert= (len (host-get _r "children")) 1)
|
||||
(assert= (host-get (first (host-get _r "children")) "tagName") "P")))
|
||||
)
|
||||
(deftest "converts value as Boolean"
|
||||
(assert= (eval-hs "1 as Boolean") true)
|
||||
(assert= (eval-hs "0 as Boolean") false)
|
||||
@@ -12073,40 +12113,382 @@
|
||||
)
|
||||
|
||||
;; ── socket (16 tests) ──
|
||||
(defsuite "hs-upstream-socket"
|
||||
(deftest "converts relative URL to ws:// on http pages"
|
||||
(error "SKIP (untranslated): converts relative URL to ws:// on http pages"))
|
||||
(deftest "converts relative URL to wss:// on https pages"
|
||||
(error "SKIP (untranslated): converts relative URL to wss:// on https pages"))
|
||||
(deftest "dispatchEvent sends JSON-encoded event over the socket"
|
||||
(error "SKIP (untranslated): dispatchEvent sends JSON-encoded event over the socket"))
|
||||
(deftest "namespaced sockets work"
|
||||
(error "SKIP (untranslated): namespaced sockets work"))
|
||||
(deftest "on message as JSON handler decodes JSON payload"
|
||||
(error "SKIP (untranslated): on message as JSON handler decodes JSON payload"))
|
||||
(deftest "on message as JSON throws on non-JSON payload"
|
||||
(error "SKIP (untranslated): on message as JSON throws on non-JSON payload"))
|
||||
(deftest "on message handler fires on incoming text message"
|
||||
(error "SKIP (untranslated): on message handler fires on incoming text message"))
|
||||
(deftest "parses socket with absolute ws:// URL"
|
||||
(error "SKIP (untranslated): parses socket with absolute ws:// URL"))
|
||||
(deftest "rpc proxy blacklists then/catch/length/toJSON"
|
||||
(error "SKIP (untranslated): rpc proxy blacklists then/catch/length/toJSON"))
|
||||
(deftest "rpc proxy default timeout rejects the promise"
|
||||
(error "SKIP (untranslated): rpc proxy default timeout rejects the promise"))
|
||||
(deftest "rpc proxy noTimeout avoids timeout rejection"
|
||||
(error "SKIP (untranslated): rpc proxy noTimeout avoids timeout rejection"))
|
||||
(deftest "rpc proxy reply with throw rejects the promise"
|
||||
(error "SKIP (untranslated): rpc proxy reply with throw rejects the promise"))
|
||||
(deftest "rpc proxy sends a message and resolves the reply"
|
||||
(error "SKIP (untranslated): rpc proxy sends a message and resolves the reply"))
|
||||
(deftest "rpc proxy timeout(n) rejects after a custom window"
|
||||
(error "SKIP (untranslated): rpc proxy timeout(n) rejects after a custom window"))
|
||||
(deftest "rpc reconnects after the underlying socket closes"
|
||||
(error "SKIP (untranslated): rpc reconnects after the underlying socket closes"))
|
||||
(deftest "with timeout parses and uses the configured timeout"
|
||||
(error "SKIP (untranslated): with timeout parses and uses the configured timeout"))
|
||||
)
|
||||
(defsuite
|
||||
"hs-upstream-socket"
|
||||
(deftest
|
||||
"converts relative URL to ws:// on http pages"
|
||||
(hs-cleanup!)
|
||||
(host-set! (host-global "globalThis") "__hs_ws_created" nil)
|
||||
(let
|
||||
((_el (dom-create-element "div")))
|
||||
(dom-set-attr _el "_" "socket _T1Sock \"/ws\" end")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(let
|
||||
((_created (host-get (host-global "globalThis") "__hs_ws_created")))
|
||||
(assert= (host-get (host-get _created 0) "url") "ws://localhost/ws"))))
|
||||
(deftest
|
||||
"converts relative URL to wss:// on https pages"
|
||||
(hs-cleanup!)
|
||||
(host-set! (host-global "globalThis") "__hs_ws_created" nil)
|
||||
(let
|
||||
((_orig-proto (host-get (host-global "location") "protocol"))
|
||||
(_orig-host (host-get (host-global "location") "host")))
|
||||
(do
|
||||
(host-set! (host-global "location") "protocol" "https:")
|
||||
(host-set! (host-global "location") "host" "secure.example.com")
|
||||
(let
|
||||
((_el (dom-create-element "div")))
|
||||
(dom-set-attr _el "_" "socket _T2Sock \"/wss-test\" end")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(let
|
||||
((_url (host-get (host-get (host-get (host-global "globalThis") "__hs_ws_created") 0) "url")))
|
||||
(do
|
||||
(host-set! (host-global "location") "protocol" _orig-proto)
|
||||
(host-set! (host-global "location") "host" _orig-host)
|
||||
(assert= _url "wss://secure.example.com/wss-test")))))))
|
||||
(deftest
|
||||
"dispatchEvent sends JSON-encoded event over the socket"
|
||||
(hs-cleanup!)
|
||||
(host-set! (host-global "globalThis") "__hs_ws_created" nil)
|
||||
(let
|
||||
((_el (dom-create-element "div")))
|
||||
(dom-set-attr _el "_" "socket _T3Sock \"/ws\" end")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(let
|
||||
((_wrapper (host-get (host-global "window") "_T3Sock"))
|
||||
(_ws
|
||||
(host-get
|
||||
(host-get (host-global "globalThis") "__hs_ws_created")
|
||||
0)))
|
||||
(let
|
||||
((_evt (host-new "Object")))
|
||||
(host-set! _evt "type" "greet")
|
||||
(let
|
||||
((_detail (host-new "Object")))
|
||||
(host-set! _detail "name" "world")
|
||||
(host-set! _detail "sender" "ignored")
|
||||
(host-set! _evt "detail" _detail)
|
||||
(host-call-fn (host-get _wrapper "dispatchEvent") (list _evt))
|
||||
(let
|
||||
((_msg (json-parse (host-get (host-get _ws "_sent") 0))))
|
||||
(do
|
||||
(assert= (host-get _msg "type") "greet")
|
||||
(assert= (host-get _msg "name") "world"))))))))
|
||||
(deftest
|
||||
"namespaced sockets work"
|
||||
(hs-cleanup!)
|
||||
(host-set! (host-global "globalThis") "__hs_ws_created" nil)
|
||||
(let
|
||||
((_el (dom-create-element "div")))
|
||||
(dom-set-attr _el "_" "socket _T4App.Chat \"/ws\" end")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(let
|
||||
((_ns (host-get (host-global "window") "_T4App")))
|
||||
(do
|
||||
(assert (not (nil? _ns)))
|
||||
(assert (not (nil? (host-get _ns "Chat"))))))))
|
||||
(deftest
|
||||
"on message as JSON handler decodes JSON payload"
|
||||
(hs-cleanup!)
|
||||
(host-set! (host-global "globalThis") "__hs_ws_created" nil)
|
||||
(host-set! (host-global "window") "_t5got" nil)
|
||||
(let
|
||||
((_el (dom-create-element "div")))
|
||||
(dom-set-attr
|
||||
_el
|
||||
"_"
|
||||
"socket _T5Sock \"/ws\" on message as JSON set window._t5got to the event end")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(let
|
||||
((_ws (host-get (host-get (host-global "globalThis") "__hs_ws_created") 0)))
|
||||
(let
|
||||
((_handler (host-get _ws "onmessage")))
|
||||
(let
|
||||
((_evt (host-new "Object")))
|
||||
(host-set! _evt "data" "{\"greeting\":\"hello\"}")
|
||||
(host-call-fn _handler (list _evt))
|
||||
(assert=
|
||||
(host-get
|
||||
(host-get (host-global "window") "_t5got")
|
||||
"greeting")
|
||||
"hello"))))))
|
||||
(deftest
|
||||
"on message as JSON throws on non-JSON payload"
|
||||
(hs-cleanup!)
|
||||
(host-set! (host-global "globalThis") "__hs_ws_created" nil)
|
||||
(host-set! (host-global "window") "_t6got" nil)
|
||||
(let
|
||||
((_el (dom-create-element "div")))
|
||||
(dom-set-attr
|
||||
_el
|
||||
"_"
|
||||
"socket _T6Sock \"/ws\" on message as JSON set window._t6got to the event end")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(let
|
||||
((_ws (host-get (host-get (host-global "globalThis") "__hs_ws_created") 0)))
|
||||
(let
|
||||
((_handler (host-get _ws "onmessage")))
|
||||
(let
|
||||
((_evt (host-new "Object")))
|
||||
(host-set! _evt "data" "not-valid-json")
|
||||
(host-call-fn _handler (list _evt))
|
||||
(assert (nil? (host-get (host-global "window") "_t6got"))))))))
|
||||
(deftest
|
||||
"on message handler fires on incoming text message"
|
||||
(hs-cleanup!)
|
||||
(host-set! (host-global "globalThis") "__hs_ws_created" nil)
|
||||
(host-set! (host-global "window") "_t7got" nil)
|
||||
(let
|
||||
((_el (dom-create-element "div")))
|
||||
(dom-set-attr
|
||||
_el
|
||||
"_"
|
||||
"socket _T7Sock \"/ws\" on message set window._t7got to the event.data end")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(let
|
||||
((_ws (host-get (host-get (host-global "globalThis") "__hs_ws_created") 0)))
|
||||
(let
|
||||
((_handler (host-get _ws "onmessage")))
|
||||
(let
|
||||
((_evt (host-new "Object")))
|
||||
(host-set! _evt "data" "hello")
|
||||
(host-call-fn _handler (list _evt))
|
||||
(assert= (host-get (host-global "window") "_t7got") "hello"))))))
|
||||
(deftest
|
||||
"parses socket with absolute ws:// URL"
|
||||
(hs-cleanup!)
|
||||
(host-set! (host-global "globalThis") "__hs_ws_created" nil)
|
||||
(let
|
||||
((_el (dom-create-element "div")))
|
||||
(dom-set-attr _el "_" "socket _T8Sock \"ws://example.com/ws\" end")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(let
|
||||
((_created (host-get (host-global "globalThis") "__hs_ws_created")))
|
||||
(assert=
|
||||
(host-get (host-get _created 0) "url")
|
||||
"ws://example.com/ws"))))
|
||||
(deftest
|
||||
"rpc proxy blacklists then/catch/length/toJSON"
|
||||
(hs-cleanup!)
|
||||
(host-set! (host-global "globalThis") "__hs_ws_created" nil)
|
||||
(let
|
||||
((_el (dom-create-element "div")))
|
||||
(dom-set-attr _el "_" "socket _T9Sock \"ws://localhost/ws\" end")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(let
|
||||
((_rpc (host-get (host-get (host-global "window") "_T9Sock") "rpc")))
|
||||
(do
|
||||
(assert (nil? (host-get _rpc "then")))
|
||||
(assert (nil? (host-get _rpc "catch")))
|
||||
(assert (nil? (host-get _rpc "length")))
|
||||
(assert (nil? (host-get _rpc "toJSON")))))))
|
||||
(deftest
|
||||
"rpc proxy default timeout rejects the promise"
|
||||
(hs-cleanup!)
|
||||
(host-set! (host-global "globalThis") "__hs_ws_created" nil)
|
||||
(let
|
||||
((_el (dom-create-element "div")))
|
||||
(dom-set-attr _el "_" "socket _T10Sock \"ws://localhost/ws\" end")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(let
|
||||
((_wrapper (host-get (host-global "window") "_T10Sock"))
|
||||
(_ws
|
||||
(host-get
|
||||
(host-get (host-global "globalThis") "__hs_ws_created")
|
||||
0))
|
||||
(_orig-st (host-global "setTimeout")))
|
||||
(do
|
||||
(host-set!
|
||||
(host-global "globalThis")
|
||||
"setTimeout"
|
||||
(host-callback (fn (thunk ms) (host-call-fn thunk (list)))))
|
||||
(host-call-fn
|
||||
(host-get (host-get _wrapper "rpc") "greet")
|
||||
(list "world"))
|
||||
(host-set! (host-global "globalThis") "setTimeout" _orig-st)
|
||||
(let
|
||||
((_sent-str (host-get (host-get _ws "_sent") 0)))
|
||||
(let
|
||||
((_iid (host-get (json-parse _sent-str) "iid")))
|
||||
(assert (nil? (host-get (host-get _wrapper "_pending") _iid)))))))))
|
||||
(deftest
|
||||
"rpc proxy noTimeout avoids timeout rejection"
|
||||
(hs-cleanup!)
|
||||
(host-set! (host-global "globalThis") "__hs_ws_created" nil)
|
||||
(let
|
||||
((_el (dom-create-element "div")))
|
||||
(dom-set-attr _el "_" "socket _T11Sock \"ws://localhost/ws\" end")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(let
|
||||
((_wrapper (host-get (host-global "window") "_T11Sock"))
|
||||
(_st-calls 0)
|
||||
(_orig-st (host-global "setTimeout")))
|
||||
(do
|
||||
(host-set!
|
||||
(host-global "globalThis")
|
||||
"setTimeout"
|
||||
(host-callback
|
||||
(fn (thunk ms) (set! _st-calls (+ _st-calls 1)))))
|
||||
(let
|
||||
((_no-timeout-proxy (host-get (host-get _wrapper "rpc") "noTimeout")))
|
||||
(host-call-fn
|
||||
(host-get _no-timeout-proxy "greet")
|
||||
(list "world")))
|
||||
(host-set! (host-global "globalThis") "setTimeout" _orig-st)
|
||||
(assert= _st-calls 0)))))
|
||||
(deftest
|
||||
"rpc proxy reply with throw rejects the promise"
|
||||
(hs-cleanup!)
|
||||
(host-set! (host-global "globalThis") "__hs_ws_created" nil)
|
||||
(let
|
||||
((_el (dom-create-element "div")))
|
||||
(dom-set-attr _el "_" "socket _T12Sock \"ws://localhost/ws\" end")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(let
|
||||
((_wrapper (host-get (host-global "window") "_T12Sock"))
|
||||
(_ws
|
||||
(host-get
|
||||
(host-get (host-global "globalThis") "__hs_ws_created")
|
||||
0)))
|
||||
(do
|
||||
(host-call-fn
|
||||
(host-get (host-get _wrapper "rpc") "greet")
|
||||
(list "world"))
|
||||
(let
|
||||
((_iid (host-get (json-parse (host-get (host-get _ws "_sent") 0)) "iid")))
|
||||
(let
|
||||
((_reply (host-new "Object")))
|
||||
(host-set! _reply "iid" _iid)
|
||||
(host-set! _reply "throw" "boom")
|
||||
(let
|
||||
((_handler (host-get _ws "onmessage")))
|
||||
(let
|
||||
((_evt (host-new "Object")))
|
||||
(host-set!
|
||||
_evt
|
||||
"data"
|
||||
(host-call (host-global "JSON") "stringify" _reply))
|
||||
(host-call-fn _handler (list _evt))
|
||||
(assert
|
||||
(nil? (host-get (host-get _wrapper "_pending") _iid)))))))))))
|
||||
(deftest
|
||||
"rpc proxy sends a message and resolves the reply"
|
||||
(hs-cleanup!)
|
||||
(host-set! (host-global "globalThis") "__hs_ws_created" nil)
|
||||
(let
|
||||
((_el (dom-create-element "div")))
|
||||
(dom-set-attr _el "_" "socket _T13Sock \"ws://localhost/ws\" end")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(let
|
||||
((_wrapper (host-get (host-global "window") "_T13Sock"))
|
||||
(_ws
|
||||
(host-get
|
||||
(host-get (host-global "globalThis") "__hs_ws_created")
|
||||
0)))
|
||||
(do
|
||||
(host-call-fn
|
||||
(host-get (host-get _wrapper "rpc") "greet")
|
||||
(list "world"))
|
||||
(let
|
||||
((_sent (json-parse (host-get (host-get _ws "_sent") 0))))
|
||||
(do
|
||||
(assert= (host-get _sent "function") "greet")
|
||||
(let
|
||||
((_iid (host-get _sent "iid")))
|
||||
(let
|
||||
((_reply (host-new "Object")))
|
||||
(host-set! _reply "iid" _iid)
|
||||
(host-set! _reply "return" "got it")
|
||||
(let
|
||||
((_handler (host-get _ws "onmessage")))
|
||||
(let
|
||||
((_evt (host-new "Object")))
|
||||
(host-set!
|
||||
_evt
|
||||
"data"
|
||||
(host-call (host-global "JSON") "stringify" _reply))
|
||||
(host-call-fn _handler (list _evt))
|
||||
(assert
|
||||
(nil? (host-get (host-get _wrapper "_pending") _iid)))))))))))))
|
||||
(deftest
|
||||
"rpc proxy timeout(n) rejects after a custom window"
|
||||
(hs-cleanup!)
|
||||
(host-set! (host-global "globalThis") "__hs_ws_created" nil)
|
||||
(let
|
||||
((_el (dom-create-element "div")))
|
||||
(dom-set-attr _el "_" "socket _T14Sock \"ws://localhost/ws\" end")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(let
|
||||
((_wrapper (host-get (host-global "window") "_T14Sock"))
|
||||
(_ws
|
||||
(host-get
|
||||
(host-get (host-global "globalThis") "__hs_ws_created")
|
||||
0))
|
||||
(_orig-st (host-global "setTimeout")))
|
||||
(do
|
||||
(host-set!
|
||||
(host-global "globalThis")
|
||||
"setTimeout"
|
||||
(host-callback (fn (thunk ms) (host-call-fn thunk (list)))))
|
||||
(let
|
||||
((_t100-fn (host-call-fn (host-get (host-get _wrapper "rpc") "timeout") (list 100))))
|
||||
(host-call-fn (host-get _t100-fn "greet") (list "world")))
|
||||
(host-set! (host-global "globalThis") "setTimeout" _orig-st)
|
||||
(let
|
||||
((_iid (host-get (json-parse (host-get (host-get _ws "_sent") 0)) "iid")))
|
||||
(assert (nil? (host-get (host-get _wrapper "_pending") _iid))))))))
|
||||
(deftest
|
||||
"rpc reconnects after the underlying socket closes"
|
||||
(hs-cleanup!)
|
||||
(host-set! (host-global "globalThis") "__hs_ws_created" nil)
|
||||
(let
|
||||
((_el (dom-create-element "div")))
|
||||
(dom-set-attr _el "_" "socket _T15Sock \"ws://localhost/ws\" end")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(let
|
||||
((_wrapper (host-get (host-global "window") "_T15Sock"))
|
||||
(_ws
|
||||
(host-get
|
||||
(host-get (host-global "globalThis") "__hs_ws_created")
|
||||
0)))
|
||||
(do
|
||||
(host-call _ws "close")
|
||||
(host-call-fn
|
||||
(host-get (host-get _wrapper "rpc") "greet")
|
||||
(list "world"))
|
||||
(let
|
||||
((_created (host-get (host-global "globalThis") "__hs_ws_created")))
|
||||
(assert= (host-get _created "length") 2))))))
|
||||
(deftest
|
||||
"with timeout parses and uses the configured timeout"
|
||||
(hs-cleanup!)
|
||||
(host-set! (host-global "globalThis") "__hs_ws_created" nil)
|
||||
(let
|
||||
((_el (dom-create-element "div")))
|
||||
(dom-set-attr
|
||||
_el
|
||||
"_"
|
||||
"socket _T16Sock \"ws://localhost/ws\" with timeout 1500 end")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(let
|
||||
((_wrapper (host-get (host-global "window") "_T16Sock")))
|
||||
(assert= (host-get _wrapper "_timeout") 1500)))))
|
||||
|
||||
;; ── swap (4 tests) ──
|
||||
(defsuite "hs-upstream-swap"
|
||||
@@ -14062,7 +14444,13 @@ end")
|
||||
(hs-activate! _el-d2)
|
||||
))
|
||||
(deftest "local variable in when expression produces a parse error"
|
||||
(error "SKIP (untranslated): local variable in when expression produces a parse error"))
|
||||
(hs-cleanup!)
|
||||
(let ((_el (dom-create-element "div")))
|
||||
(dom-set-attr _el "_" "when myVar changes put it into me")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(assert= (first (hs-compile "when myVar changes put it into me")) (quote when-feat-no-op)))
|
||||
)
|
||||
(deftest "math on tracked symbols works"
|
||||
(hs-cleanup!)
|
||||
(let ((_el-div (dom-create-element "div")))
|
||||
|
||||
@@ -555,7 +555,84 @@ class HsIntersectionObserver {
|
||||
}
|
||||
globalThis.IntersectionObserver = HsIntersectionObserver;
|
||||
globalThis.IntersectionObserverEntry = class {};
|
||||
globalThis.navigator={userAgent:'node'}; globalThis.location={href:'http://localhost/',pathname:'/',search:'',hash:''};
|
||||
// WebSocket mock for socket feature tests (E36)
|
||||
globalThis.WebSocket = function HsWebSocket(url) {
|
||||
const sock = {
|
||||
url, readyState: 1, onmessage: null, onclose: null, onerror: null, onopen: null,
|
||||
_listeners: {}, _sent: [],
|
||||
send(msg) { sock._sent.push(msg); },
|
||||
addEventListener(t, h) { (sock._listeners[t] = sock._listeners[t] || []).push(h); },
|
||||
removeEventListener(t, h) { if (sock._listeners[t]) sock._listeners[t] = sock._listeners[t].filter(x => x !== h); },
|
||||
close() { sock.readyState = 3; (sock._listeners['close'] || []).forEach(h => h({})); if (sock.onclose) sock.onclose({}); }
|
||||
};
|
||||
globalThis.__hs_ws_created = globalThis.__hs_ws_created || [];
|
||||
globalThis.__hs_ws_created.push(sock);
|
||||
return sock;
|
||||
};
|
||||
globalThis.WebSocket.CONNECTING = 0; globalThis.WebSocket.OPEN = 1; globalThis.WebSocket.CLOSING = 2; globalThis.WebSocket.CLOSED = 3;
|
||||
var _iidCounter = 0;
|
||||
function _hsRpcCall(wrapper, fnName, args, timeout) {
|
||||
if (wrapper._closed) {
|
||||
const ws2 = new (wrapper._WS || globalThis.WebSocket)(wrapper._url);
|
||||
wrapper._ws = ws2; wrapper._closed = false;
|
||||
if (wrapper._onmessage_handler) ws2.onmessage = wrapper._onmessage_handler;
|
||||
ws2.addEventListener('close', () => { wrapper._closed = true; });
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const iid = String(++_iidCounter);
|
||||
const ws = wrapper._ws;
|
||||
if (!wrapper._pending) wrapper._pending = {};
|
||||
wrapper._pending[iid] = { resolve, reject };
|
||||
if (ws && ws.send) ws.send(JSON.stringify({ iid, function: fnName, args }));
|
||||
if (timeout !== Infinity && timeout != null) {
|
||||
setTimeout(() => {
|
||||
if (wrapper._pending && wrapper._pending[iid]) {
|
||||
delete wrapper._pending[iid];
|
||||
reject('Timed out');
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
function _hsMakeRpcProxy(wrapper, overrides) {
|
||||
overrides = overrides || {};
|
||||
// The OCaml WASM kernel cannot store values created inside a JS Proxy's get trap —
|
||||
// they arrive as nil. Use a dispatch-object pattern instead: host-get detects
|
||||
// _hsRpcDispatch and calls it directly, bypassing Proxy trap issues.
|
||||
const rpc = function() {};
|
||||
rpc._hsRpcDispatch = function(name) {
|
||||
name = String(name);
|
||||
if (['then', 'catch', 'length', 'toJSON'].includes(name)) return null;
|
||||
if (name === 'noTimeout') return _hsMakeRpcProxy(wrapper, Object.assign({}, overrides, { timeout: Infinity }));
|
||||
if (name === 'timeout') return function(n) { return _hsMakeRpcProxy(wrapper, Object.assign({}, overrides, { timeout: n })); };
|
||||
const t = overrides.timeout !== undefined ? overrides.timeout : (wrapper._timeout != null ? wrapper._timeout : 0);
|
||||
return function() { return _hsRpcCall(wrapper, name, Array.from(arguments), t); };
|
||||
};
|
||||
return rpc;
|
||||
}
|
||||
globalThis._hs_make_rpc_proxy = _hsMakeRpcProxy;
|
||||
function _hsSetupSocket(wrapper) {
|
||||
wrapper.dispatchEvent = function(evt) {
|
||||
if (wrapper._closed) {
|
||||
const ws2 = new (wrapper._WS || globalThis.WebSocket)(wrapper._url);
|
||||
wrapper._ws = ws2; wrapper._closed = false;
|
||||
if (wrapper._onmessage_handler) ws2.onmessage = wrapper._onmessage_handler;
|
||||
ws2.addEventListener('close', () => { wrapper._closed = true; });
|
||||
}
|
||||
const ws = wrapper._ws;
|
||||
if (!ws) return;
|
||||
const payload = { type: evt.type };
|
||||
const detail = evt.detail || {};
|
||||
for (const k of Object.keys(detail)) {
|
||||
if (k !== 'sender' && k !== '_namedArgList_' && k !== '_type') payload[k] = detail[k];
|
||||
}
|
||||
ws.send(JSON.stringify(payload));
|
||||
};
|
||||
wrapper.rpc = _hsMakeRpcProxy(wrapper, {});
|
||||
return wrapper;
|
||||
}
|
||||
globalThis._hsSetupSocket = _hsSetupSocket;
|
||||
globalThis.navigator={userAgent:'node'}; globalThis.location={href:'http://localhost/',pathname:'/',search:'',hash:'',protocol:'http:',host:'localhost',hostname:'localhost',port:''};
|
||||
globalThis.history={pushState(){},replaceState(){},back(){},forward(){}};
|
||||
globalThis.getSelection=()=>({toString:()=>(globalThis.__test_selection||'')});
|
||||
const _origLog = console.log;
|
||||
@@ -573,9 +650,12 @@ K.registerNative('host-get',a=>{
|
||||
// through JS property access. Hand-roll common collection queries so
|
||||
// compiled HS `x.length` / `x.size` works on scoped lists.
|
||||
if(a[0] && a[0]._type==='list' && (a[1]==='length' || a[1]==='size')) return a[0].items.length;
|
||||
if(a[0] && a[0]._type==='list' && typeof a[1]==='number') return a[0].items[a[1]]!==undefined?a[0].items[a[1]]:null;
|
||||
if(a[0] && a[0]._type==='dict' && a[1]==='size') return Object.keys(a[0]).filter(k=>k!=='_type').length;
|
||||
// innerText is DOM-level alias for textContent (close enough for mock purposes)
|
||||
if(a[0] instanceof El && a[1]==='innerText') return String(a[0].textContent||'');
|
||||
// RPC dispatch object: _hsRpcDispatch bypasses Proxy-in-WASM-kernel nil issue
|
||||
if(a[0] && typeof a[0]._hsRpcDispatch==='function'){const rv=a[0]._hsRpcDispatch(String(a[1]));return rv===undefined?null:rv;}
|
||||
let v=a[0][a[1]];
|
||||
if(v===undefined)return null;
|
||||
// Only coerce DOM property strings for actual DOM elements — plain JS objects
|
||||
@@ -843,6 +923,7 @@ for(let i=startTest;i<Math.min(endTest,testCount);i++){
|
||||
"hs-upstream-core/runtimeErrors",
|
||||
"hs-upstream-expressions/collectionExpressions",
|
||||
"hs-upstream-expressions/typecheck",
|
||||
"hs-upstream-socket",
|
||||
]);
|
||||
// Enable step limit for timeout protection — reset counter first so accumulation
|
||||
// across tests doesn't cause signed-32-bit wraparound (~2B extra steps before limit fires).
|
||||
@@ -853,13 +934,26 @@ for(let i=startTest;i<Math.min(endTest,testCount);i++){
|
||||
"async hypertrace is reasonable": 8000,
|
||||
"hypertrace from javascript is reasonable": 8000,
|
||||
"hypertrace is reasonable": 8000,
|
||||
"passes the sieve test": 60000,
|
||||
"passes the sieve test": 180000,
|
||||
"behavior scoping is isolated from other behaviors": 60000,
|
||||
"behavior scoping is isolated from the core element scope": 60000,
|
||||
// repeat suite: two JIT preheat calls each take 7-12s cold
|
||||
"can nest loops": 60000,
|
||||
"only executes the init expression once": 60000,
|
||||
"repeat forever works": 60000,
|
||||
"repeat forever works w/o keyword": 60000,
|
||||
"until keyword works": 60000,
|
||||
"while keyword works": 60000,
|
||||
};
|
||||
const _SLOW_DEADLINE_SUITES = {
|
||||
"hs-upstream-core/runtimeErrors": 30000,
|
||||
"hs-upstream-expressions/collectionExpressions": 60000,
|
||||
"hs-upstream-expressions/typecheck": 30000,
|
||||
"hs-upstream-behavior": 20000,
|
||||
// eventsource: JIT saturation after multiple compilations in suite sequence
|
||||
"hs-upstream-ext/eventsource": 30000,
|
||||
// socket: first call to hs-socket-register! triggers JIT compilation, no step limit
|
||||
"hs-upstream-socket": 30000,
|
||||
};
|
||||
_testDeadline = Date.now() + (_SLOW_DEADLINE[name] || _SLOW_DEADLINE_SUITES[suite] || 10000);
|
||||
globalThis.__hs_deadline = _testDeadline; // expose to WASM cek_step_loop
|
||||
|
||||
@@ -415,6 +415,342 @@ MANUAL_TEST_BODIES = {
|
||||
' (hs-activate! _el))',
|
||||
' (assert (nil? caught))))',
|
||||
],
|
||||
# bind: bind $nope to a plain div does nothing — $nope stays nil
|
||||
"unsupported element: bind to plain div errors": [
|
||||
' (hs-cleanup!)',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "_" "bind $nope to me")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (assert (nil? (host-get (host-global "window") "$nope"))))',
|
||||
],
|
||||
# when: non-attribute reference in when...changes is a parse error (when-feat-no-op)
|
||||
"local variable in when expression produces a parse error": [
|
||||
' (hs-cleanup!)',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "_" "when myVar changes put it into me")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (assert= (first (hs-compile "when myVar changes put it into me")) (quote when-feat-no-op)))',
|
||||
],
|
||||
# asExpression: NodeList as HTML — each element serialised via outerHTML
|
||||
"converts a NodeList into HTML": [
|
||||
' (let ((_frag (host-call (dom-document) "createDocumentFragment")))',
|
||||
' (let ((_d (dom-create-element "div")))',
|
||||
' (do',
|
||||
' (host-set! _d "id" "first")',
|
||||
' (host-set! _d "innerText" "With Text")',
|
||||
' (dom-append _frag _d)',
|
||||
' (let ((_span (dom-create-element "span")))',
|
||||
' (do',
|
||||
' (host-set! _span "id" "second")',
|
||||
' (dom-append _frag _span)',
|
||||
' (let ((_i (dom-create-element "i")))',
|
||||
' (do',
|
||||
' (host-set! _i "id" "third")',
|
||||
' (dom-append _frag _i)',
|
||||
' (let ((_nodeList (host-get _frag "childNodes")))',
|
||||
' (assert=',
|
||||
' (eval-hs-locals "nodeList as HTML" (list (list (quote nodeList) _nodeList)))',
|
||||
' "<div id=\\"first\\">With Text</div><span id=\\"second\\"></span><i id=\\"third\\"></i>")))))))))',
|
||||
],
|
||||
# asExpression: array of [element, html-string] as Fragment
|
||||
"converts arrays into fragments": [
|
||||
' (let ((_p (dom-create-element "p")))',
|
||||
' (let ((_arr (list _p "<p></p>")))',
|
||||
' (let ((_r (eval-hs-locals "value as Fragment" (list (list (quote value) _arr)))))',
|
||||
' (do',
|
||||
' (assert= (len (host-get _r "children")) 2)',
|
||||
' (assert= (host-get (nth (host-get _r "children") 0) "tagName") "P")',
|
||||
' (assert= (host-get (nth (host-get _r "children") 1) "tagName") "P")))))',
|
||||
],
|
||||
# asExpression: single element as Fragment wraps it in a DocumentFragment
|
||||
"converts elements into fragments": [
|
||||
' (let ((_p (dom-create-element "p")))',
|
||||
' (let ((_r (eval-hs-locals "value as Fragment" (list (list (quote value) _p)))))',
|
||||
' (do',
|
||||
' (assert= (len (host-get _r "children")) 1)',
|
||||
' (assert= (host-get (first (host-get _r "children")) "tagName") "P"))))',
|
||||
],
|
||||
# asExpression: HTML string as Fragment — parses and wraps children
|
||||
"converts strings into fragments": [
|
||||
' (let ((_r (eval-hs-locals "value as Fragment" (list (list (quote value) "<p></p>")))))',
|
||||
' (do',
|
||||
' (assert= (len (host-get _r "children")) 1)',
|
||||
' (assert= (host-get (first (host-get _r "children")) "tagName") "P")))',
|
||||
],
|
||||
# socket E36: relative URL normalised to ws:// (http page)
|
||||
"converts relative URL to ws:// on http pages": [
|
||||
' (hs-cleanup!)',
|
||||
' (host-set! (host-global "globalThis") "__hs_ws_created" nil)',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "_" "socket _T1Sock \\"/ws\\" end")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (let ((_created (host-get (host-global "globalThis") "__hs_ws_created")))',
|
||||
' (assert= (host-get (host-get _created 0) "url") "ws://localhost/ws")))',
|
||||
],
|
||||
# socket E36: relative URL normalised to wss:// (https page)
|
||||
"converts relative URL to wss:// on https pages": [
|
||||
' (hs-cleanup!)',
|
||||
' (host-set! (host-global "globalThis") "__hs_ws_created" nil)',
|
||||
' (let ((_orig-proto (host-get (host-global "location") "protocol"))',
|
||||
' (_orig-host (host-get (host-global "location") "host")))',
|
||||
' (do',
|
||||
' (host-set! (host-global "location") "protocol" "https:")',
|
||||
' (host-set! (host-global "location") "host" "secure.example.com")',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "_" "socket _T2Sock \\"/wss-test\\" end")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (let ((_url (host-get (host-get (host-get (host-global "globalThis") "__hs_ws_created") 0) "url")))',
|
||||
' (do',
|
||||
' (host-set! (host-global "location") "protocol" _orig-proto)',
|
||||
' (host-set! (host-global "location") "host" _orig-host)',
|
||||
' (assert= _url "wss://secure.example.com/wss-test"))))))',
|
||||
],
|
||||
# socket E36: dispatchEvent JSON-encodes and sends the event
|
||||
"dispatchEvent sends JSON-encoded event over the socket": [
|
||||
' (hs-cleanup!)',
|
||||
' (host-set! (host-global "globalThis") "__hs_ws_created" nil)',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "_" "socket _T3Sock \\"/ws\\" end")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (let ((_wrapper (host-get (host-global "window") "_T3Sock"))',
|
||||
' (_ws (host-get (host-get (host-global "globalThis") "__hs_ws_created") 0)))',
|
||||
' (let ((_evt (host-new "Object")))',
|
||||
' (host-set! _evt "type" "greet")',
|
||||
' (let ((_detail (host-new "Object")))',
|
||||
' (host-set! _detail "name" "world")',
|
||||
' (host-set! _detail "sender" "ignored")',
|
||||
' (host-set! _evt "detail" _detail)',
|
||||
' (host-call-fn (host-get _wrapper "dispatchEvent") (list _evt))',
|
||||
' (let ((_msg (json-parse (host-get (host-get _ws "_sent") 0))))',
|
||||
' (do',
|
||||
' (assert= (host-get _msg "type") "greet")',
|
||||
' (assert= (host-get _msg "name") "world")))))))',
|
||||
],
|
||||
# socket E36: dotted name creates nested namespace objects
|
||||
"namespaced sockets work": [
|
||||
' (hs-cleanup!)',
|
||||
' (host-set! (host-global "globalThis") "__hs_ws_created" nil)',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "_" "socket _T4App.Chat \\"/ws\\" end")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (let ((_ns (host-get (host-global "window") "_T4App")))',
|
||||
' (do',
|
||||
' (assert (not (nil? _ns)))',
|
||||
' (assert (not (nil? (host-get _ns "Chat")))))))',
|
||||
],
|
||||
# socket E36: on message as JSON — handler receives parsed JSON
|
||||
"on message as JSON handler decodes JSON payload": [
|
||||
' (hs-cleanup!)',
|
||||
' (host-set! (host-global "globalThis") "__hs_ws_created" nil)',
|
||||
' (host-set! (host-global "window") "_t5got" nil)',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "_" "socket _T5Sock \\"/ws\\" on message as JSON set window._t5got to the event end")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (let ((_ws (host-get (host-get (host-global "globalThis") "__hs_ws_created") 0)))',
|
||||
' (let ((_handler (host-get _ws "onmessage")))',
|
||||
' (let ((_evt (host-new "Object")))',
|
||||
' (host-set! _evt "data" "{\\"greeting\\":\\"hello\\"}")',
|
||||
' (host-call-fn _handler (list _evt))',
|
||||
' (assert= (host-get (host-get (host-global "window") "_t5got") "greeting") "hello")))))',
|
||||
],
|
||||
# socket E36: on message as JSON with non-JSON data — handler not called
|
||||
"on message as JSON throws on non-JSON payload": [
|
||||
' (hs-cleanup!)',
|
||||
' (host-set! (host-global "globalThis") "__hs_ws_created" nil)',
|
||||
' (host-set! (host-global "window") "_t6got" nil)',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "_" "socket _T6Sock \\"/ws\\" on message as JSON set window._t6got to the event end")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (let ((_ws (host-get (host-get (host-global "globalThis") "__hs_ws_created") 0)))',
|
||||
' (let ((_handler (host-get _ws "onmessage")))',
|
||||
' (let ((_evt (host-new "Object")))',
|
||||
' (host-set! _evt "data" "not-valid-json")',
|
||||
' (host-call-fn _handler (list _evt))',
|
||||
' (assert (nil? (host-get (host-global "window") "_t6got")))))))',
|
||||
],
|
||||
# socket E36: plain on message fires handler with raw event
|
||||
"on message handler fires on incoming text message": [
|
||||
' (hs-cleanup!)',
|
||||
' (host-set! (host-global "globalThis") "__hs_ws_created" nil)',
|
||||
' (host-set! (host-global "window") "_t7got" nil)',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "_" "socket _T7Sock \\"/ws\\" on message set window._t7got to the event.data end")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (let ((_ws (host-get (host-get (host-global "globalThis") "__hs_ws_created") 0)))',
|
||||
' (let ((_handler (host-get _ws "onmessage")))',
|
||||
' (let ((_evt (host-new "Object")))',
|
||||
' (host-set! _evt "data" "hello")',
|
||||
' (host-call-fn _handler (list _evt))',
|
||||
' (assert= (host-get (host-global "window") "_t7got") "hello")))))',
|
||||
],
|
||||
# socket E36: absolute ws:// URL passes through unchanged
|
||||
"parses socket with absolute ws:// URL": [
|
||||
' (hs-cleanup!)',
|
||||
' (host-set! (host-global "globalThis") "__hs_ws_created" nil)',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "_" "socket _T8Sock \\"ws://example.com/ws\\" end")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (let ((_created (host-get (host-global "globalThis") "__hs_ws_created")))',
|
||||
' (assert= (host-get (host-get _created 0) "url") "ws://example.com/ws")))',
|
||||
],
|
||||
# socket E36: rpc proxy blacklists then/catch/length/toJSON
|
||||
"rpc proxy blacklists then/catch/length/toJSON": [
|
||||
' (hs-cleanup!)',
|
||||
' (host-set! (host-global "globalThis") "__hs_ws_created" nil)',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "_" "socket _T9Sock \\"ws://localhost/ws\\" end")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (let ((_rpc (host-get (host-get (host-global "window") "_T9Sock") "rpc")))',
|
||||
' (do',
|
||||
' (assert (nil? (host-get _rpc "then")))',
|
||||
' (assert (nil? (host-get _rpc "catch")))',
|
||||
' (assert (nil? (host-get _rpc "length")))',
|
||||
' (assert (nil? (host-get _rpc "toJSON"))))))',
|
||||
],
|
||||
# socket E36: rpc default timeout (0ms) fires setTimeout → pending cleared
|
||||
"rpc proxy default timeout rejects the promise": [
|
||||
' (hs-cleanup!)',
|
||||
' (host-set! (host-global "globalThis") "__hs_ws_created" nil)',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "_" "socket _T10Sock \\"ws://localhost/ws\\" end")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (let ((_wrapper (host-get (host-global "window") "_T10Sock"))',
|
||||
' (_ws (host-get (host-get (host-global "globalThis") "__hs_ws_created") 0))',
|
||||
' (_orig-st (host-global "setTimeout")))',
|
||||
' (do',
|
||||
' (host-set! (host-global "globalThis") "setTimeout"',
|
||||
' (host-callback (fn (thunk ms) (host-call-fn thunk (list)))))',
|
||||
' (host-call-fn (host-get (host-get _wrapper "rpc") "greet") (list "world"))',
|
||||
' (host-set! (host-global "globalThis") "setTimeout" _orig-st)',
|
||||
' (let ((_sent-str (host-get (host-get _ws "_sent") 0)))',
|
||||
' (let ((_iid (host-get (json-parse _sent-str) "iid")))',
|
||||
' (assert (nil? (host-get (host-get _wrapper "_pending") _iid))))))))',
|
||||
],
|
||||
# socket E36: noTimeout proxy skips setTimeout entirely
|
||||
"rpc proxy noTimeout avoids timeout rejection": [
|
||||
' (hs-cleanup!)',
|
||||
' (host-set! (host-global "globalThis") "__hs_ws_created" nil)',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "_" "socket _T11Sock \\"ws://localhost/ws\\" end")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (let ((_wrapper (host-get (host-global "window") "_T11Sock"))',
|
||||
' (_st-calls 0)',
|
||||
' (_orig-st (host-global "setTimeout")))',
|
||||
' (do',
|
||||
' (host-set! (host-global "globalThis") "setTimeout"',
|
||||
' (host-callback (fn (thunk ms) (set! _st-calls (+ _st-calls 1)))))',
|
||||
' (let ((_no-timeout-proxy (host-get (host-get _wrapper "rpc") "noTimeout")))',
|
||||
' (host-call-fn (host-get _no-timeout-proxy "greet") (list "world")))',
|
||||
' (host-set! (host-global "globalThis") "setTimeout" _orig-st)',
|
||||
' (assert= _st-calls 0))))',
|
||||
],
|
||||
# socket E36: onmessage with {iid,throw} clears pending entry (reject called)
|
||||
"rpc proxy reply with throw rejects the promise": [
|
||||
' (hs-cleanup!)',
|
||||
' (host-set! (host-global "globalThis") "__hs_ws_created" nil)',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "_" "socket _T12Sock \\"ws://localhost/ws\\" end")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (let ((_wrapper (host-get (host-global "window") "_T12Sock"))',
|
||||
' (_ws (host-get (host-get (host-global "globalThis") "__hs_ws_created") 0)))',
|
||||
' (do',
|
||||
' (host-call-fn (host-get (host-get _wrapper "rpc") "greet") (list "world"))',
|
||||
' (let ((_iid (host-get (json-parse (host-get (host-get _ws "_sent") 0)) "iid")))',
|
||||
' (let ((_reply (host-new "Object")))',
|
||||
' (host-set! _reply "iid" _iid)',
|
||||
' (host-set! _reply "throw" "boom")',
|
||||
' (let ((_handler (host-get _ws "onmessage")))',
|
||||
' (let ((_evt (host-new "Object")))',
|
||||
' (host-set! _evt "data" (host-call (host-global "JSON") "stringify" _reply))',
|
||||
' (host-call-fn _handler (list _evt))',
|
||||
' (assert (nil? (host-get (host-get _wrapper "_pending") _iid))))))))))',
|
||||
],
|
||||
# socket E36: rpc call sends {iid,function,args}; onmessage reply clears pending
|
||||
"rpc proxy sends a message and resolves the reply": [
|
||||
' (hs-cleanup!)',
|
||||
' (host-set! (host-global "globalThis") "__hs_ws_created" nil)',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "_" "socket _T13Sock \\"ws://localhost/ws\\" end")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (let ((_wrapper (host-get (host-global "window") "_T13Sock"))',
|
||||
' (_ws (host-get (host-get (host-global "globalThis") "__hs_ws_created") 0)))',
|
||||
' (do',
|
||||
' (host-call-fn (host-get (host-get _wrapper "rpc") "greet") (list "world"))',
|
||||
' (let ((_sent (json-parse (host-get (host-get _ws "_sent") 0))))',
|
||||
' (do',
|
||||
' (assert= (host-get _sent "function") "greet")',
|
||||
' (let ((_iid (host-get _sent "iid")))',
|
||||
' (let ((_reply (host-new "Object")))',
|
||||
' (host-set! _reply "iid" _iid)',
|
||||
' (host-set! _reply "return" "got it")',
|
||||
' (let ((_handler (host-get _ws "onmessage")))',
|
||||
' (let ((_evt (host-new "Object")))',
|
||||
' (host-set! _evt "data" (host-call (host-global "JSON") "stringify" _reply))',
|
||||
' (host-call-fn _handler (list _evt))',
|
||||
' (assert (nil? (host-get (host-get _wrapper "_pending") _iid))))))))))))',
|
||||
],
|
||||
# socket E36: .timeout(n) proxy fires setTimeout with that delay → pending cleared
|
||||
"rpc proxy timeout(n) rejects after a custom window": [
|
||||
' (hs-cleanup!)',
|
||||
' (host-set! (host-global "globalThis") "__hs_ws_created" nil)',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "_" "socket _T14Sock \\"ws://localhost/ws\\" end")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (let ((_wrapper (host-get (host-global "window") "_T14Sock"))',
|
||||
' (_ws (host-get (host-get (host-global "globalThis") "__hs_ws_created") 0))',
|
||||
' (_orig-st (host-global "setTimeout")))',
|
||||
' (do',
|
||||
' (host-set! (host-global "globalThis") "setTimeout"',
|
||||
' (host-callback (fn (thunk ms) (host-call-fn thunk (list)))))',
|
||||
' (let ((_t100-fn (host-call-fn (host-get (host-get _wrapper "rpc") "timeout") (list 100))))',
|
||||
' (host-call-fn (host-get _t100-fn "greet") (list "world")))',
|
||||
' (host-set! (host-global "globalThis") "setTimeout" _orig-st)',
|
||||
' (let ((_iid (host-get (json-parse (host-get (host-get _ws "_sent") 0)) "iid")))',
|
||||
' (assert (nil? (host-get (host-get _wrapper "_pending") _iid)))))))',
|
||||
],
|
||||
# socket E36: after ws.close(), next RPC lazily creates new WebSocket
|
||||
"rpc reconnects after the underlying socket closes": [
|
||||
' (hs-cleanup!)',
|
||||
' (host-set! (host-global "globalThis") "__hs_ws_created" nil)',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "_" "socket _T15Sock \\"ws://localhost/ws\\" end")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (let ((_wrapper (host-get (host-global "window") "_T15Sock"))',
|
||||
' (_ws (host-get (host-get (host-global "globalThis") "__hs_ws_created") 0)))',
|
||||
' (do',
|
||||
' (host-call _ws "close")',
|
||||
' (host-call-fn (host-get (host-get _wrapper "rpc") "greet") (list "world"))',
|
||||
' (let ((_created (host-get (host-global "globalThis") "__hs_ws_created")))',
|
||||
' (assert= (host-get _created "length") 2)))))',
|
||||
],
|
||||
# socket E36: with timeout N sets wrapper._timeout to N
|
||||
"with timeout parses and uses the configured timeout": [
|
||||
' (hs-cleanup!)',
|
||||
' (host-set! (host-global "globalThis") "__hs_ws_created" nil)',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "_" "socket _T16Sock \\"ws://localhost/ws\\" with timeout 1500 end")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (let ((_wrapper (host-get (host-global "window") "_T16Sock")))',
|
||||
' (assert= (host-get _wrapper "_timeout") 1500)))',
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user