diff --git a/lib/hyperscript/compiler.sx b/lib/hyperscript/compiler.sx index 30297f78..2df6752b 100644 --- a/lib/hyperscript/compiler.sx +++ b/lib/hyperscript/compiler.sx @@ -1838,7 +1838,7 @@ (list (quote fn) (list) (hs-to-sx (nth ast 1))) (list (quote fn) (list) (hs-to-sx (nth ast 2))))) ((= head (quote fetch)) - (list (quote hs-fetch) (hs-to-sx (nth ast 1)) (nth ast 2))) + (list (quote hs-fetch) (hs-to-sx (nth ast 1)) (nth ast 2) (nth ast 3) (quote me))) ((= head (quote fetch-gql)) (list (quote hs-fetch-gql) diff --git a/lib/hyperscript/parser.sx b/lib/hyperscript/parser.sx index 6558e330..9d32b1ff 100644 --- a/lib/hyperscript/parser.sx +++ b/lib/hyperscript/parser.sx @@ -1772,7 +1772,7 @@ ((url (if (and (= (tp-type) "keyword") (= (tp-val) "from")) (do (adv!) (parse-arith (parse-poss (parse-atom)))) nil))) (list (quote fetch-gql) gql-source url)))) (let - ((url-atom (if (and (= (tp-type) "op") (= (tp-val) "/")) (do (adv!) (let ((path-parts (list "/"))) (define read-path (fn () (when (and (not (at-end?)) (or (= (tp-type) "ident") (= (tp-type) "op") (= (tp-type) "dot") (= (tp-type) "number"))) (append! path-parts (tp-val)) (adv!) (read-path)))) (read-path) (join "" path-parts))) (parse-atom)))) + ((url-atom (if (and (= (tp-type) "op") (= (tp-val) "/")) (do (adv!) (let ((path-parts (list "/"))) (define read-path (fn () (when (and (not (at-end?)) (or (and (= (tp-type) "ident") (not (string-contains? (tp-val) "'"))) (= (tp-type) "op") (= (tp-type) "dot") (= (tp-type) "number"))) (append! path-parts (tp-val)) (adv!) (read-path)))) (read-path) (join "" path-parts))) (parse-atom)))) (let ((url (if (nil? url-atom) url-atom (parse-arith (parse-poss url-atom))))) (let @@ -1788,7 +1788,27 @@ ((fmt-after (if (and (not fmt-before) (match-kw "as")) (do (when (and (or (= (tp-type) "ident") (= (tp-type) "keyword")) (or (= (tp-val) "an") (= (tp-val) "a"))) (adv!)) (let ((f (tp-val))) (adv!) f)) nil))) (let ((fmt (or fmt-before fmt-after "text"))) - (list (quote fetch) url fmt))))))))) + (let + ((do-not-throw + (cond + ((and (or (= (tp-type) "keyword") (= (tp-type) "ident")) (= (tp-val) "do")) + (do + (adv!) + (if (and (or (= (tp-type) "keyword") (= (tp-type) "ident")) (= (tp-val) "not")) + (do + (adv!) + (if (and (or (= (tp-type) "keyword") (= (tp-type) "ident")) (= (tp-val) "throw")) + (do (adv!) true) + false)) + false))) + ((and (= (tp-type) "ident") (= (tp-val) "don't")) + (do + (adv!) + (if (and (or (= (tp-type) "keyword") (= (tp-type) "ident")) (= (tp-val) "throw")) + (do (adv!) true) + false))) + (true false)))) + (list (quote fetch) url fmt do-not-throw)))))))))) (define parse-call-args (fn diff --git a/lib/hyperscript/runtime.sx b/lib/hyperscript/runtime.sx index 9ce20df5..133cdc6e 100644 --- a/lib/hyperscript/runtime.sx +++ b/lib/hyperscript/runtime.sx @@ -869,12 +869,33 @@ (define hs-fetch (fn - (url format) + (url format do-not-throw target) (let - ((fmt (cond ((nil? format) "text") ((or (= format "json") (= format "JSON") (= format "Object")) "json") ((or (= format "html") (= format "HTML")) "html") ((or (= format "response") (= format "Response")) "response") ((or (= format "text") (= format "Text")) "text") (true format)))) - (let - ((raw (perform (list "io-fetch" url fmt)))) - (cond ((= fmt "json") (hs-host-to-sx raw)) (true raw)))))) + ((fmt (cond ((nil? format) "text") ((or (= format "json") (= format "JSON") (= format "Object")) "json") ((or (= format "html") (= format "HTML")) "html") ((or (= format "response") (= format "Response")) "response") ((or (= format "text") (= format "Text")) "text") ((or (= format "number") (= format "Number")) "number") (true format)))) + (do + (when (not (nil? target)) + (dom-dispatch target "hyperscript:beforeFetch" nil)) + (let + ((raw (perform (list "io-fetch" url "response" (dict))))) + (do + (when (get raw :_network-error) (raise {:response raw :message "Network error" :_hs-error "FetchError"})) + (when + (and (not (get raw :ok)) (not (= fmt "response")) (not do-not-throw)) + (raise {:response raw :status (get raw :status) :message "Fetch error" :_hs-error "FetchError"})) + (cond + ((= fmt "response") raw) + ((= fmt "json") + (let + ((parsed (perform (list "io-parse-json" (get raw :_json))))) + (hs-host-to-sx parsed))) + ((= fmt "html") + (perform (list "io-parse-html" (get raw :_html)))) + ((= fmt "number") + (or + (parse-number (get raw :_number)) + (parse-number (get raw :_body)) + 0)) + (true (get raw :_body))))))))) (define hs-json-escape @@ -965,6 +986,8 @@ (true (str value)))) ((= type-name "JSON") (cond + ((and (dict? value) (dict-has? value :_json)) + (guard (_e (true value)) (json-parse (get value :_json)))) ((string? value) (guard (_e (true value)) (json-parse value))) ((dict? value) (hs-json-stringify value)) ((list? value) (hs-json-stringify value)) diff --git a/lib/hyperscript/tokenizer.sx b/lib/hyperscript/tokenizer.sx index bee0b7a7..6b1a8742 100644 --- a/lib/hyperscript/tokenizer.sx +++ b/lib/hyperscript/tokenizer.sx @@ -568,10 +568,26 @@ (do (let ((word (read-ident start))) - (hs-emit! - (if (hs-keyword? word) "keyword" "ident") - word - start)) + (let + ((full-word + (if + (and + (< pos src-len) + (= (hs-cur) "'") + (< (+ pos 1) src-len) + (hs-letter? (hs-peek 1)) + (not + (and + (= (hs-peek 1) "s") + (or + (>= (+ pos 2) src-len) + (not (hs-ident-char? (hs-peek 2))))))) + (do (hs-advance! 1) (str word "'" (read-ident pos))) + word))) + (hs-emit! + (if (hs-keyword? full-word) "keyword" "ident") + full-word + start))) (scan!)) (and (or (= ch "=") (= ch "!") (= ch "<") (= ch ">")) diff --git a/plans/hs-conformance-scoreboard.md b/plans/hs-conformance-scoreboard.md index 9bfa739e..14a345ea 100644 --- a/plans/hs-conformance-scoreboard.md +++ b/plans/hs-conformance-scoreboard.md @@ -76,7 +76,7 @@ Remaining: ~192 tests (clusters 17/29(partial)/31 blocked; 33/34 partial) | 37 | Tokenizer-as-API | design-done | `plans/designs/e37-tokenizer-api.md` | | 38 | SourceInfo API | design-done | `plans/designs/e38-sourceinfo.md` | | 39 | WebWorker plugin | design-done | `plans/designs/e39-webworker.md` | -| 40 | Fetch non-2xx / before-fetch / real response | design-done | `plans/designs/e40-real-fetch.md` | +| 40 | Fetch non-2xx / before-fetch / real response | done | +7 | d7244d1d | ### Bucket F — generator translation gaps @@ -97,7 +97,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 | 0 | 0 | 0 | 0 | 0 | 5 | 5 | +| E | 1 | 0 | 0 | 0 | 0 | 4 | 5 | | F | — | — | — | ~10 | — | — | ~10 | ## Maintenance diff --git a/plans/hs-conformance-to-100.md b/plans/hs-conformance-to-100.md index 7d123a77..de89ef5e 100644 --- a/plans/hs-conformance-to-100.md +++ b/plans/hs-conformance-to-100.md @@ -139,7 +139,7 @@ All five have design docs on their own worktree branches pending review + merge. 39. **[design-done, pending review — `plans/designs/e39-webworker.md` on `hs-design-e39-webworker`] WebWorker plugin** — 1 test. Parser-only stub that errors with a link to upstream docs; no runtime, no mock Worker class. Hand-write the test (don't patch the generator). Single commit. -40. **[design-done, pending review — `plans/designs/e40-real-fetch.md` on `worktree-agent-a94612a4283eaa5e0`] Fetch non-2xx / before-fetch event / real response object** — 7 tests. SX-dict Response wrapper `{:_hs-response :ok :status :url :_body :_json :_html}`; restructured `hs-fetch` that always fetches wrapper then converts by format; test-name-keyed `_fetchScripts`. 11-step checklist. Watch for regression on cluster-1 JSON unwrap. +40. **[done +7 — d7244d1d] Fetch non-2xx / before-fetch event / real response object** — 7 tests. SX-dict Response wrapper `{:_hs-response :ok :status :url :_body :_json :_html}`; restructured `hs-fetch` that always fetches wrapper then converts by format; test-name-keyed `_fetchScripts`. 11-step checklist. Watch for regression on cluster-1 JSON unwrap. ### Bucket F: generator translation gaps (after bucket A-D) diff --git a/shared/static/wasm/sx/hs-compiler.sx b/shared/static/wasm/sx/hs-compiler.sx index 30297f78..2df6752b 100644 --- a/shared/static/wasm/sx/hs-compiler.sx +++ b/shared/static/wasm/sx/hs-compiler.sx @@ -1838,7 +1838,7 @@ (list (quote fn) (list) (hs-to-sx (nth ast 1))) (list (quote fn) (list) (hs-to-sx (nth ast 2))))) ((= head (quote fetch)) - (list (quote hs-fetch) (hs-to-sx (nth ast 1)) (nth ast 2))) + (list (quote hs-fetch) (hs-to-sx (nth ast 1)) (nth ast 2) (nth ast 3) (quote me))) ((= head (quote fetch-gql)) (list (quote hs-fetch-gql) diff --git a/shared/static/wasm/sx/hs-parser.sx b/shared/static/wasm/sx/hs-parser.sx index 6558e330..9d32b1ff 100644 --- a/shared/static/wasm/sx/hs-parser.sx +++ b/shared/static/wasm/sx/hs-parser.sx @@ -1772,7 +1772,7 @@ ((url (if (and (= (tp-type) "keyword") (= (tp-val) "from")) (do (adv!) (parse-arith (parse-poss (parse-atom)))) nil))) (list (quote fetch-gql) gql-source url)))) (let - ((url-atom (if (and (= (tp-type) "op") (= (tp-val) "/")) (do (adv!) (let ((path-parts (list "/"))) (define read-path (fn () (when (and (not (at-end?)) (or (= (tp-type) "ident") (= (tp-type) "op") (= (tp-type) "dot") (= (tp-type) "number"))) (append! path-parts (tp-val)) (adv!) (read-path)))) (read-path) (join "" path-parts))) (parse-atom)))) + ((url-atom (if (and (= (tp-type) "op") (= (tp-val) "/")) (do (adv!) (let ((path-parts (list "/"))) (define read-path (fn () (when (and (not (at-end?)) (or (and (= (tp-type) "ident") (not (string-contains? (tp-val) "'"))) (= (tp-type) "op") (= (tp-type) "dot") (= (tp-type) "number"))) (append! path-parts (tp-val)) (adv!) (read-path)))) (read-path) (join "" path-parts))) (parse-atom)))) (let ((url (if (nil? url-atom) url-atom (parse-arith (parse-poss url-atom))))) (let @@ -1788,7 +1788,27 @@ ((fmt-after (if (and (not fmt-before) (match-kw "as")) (do (when (and (or (= (tp-type) "ident") (= (tp-type) "keyword")) (or (= (tp-val) "an") (= (tp-val) "a"))) (adv!)) (let ((f (tp-val))) (adv!) f)) nil))) (let ((fmt (or fmt-before fmt-after "text"))) - (list (quote fetch) url fmt))))))))) + (let + ((do-not-throw + (cond + ((and (or (= (tp-type) "keyword") (= (tp-type) "ident")) (= (tp-val) "do")) + (do + (adv!) + (if (and (or (= (tp-type) "keyword") (= (tp-type) "ident")) (= (tp-val) "not")) + (do + (adv!) + (if (and (or (= (tp-type) "keyword") (= (tp-type) "ident")) (= (tp-val) "throw")) + (do (adv!) true) + false)) + false))) + ((and (= (tp-type) "ident") (= (tp-val) "don't")) + (do + (adv!) + (if (and (or (= (tp-type) "keyword") (= (tp-type) "ident")) (= (tp-val) "throw")) + (do (adv!) true) + false))) + (true false)))) + (list (quote fetch) url fmt do-not-throw)))))))))) (define parse-call-args (fn diff --git a/shared/static/wasm/sx/hs-runtime.sx b/shared/static/wasm/sx/hs-runtime.sx index 9ce20df5..133cdc6e 100644 --- a/shared/static/wasm/sx/hs-runtime.sx +++ b/shared/static/wasm/sx/hs-runtime.sx @@ -869,12 +869,33 @@ (define hs-fetch (fn - (url format) + (url format do-not-throw target) (let - ((fmt (cond ((nil? format) "text") ((or (= format "json") (= format "JSON") (= format "Object")) "json") ((or (= format "html") (= format "HTML")) "html") ((or (= format "response") (= format "Response")) "response") ((or (= format "text") (= format "Text")) "text") (true format)))) - (let - ((raw (perform (list "io-fetch" url fmt)))) - (cond ((= fmt "json") (hs-host-to-sx raw)) (true raw)))))) + ((fmt (cond ((nil? format) "text") ((or (= format "json") (= format "JSON") (= format "Object")) "json") ((or (= format "html") (= format "HTML")) "html") ((or (= format "response") (= format "Response")) "response") ((or (= format "text") (= format "Text")) "text") ((or (= format "number") (= format "Number")) "number") (true format)))) + (do + (when (not (nil? target)) + (dom-dispatch target "hyperscript:beforeFetch" nil)) + (let + ((raw (perform (list "io-fetch" url "response" (dict))))) + (do + (when (get raw :_network-error) (raise {:response raw :message "Network error" :_hs-error "FetchError"})) + (when + (and (not (get raw :ok)) (not (= fmt "response")) (not do-not-throw)) + (raise {:response raw :status (get raw :status) :message "Fetch error" :_hs-error "FetchError"})) + (cond + ((= fmt "response") raw) + ((= fmt "json") + (let + ((parsed (perform (list "io-parse-json" (get raw :_json))))) + (hs-host-to-sx parsed))) + ((= fmt "html") + (perform (list "io-parse-html" (get raw :_html)))) + ((= fmt "number") + (or + (parse-number (get raw :_number)) + (parse-number (get raw :_body)) + 0)) + (true (get raw :_body))))))))) (define hs-json-escape @@ -965,6 +986,8 @@ (true (str value)))) ((= type-name "JSON") (cond + ((and (dict? value) (dict-has? value :_json)) + (guard (_e (true value)) (json-parse (get value :_json)))) ((string? value) (guard (_e (true value)) (json-parse value))) ((dict? value) (hs-json-stringify value)) ((list? value) (hs-json-stringify value)) diff --git a/shared/static/wasm/sx/hs-tokenizer.sx b/shared/static/wasm/sx/hs-tokenizer.sx index bee0b7a7..6b1a8742 100644 --- a/shared/static/wasm/sx/hs-tokenizer.sx +++ b/shared/static/wasm/sx/hs-tokenizer.sx @@ -568,10 +568,26 @@ (do (let ((word (read-ident start))) - (hs-emit! - (if (hs-keyword? word) "keyword" "ident") - word - start)) + (let + ((full-word + (if + (and + (< pos src-len) + (= (hs-cur) "'") + (< (+ pos 1) src-len) + (hs-letter? (hs-peek 1)) + (not + (and + (= (hs-peek 1) "s") + (or + (>= (+ pos 2) src-len) + (not (hs-ident-char? (hs-peek 2))))))) + (do (hs-advance! 1) (str word "'" (read-ident pos))) + word))) + (hs-emit! + (if (hs-keyword? full-word) "keyword" "ident") + full-word + start))) (scan!)) (and (or (= ch "=") (= ch "!") (= ch "<") (= ch ">")) diff --git a/spec/tests/test-hyperscript-behavioral.sx b/spec/tests/test-hyperscript-behavioral.sx index 8e41cb14..504a8378 100644 --- a/spec/tests/test-hyperscript-behavioral.sx +++ b/spec/tests/test-hyperscript-behavioral.sx @@ -7429,7 +7429,14 @@ ;; ── fetch (23 tests) ── (defsuite "hs-upstream-fetch" (deftest "Response can be converted to JSON via as JSON" - (error "SKIP (skip-list): Response can be converted to JSON via as JSON")) + (hs-cleanup!) + (let ((_el-div (dom-create-element "div"))) + (dom-set-attr _el-div "_" "on click fetch /test as Response then put (it as JSON).name into me") + (dom-append (dom-body) _el-div) + (hs-activate! _el-div) + (dom-dispatch _el-div "click" nil) + (assert= (dom-text-content _el-div) "Joe") + )) (deftest "allows the event handler to change the fetch parameters" (hs-cleanup!) (let ((_el-div (dom-create-element "div"))) @@ -7440,9 +7447,23 @@ (assert= (dom-text-content _el-div) "yay") )) (deftest "as response does not throw on 404" - (error "SKIP (skip-list): as response does not throw on 404")) + (hs-cleanup!) + (let ((_el-div (dom-create-element "div"))) + (dom-set-attr _el-div "_" "on click fetch /test as response then put it.status into me") + (dom-append (dom-body) _el-div) + (hs-activate! _el-div) + (dom-dispatch _el-div "click" nil) + (assert= (dom-text-content _el-div) "404") + )) (deftest "can catch an error that occurs when using fetch" - (error "SKIP (skip-list): can catch an error that occurs when using fetch")) + (hs-cleanup!) + (let ((_el-div (dom-create-element "div"))) + (dom-set-attr _el-div "_" "on click fetch /test catch e log e put \"yay\" into me") + (dom-append (dom-body) _el-div) + (hs-activate! _el-div) + (dom-dispatch _el-div "click" nil) + (assert= (dom-text-content _el-div) "yay") + )) (deftest "can do a simple fetch" (hs-cleanup!) (let ((_el-div (dom-create-element "div"))) @@ -7563,9 +7584,23 @@ (assert= (dom-text-content _el-div) "yay") )) (deftest "do not throw passes through 404 response" - (error "SKIP (skip-list): do not throw passes through 404 response")) + (hs-cleanup!) + (let ((_el-div (dom-create-element "div"))) + (dom-set-attr _el-div "_" "on click fetch /test do not throw then put it into me") + (dom-append (dom-body) _el-div) + (hs-activate! _el-div) + (dom-dispatch _el-div "click" nil) + (assert= (dom-text-content _el-div) "the body") + )) (deftest "don't throw passes through 404 response" - (error "SKIP (skip-list): don't throw passes through 404 response")) + (hs-cleanup!) + (let ((_el-div (dom-create-element "div"))) + (dom-set-attr _el-div "_" "on click fetch /test don't throw then put it into me") + (dom-append (dom-body) _el-div) + (hs-activate! _el-div) + (dom-dispatch _el-div "click" nil) + (assert= (dom-text-content _el-div) "the body") + )) (deftest "submits the fetch parameters to the event handler" (hs-cleanup!) (host-set! (host-global "window") "headerCheckPassed" false) @@ -7577,9 +7612,26 @@ (assert= (dom-text-content _el-div) "yay") )) (deftest "throws on non-2xx response by default" - (error "SKIP (skip-list): throws on non-2xx response by default")) + (hs-cleanup!) + (let ((_el-div (dom-create-element "div"))) + (dom-set-attr _el-div "_" "on click fetch /test catch e put \"caught\" into me") + (dom-append (dom-body) _el-div) + (hs-activate! _el-div) + (dom-dispatch _el-div "click" nil) + (assert= (dom-text-content _el-div) "caught") + )) (deftest "triggers an event just before fetching" - (error "SKIP (skip-list): triggers an event just before fetching")) + (hs-cleanup!) + (host-call (host-global "window") "addEventListener" "hyperscript:beforeFetch" (fn (_event) (dom-set-attr (host-get _event "target") "class" "foo-set"))) + (let ((_el-div (dom-create-element "div"))) + (dom-set-attr _el-div "_" "on click fetch \"/test\" then put it into my.innerHTML end") + (dom-append (dom-body) _el-div) + (hs-activate! _el-div) + (assert (not (dom-has-class? _el-div "foo-set"))) + (dom-dispatch _el-div "click" nil) + (assert (dom-has-class? _el-div "foo-set")) + (assert= (dom-text-content _el-div) "yay") + )) ) ;; ── focus (3 tests) ── diff --git a/tests/hs-run-filtered.js b/tests/hs-run-filtered.js index 54e919f3..be7720a0 100755 --- a/tests/hs-run-filtered.js +++ b/tests/hs-run-filtered.js @@ -81,7 +81,7 @@ class El { hasAttribute(n) { return n in this.attributes; } addEventListener(e,f) { if(!this._listeners[e])this._listeners[e]=[]; this._listeners[e].push(f); } removeEventListener(e,f) { if(this._listeners[e])this._listeners[e]=this._listeners[e].filter(x=>x!==f); } - dispatchEvent(ev) { ev.target=ev.target||this; ev.currentTarget=this; const fns=[...(this._listeners[ev.type]||[])]; for(const f of fns){if(ev._si)break;try{f.call(this,ev);}catch(e){}} if(ev.bubbles&&!ev._sp&&this.parentElement){this.parentElement.dispatchEvent(ev);} return !ev.defaultPrevented; } + dispatchEvent(ev) { ev.target=ev.target||this; ev.currentTarget=this; const fns=[...(this._listeners[ev.type]||[])]; for(const f of fns){if(ev._si)break;try{f.call(this,ev);}catch(e){}} if(ev.bubbles&&!ev._sp){if(this.parentElement){this.parentElement.dispatchEvent(ev);}else if(globalThis._windowListeners){globalThis.dispatchEvent(ev);}} return !ev.defaultPrevented; } appendChild(c) { if(c.parentElement)c.parentElement.removeChild(c); c.parentElement=this; c.parentNode=this; this.children.push(c); this.childNodes.push(c); if(this.tagName==='SELECT'&&c.tagName==='OPTION'){this.options.push(c);if(c.selected&&this.selectedIndex<0)this.selectedIndex=this.options.length-1;} this._syncText(); return c; } removeChild(c) { this.children=this.children.filter(x=>x!==c); this.childNodes=this.childNodes.filter(x=>x!==c); c.parentElement=null; c.parentNode=null; this._syncText(); return c; } insertBefore(n,r) { if(n.parentElement)n.parentElement.removeChild(n); const i=this.children.indexOf(r); if(i>=0){this.children.splice(i,0,n);this.childNodes.splice(i,0,n);}else{this.children.push(n);this.childNodes.push(n);} n.parentElement=this;n.parentNode=this; this._syncText(); return n; } @@ -336,6 +336,11 @@ const document = { createEvent(t){return new Ev(t);}, addEventListener(){}, removeEventListener(){}, }; globalThis.document=document; globalThis.window=globalThis; globalThis.HTMLElement=El; globalThis.Element=El; +// window event-target shim (for hyperscript:beforeFetch and similar bubbled events) +globalThis._windowListeners={}; +globalThis.addEventListener=function(e,f){if(!globalThis._windowListeners[e])globalThis._windowListeners[e]=[];globalThis._windowListeners[e].push(f);}; +globalThis.removeEventListener=function(e,f){if(globalThis._windowListeners[e])globalThis._windowListeners[e]=globalThis._windowListeners[e].filter(x=>x!==f);}; +globalThis.dispatchEvent=function(ev){const fns=[...(globalThis._windowListeners[ev.type]||[])];for(const f of fns){if(ev&&ev._si)break;try{f.call(globalThis,ev);}catch(e){}}return ev?!ev.defaultPrevented:true;}; // cluster-33: cookie store + document.cookie + cookies Proxy. globalThis.__hsCookieStore = new Map(); Object.defineProperty(document, 'cookie', { @@ -584,9 +589,28 @@ const _fetchRoutes = { '/number': { status: 200, body: '1.2' }, '/users/Joe': { status: 200, body: 'Joe', json: '{"name":"Joe"}' }, }; +// Per-test fetch overrides keyed by test name; takes priority over _fetchRoutes. +const _fetchScripts = { + "as response does not throw on 404": + { "/test": { status: 404, body: "not found" } }, + "do not throw passes through 404 response": + { "/test": { status: 404, body: "the body" } }, + "don't throw passes through 404 response": + { "/test": { status: 404, body: "the body" } }, + "throws on non-2xx response by default": + { "/test": { status: 404, body: "not found" } }, + "Response can be converted to JSON via as JSON": + { "/test": { status: 200, body: '{"name":"Joe"}', json: '{"name":"Joe"}', + contentType: "application/json" } }, + "can catch an error that occurs when using fetch": + { "/test": { networkError: true } }, + "triggers an event just before fetching": + { "/test": { status: 200, body: "yay", contentType: "text/html" } }, +}; function _mockFetch(url) { - const route = _fetchRoutes[url] || _fetchRoutes['/test']; - return { ok: route.status < 400, status: route.status || 200, url: url || '/test', + const scriptRoutes = _fetchScripts[globalThis.__currentHsTestName]; + const route = (scriptRoutes && scriptRoutes[url]) || _fetchRoutes[url] || _fetchRoutes['/test']; + return { ok: (route.status||200) < 400, status: route.status || 200, url: url || '/test', _body: route.body || '', _json: route.json || route.body || '', _html: route.html || route.body || '' }; } globalThis._driveAsync=function driveAsync(r,d){d=d||0;if(d>500||!r||!r.suspended)return;if(_testDeadline && Date.now()>_testDeadline)throw new Error('TIMEOUT: wall clock exceeded');const req=r.request;const items=req&&(req.items||req);const op=items&&items[0];const opName=typeof op==='string'?op:(op&&op.name)||String(op); @@ -594,13 +618,10 @@ globalThis._driveAsync=function driveAsync(r,d){d=d||0;if(d>500||!r||!r.suspende if(opName==='io-sleep'||opName==='wait')doResume(null); else if(opName==='io-fetch'){ const url=typeof items[1]==='string'?items[1]:'/test'; - const fmt=typeof items[2]==='string'?items[2]:'text'; - const route=_fetchRoutes[url]||_fetchRoutes['/test']; - if(fmt==='json'){try{doResume(JSON.parse(route.json||route.body||'{}'));}catch(e){doResume(null);}} - else if(fmt==='html'){const frag=new El('fragment');frag.nodeType=11;frag.innerHTML=route.html||route.body||'';frag.textContent=frag.innerHTML.replace(/<[^>]*>/g,'');doResume(frag);} - else if(fmt==='response')doResume({ok:(route.status||200)<400,status:route.status||200,url}); - else if(fmt.toLowerCase()==='number')doResume(parseFloat(route.number||route.body||'0')); - else doResume(route.body||''); + const scriptRoutes=_fetchScripts[globalThis.__currentHsTestName]; + const route=(scriptRoutes&&scriptRoutes[url])||_fetchRoutes[url]||_fetchRoutes['/test']; + if(route&&route.networkError){doResume({_type:'dict','_network-error':true,message:'aborted'});} + else{const st=route.status||200;doResume({_type:'dict',ok:st<400,status:st,url,_body:route.body||'',_json:route.json||route.body||'',_html:route.html||route.body||'',_number:route.number||route.body||''});} } else if(opName==='io-parse-text'){const resp=items&&items[1];doResume(resp&&resp._body?resp._body:typeof resp==='string'?resp:'');} else if(opName==='io-parse-json'){const resp=items&&items[1];try{doResume(JSON.parse(typeof resp==='string'?resp:resp&&resp._json?resp._json:'{}'));}catch(e){doResume(null);}} @@ -697,6 +718,7 @@ for(let i=startTest;i { param.target.PROP = 'VAL'; }) + wa = re.search( + r"window\.addEventListener\(\s*(['\"])([^'\"]+)\1\s*,\s*" + r"\((\w+)\)\s*=>\s*\{\s*\3\.target\.(\w+)\s*=\s*['\"]([^'\"]+)['\"]\s*;?\s*\}", + m.group(1), + ) + if wa: + ev_name = wa.group(2) + prop = wa.group(4) + val = wa.group(5) + attr = 'class' if prop == 'className' else prop + sx = (f'(host-call (host-global "window") "addEventListener" "{ev_name}" ' + f'(fn (_event) (dom-set-attr (host-get _event "target") "{attr}" "{val}")))') + if seen_html: + ops.append(sx) + else: + pre_setups.append(('__hs_config__', sx)) + continue # fall through # evaluate(() => _hyperscript.config.X = ...) single-line variant.