shim.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. /**
  2. * Minimal DOM shim for the tests/dom suite.
  3. *
  4. * Unlike the split/markdown shims (which only need statum to LOAD), this one
  5. * supports driving the render walk and the forms module end to end:
  6. *
  7. * - elements with ORDERED attributes, get/set/has/removeAttribute, a real
  8. * classList backed by the `class` attribute, children/parent hierarchy,
  9. * appendChild/insertBefore/removeChild, nextElementSibling,
  10. * cloneNode(true) with FRESH node identity (listeners and live control
  11. * state are NOT copied; attributes and children are),
  12. * - querySelectorAll for tag, `.class`, `[attr]`, `[attr="v"]`,
  13. * `[attr~="v"]`, `*` and comma lists of those (everything the walk and
  14. * the forms module ask for),
  15. * - an EventTarget model on document AND elements with CAPTURE and bubble
  16. * phases and event.target set — statum-document.js relies on document-level
  17. * capture listeners,
  18. * - form semantics: el.form (owner form), form.reset() (fires a cancelable
  19. * `reset` event, then restores control defaults from attributes),
  20. * computed select.value/select.multiple/option.value, input.type,
  21. * - the fetch-adjacent globals QuickJS lacks (Headers, URLSearchParams,
  22. * FormData, btoa/TextEncoder) plus a scriptable fetch stub, and in-memory
  23. * localStorage/sessionStorage.
  24. *
  25. * document.readyState stays 'loading' so statum-core.js only registers a
  26. * DOMContentLoaded listener; tests drive renderAll() directly.
  27. */
  28. export function installShim() {
  29. var log = { errors: [], warns: [], messages: [] };
  30. var fetchCalls = [];
  31. var fetchHandler = null;
  32. // -------------------------------------------------------------------------
  33. // Events
  34. // -------------------------------------------------------------------------
  35. function makeEvent(type, opts) {
  36. return {
  37. type: type,
  38. target: null,
  39. currentTarget: null,
  40. eventPhase: '',
  41. cancelable: !opts || opts.cancelable !== false,
  42. defaultPrevented: false,
  43. preventDefault: function () { this.defaultPrevented = true; }
  44. };
  45. }
  46. function addEventTarget(obj) {
  47. obj._listeners = { capture: {}, bubble: {} };
  48. obj.addEventListener = function (type, fn, capture) {
  49. var bag = capture ? this._listeners.capture : this._listeners.bubble;
  50. (bag[type] = bag[type] || []).push(fn);
  51. };
  52. obj.removeEventListener = function () {};
  53. obj.dispatchEvent = function (evt) {
  54. evt.target = this;
  55. // Path from the target's parent up to (and including) document.
  56. var path = [];
  57. var n = this.parentNode;
  58. while (n) { path.push(n); n = n.parentNode; }
  59. function run(node, bag) {
  60. var list = bag[evt.type];
  61. if (!list) return;
  62. list = list.slice();
  63. for (var i = 0; i < list.length; i++) list[i].call(node, evt);
  64. }
  65. // Capture: outermost ancestor (document) down to the parent.
  66. evt.eventPhase = 'capture';
  67. for (var i = path.length - 1; i >= 0; i--) {
  68. evt.currentTarget = path[i];
  69. run(path[i], path[i]._listeners.capture);
  70. }
  71. // Target: both listener kinds.
  72. evt.eventPhase = 'target';
  73. evt.currentTarget = this;
  74. run(this, this._listeners.capture);
  75. run(this, this._listeners.bubble);
  76. // Bubble: parent up to document.
  77. evt.eventPhase = 'bubble';
  78. for (var j = 0; j < path.length; j++) {
  79. evt.currentTarget = path[j];
  80. run(path[j], path[j]._listeners.bubble);
  81. }
  82. return !evt.defaultPrevented;
  83. };
  84. }
  85. // -------------------------------------------------------------------------
  86. // Selector matching (tag / .class / [attr] / [attr=v] / [attr~=v] / *)
  87. // -------------------------------------------------------------------------
  88. function parseComplex(sel) {
  89. var preds = [];
  90. var s = sel.replace(/^\s+|\s+$/g, '');
  91. while (s) {
  92. var m;
  93. if (s.charAt(0) === '*') {
  94. preds.push(function () { return true; });
  95. s = s.slice(1);
  96. } else if ((m = s.match(/^([a-zA-Z][a-zA-Z0-9]*)/))) {
  97. var tag = m[1].toUpperCase();
  98. preds.push(function (el) { return el.tagName === tag; });
  99. s = s.slice(m[1].length);
  100. } else if ((m = s.match(/^\.([a-zA-Z0-9_-]+)/))) {
  101. var cls = m[1];
  102. preds.push(function (el) { return hasClass(el, cls); });
  103. s = s.slice(m[0].length);
  104. } else if ((m = s.match(/^\[([a-zA-Z0-9_-]+)(?:([~^|*]?=)("([^"]*)"|'([^']*)'|([^\]\s]+)))?\]/))) {
  105. var name = m[1], op = m[2] || null, val = m[4] != null ? m[4] : (m[5] != null ? m[5] : m[6]);
  106. preds.push(function (el) {
  107. var cur = el.getAttribute(name);
  108. if (cur === null) return false;
  109. if (!op) return true;
  110. if (op === '=') return cur === val;
  111. if (op === '~=') return (' ' + cur + ' ').split(/\s+/).indexOf(val) !== -1;
  112. if (op === '^=') return cur.indexOf(val) === 0;
  113. if (op === '$=') return cur.indexOf(val) === cur.length - val.length;
  114. if (op === '*=') return cur.indexOf(val) !== -1;
  115. if (op === '|=') return cur === val || cur.indexOf(val + '-') === 0;
  116. return false;
  117. });
  118. s = s.slice(m[0].length);
  119. } else {
  120. return null; // unsupported selector: matches nothing
  121. }
  122. s = s.replace(/^\s+/, '');
  123. }
  124. return preds.length ? preds : null;
  125. }
  126. function matches(el, complex) {
  127. for (var i = 0; i < complex.length; i++) {
  128. if (!complex[i](el)) return false;
  129. }
  130. return true;
  131. }
  132. function descendants(root, out) {
  133. var kids = root.children || [];
  134. for (var i = 0; i < kids.length; i++) {
  135. out.push(kids[i]);
  136. descendants(kids[i], out);
  137. }
  138. return out;
  139. }
  140. function qsa(root, selector) {
  141. var complexes = String(selector).split(',').map(function (c) { return parseComplex(c); });
  142. var all = descendants(root, []);
  143. var hits = [];
  144. for (var i = 0; i < all.length; i++) {
  145. for (var j = 0; j < complexes.length; j++) {
  146. if (complexes[j] && matches(all[i], complexes[j])) { hits.push(all[i]); break; }
  147. }
  148. }
  149. return hits;
  150. }
  151. // -------------------------------------------------------------------------
  152. // Elements
  153. // -------------------------------------------------------------------------
  154. function hasClass(el, cls) {
  155. var cur = el.getAttribute('class') || '';
  156. return cur.split(/\s+/).indexOf(cls) !== -1;
  157. }
  158. function makeElement(tag) {
  159. if (String(tag || '').toLowerCase() === 'form') return makeForm();
  160. var el = {
  161. nodeType: 1,
  162. tagName: (tag || 'div').toUpperCase(),
  163. /** Ordered {name, value} pairs; re-setting keeps the original slot. */
  164. attributes: [],
  165. children: [],
  166. parentNode: null,
  167. innerHTML: '',
  168. textContent: '',
  169. style: {},
  170. value: undefined,
  171. checked: undefined,
  172. selected: undefined
  173. };
  174. // ----- attributes -----
  175. el.setAttribute = function (n, v) {
  176. var hit = null;
  177. for (var i = 0; i < this.attributes.length; i++) {
  178. if (this.attributes[i].name === n) { hit = this.attributes[i]; break; }
  179. }
  180. if (hit) hit.value = String(v);
  181. else this.attributes.push({ name: n, value: String(v) });
  182. };
  183. el.removeAttribute = function (n) {
  184. this.attributes = this.attributes.filter(function (a) { return a.name !== n; });
  185. };
  186. el.hasAttribute = function (n) {
  187. for (var i = 0; i < this.attributes.length; i++) {
  188. if (this.attributes[i].name === n) return true;
  189. }
  190. return false;
  191. };
  192. el.getAttribute = function (n) {
  193. for (var i = 0; i < this.attributes.length; i++) {
  194. if (this.attributes[i].name === n) return this.attributes[i].value;
  195. }
  196. return null;
  197. };
  198. // ----- classList (backed by the `class` attribute) -----
  199. el.classList = {
  200. add: function (c) {
  201. var cur = el.getAttribute('class') || '';
  202. var parts = cur ? cur.split(/\s+/).filter(Boolean) : [];
  203. if (parts.indexOf(c) === -1) { parts.push(c); el.setAttribute('class', parts.join(' ')); }
  204. },
  205. remove: function (c) {
  206. var cur = el.getAttribute('class') || '';
  207. var parts = cur ? cur.split(/\s+/).filter(function (p) { return p && p !== c; }) : [];
  208. if (parts.length) el.setAttribute('class', parts.join(' '));
  209. else el.removeAttribute('class');
  210. },
  211. contains: function (c) { return hasClass(el, c); }
  212. };
  213. // ----- tree -----
  214. el.appendChild = function (c) {
  215. if (c.parentNode) c.parentNode.removeChild(c);
  216. c.parentNode = this;
  217. this.children.push(c);
  218. return c;
  219. };
  220. el.insertBefore = function (c, ref) {
  221. if (c.parentNode) c.parentNode.removeChild(c);
  222. c.parentNode = this;
  223. var i = ref ? this.children.indexOf(ref) : this.children.length;
  224. if (i < 0) i = this.children.length;
  225. this.children.splice(i, 0, c);
  226. return c;
  227. };
  228. el.removeChild = function (c) {
  229. var i = this.children.indexOf(c);
  230. if (i >= 0) this.children.splice(i, 1);
  231. c.parentNode = null;
  232. return c;
  233. };
  234. el.remove = function () { if (this.parentNode) this.parentNode.removeChild(this); };
  235. Object.defineProperty(el, 'nextElementSibling', {
  236. get: function () {
  237. if (!this.parentNode) return null;
  238. var i = this.parentNode.children.indexOf(this);
  239. return this.parentNode.children[i + 1] || null;
  240. }
  241. });
  242. Object.defineProperty(el, 'nextSibling', {
  243. get: function () { return this.nextElementSibling; }
  244. });
  245. el.contains = function (other) {
  246. while (other) { if (other === this) return true; other = other.parentNode; }
  247. return false;
  248. };
  249. Object.defineProperty(el, 'firstChild', { get: function () { return this.children[0] || null; } });
  250. el.cloneNode = function (deep) {
  251. var copy = makeElement(this.tagName);
  252. copy.attributes = this.attributes.map(function (a) { return { name: a.name, value: a.value }; });
  253. copy.innerHTML = this.innerHTML;
  254. if (this.textContent !== undefined && this.textContent !== '') copy.textContent = this.textContent;
  255. // value/checked/selected are live state, not attributes: NOT copied
  256. // (fresh controls read as clean, exactly like the browser).
  257. if (deep) {
  258. for (var i = 0; i < this.children.length; i++) copy.appendChild(this.children[i].cloneNode(true));
  259. }
  260. return copy;
  261. };
  262. el.querySelectorAll = function (sel) { return qsa(this, sel); };
  263. el.querySelector = function (sel) { return qsa(this, sel)[0] || null; };
  264. el.closest = function (sel) {
  265. var complexes = String(sel).split(',').map(function (c) { return parseComplex(c); });
  266. var n = this;
  267. while (n && n.nodeType === 1) {
  268. for (var j = 0; j < complexes.length; j++) {
  269. if (complexes[j] && matches(n, complexes[j])) return n;
  270. }
  271. n = n.parentNode;
  272. }
  273. return null;
  274. };
  275. Object.defineProperty(el, 'form', {
  276. get: function () {
  277. var n = this.parentNode;
  278. while (n) { if (n.tagName === 'FORM') return n; n = n.parentNode; }
  279. return null;
  280. }
  281. });
  282. // ----- control-type property semantics -----
  283. if (el.tagName === 'INPUT') {
  284. var live = { value: false, checked: false, _v: '', _c: false };
  285. Object.defineProperty(el, 'type', {
  286. get: function () { return this.getAttribute('type') || 'text'; },
  287. set: function (v) { this.setAttribute('type', v); }
  288. });
  289. Object.defineProperty(el, 'value', {
  290. get: function () {
  291. if (live.value) return live._v;
  292. var a = this.getAttribute('value');
  293. return a !== null ? a : '';
  294. },
  295. set: function (v) { live.value = true; live._v = v; }
  296. });
  297. Object.defineProperty(el, 'checked', {
  298. get: function () { return live.checked ? live._c : this.hasAttribute('checked'); },
  299. set: function (v) { live.checked = true; live._c = !!v; }
  300. });
  301. } else if (el.tagName === 'TEXTAREA') {
  302. var ta = { value: false, _v: '' };
  303. Object.defineProperty(el, 'value', {
  304. get: function () { return ta.value ? ta._v : (this.textContent || ''); },
  305. set: function (v) { ta.value = true; ta._v = v; }
  306. });
  307. } else if (el.tagName === 'SELECT') {
  308. Object.defineProperty(el, 'type', {
  309. get: function () { return this.multiple ? 'select-multiple' : 'select-one'; }
  310. });
  311. Object.defineProperty(el, 'multiple', {
  312. get: function () { return this.hasAttribute('multiple'); },
  313. set: function (v) { if (v) this.setAttribute('multiple', ''); else this.removeAttribute('multiple'); }
  314. });
  315. Object.defineProperty(el, 'value', {
  316. get: function () {
  317. var opts = qsa(this, 'option');
  318. for (var i = 0; i < opts.length; i++) {
  319. if (opts[i].selected) return opts[i].value;
  320. }
  321. return '';
  322. },
  323. set: function (v) {
  324. var opts = qsa(this, 'option');
  325. var s = v == null ? '' : String(v);
  326. for (var i = 0; i < opts.length; i++) opts[i].selected = opts[i].value === s;
  327. }
  328. });
  329. } else if (el.tagName === 'OPTION') {
  330. Object.defineProperty(el, 'value', {
  331. get: function () {
  332. var a = this.getAttribute('value');
  333. return a !== null ? a : (this.textContent || '');
  334. },
  335. set: function (v) { this.setAttribute('value', v); }
  336. });
  337. }
  338. addEventTarget(el);
  339. return el;
  340. }
  341. /** Restore a form's controls to their attribute-declared defaults. */
  342. function restoreDefaults(form) {
  343. var controls = qsa(form, 'input,select,textarea');
  344. for (var i = 0; i < controls.length; i++) {
  345. var c = controls[i];
  346. if (c.tagName === 'INPUT') {
  347. var t = c.getAttribute('type') || 'text';
  348. if (t === 'checkbox' || t === 'radio') {
  349. c.checked = c.hasAttribute('checked');
  350. } else if (t !== 'file') {
  351. c.value = c.getAttribute('value') !== null ? c.getAttribute('value') : '';
  352. }
  353. } else if (c.tagName === 'TEXTAREA') {
  354. c.value = c.getAttribute('value') !== null ? c.getAttribute('value') : (c.textContent || '');
  355. } else if (c.tagName === 'SELECT') {
  356. var opts = qsa(c, 'option');
  357. for (var j = 0; j < opts.length; j++) opts[j].selected = opts[j].hasAttribute('selected');
  358. }
  359. }
  360. }
  361. function makeForm() {
  362. var f = makeElement('_form'); // generic path; '_form' is not special-cased
  363. f.tagName = 'FORM';
  364. f.reset = function () {
  365. var evt = makeEvent('reset');
  366. this.dispatchEvent(evt);
  367. if (evt.defaultPrevented) return;
  368. restoreDefaults(this);
  369. };
  370. f.submit = function () {};
  371. return f;
  372. }
  373. // -------------------------------------------------------------------------
  374. // FormData / Headers / URLSearchParams
  375. // -------------------------------------------------------------------------
  376. function FormData(form) {
  377. this._pairs = [];
  378. if (!form) return;
  379. var controls = qsa(form, 'input,select,textarea');
  380. for (var i = 0; i < controls.length; i++) {
  381. var c = controls[i];
  382. var name = c.getAttribute('name');
  383. if (!name) continue;
  384. if (c.tagName === 'INPUT') {
  385. var t = c.getAttribute('type') || 'text';
  386. if (t === 'checkbox' || t === 'radio') { if (c.checked) this._pairs.push([name, c.value == null ? '' : String(c.value)]); }
  387. else if (t === 'file') { /* no file state in the shim */ }
  388. else this._pairs.push([name, c.value == null ? '' : String(c.value)]);
  389. } else if (c.tagName === 'TEXTAREA') {
  390. this._pairs.push([name, c.value == null ? '' : String(c.value)]);
  391. } else if (c.tagName === 'SELECT') {
  392. var opts = qsa(c, 'option');
  393. for (var j = 0; j < opts.length; j++) {
  394. if (opts[j].selected) this._pairs.push([name, opts[j].value]);
  395. }
  396. }
  397. }
  398. }
  399. FormData.prototype.forEach = function (fn) {
  400. for (var i = 0; i < this._pairs.length; i++) fn(this._pairs[i][1], this._pairs[i][0]);
  401. };
  402. /** Minimal File stand-in so `value instanceof File` checks in core pass. */
  403. function File() {}
  404. globalThis.File = File;
  405. function Headers(init) {
  406. this._h = {};
  407. if (init && typeof init.forEach === 'function') {
  408. var self = this;
  409. init.forEach(function (v, k) { self._h[String(k).toLowerCase()] = String(v); });
  410. }
  411. }
  412. Headers.prototype.set = function (n, v) { this._h[String(n).toLowerCase()] = String(v); };
  413. Headers.prototype.append = function (n, v) {
  414. var k = String(n).toLowerCase();
  415. this._h[k] = this._h[k] ? this._h[k] + ', ' + v : String(v);
  416. };
  417. Headers.prototype.get = function (n) {
  418. var k = Object.keys(this._h).filter(function (x) { return x === String(n).toLowerCase(); })[0];
  419. return k !== undefined ? this._h[k] : null;
  420. };
  421. function URLSearchParams(params) {
  422. this._pairs = [];
  423. if (params && typeof params === 'object') {
  424. var self = this;
  425. Object.keys(params).forEach(function (k) {
  426. var v = params[k];
  427. self._pairs.push([k, v == null ? '' : String(v)]);
  428. });
  429. }
  430. }
  431. URLSearchParams.prototype.toString = function () {
  432. return this._pairs.map(function (p) {
  433. return encodeURIComponent(p[0]) + '=' + encodeURIComponent(p[1]);
  434. }).join('&');
  435. };
  436. function makeStorage() {
  437. var m = {};
  438. return {
  439. get length() { return Object.keys(m).length; },
  440. key: function (i) { return Object.keys(m)[i] || null; },
  441. getItem: function (k) { return k in m ? m[k] : null; },
  442. setItem: function (k, v) { m[k] = String(v); },
  443. removeItem: function (k) { delete m[k]; }
  444. };
  445. }
  446. // -------------------------------------------------------------------------
  447. // Globals
  448. // -------------------------------------------------------------------------
  449. globalThis.console = {
  450. log: function () { log.messages.push(['log', Array.prototype.slice.call(arguments)]); },
  451. info: function () { log.messages.push(['info', Array.prototype.slice.call(arguments)]); },
  452. debug: function () { log.messages.push(['debug', Array.prototype.slice.call(arguments)]); },
  453. warn: function () {
  454. log.warns.push(Array.prototype.slice.call(arguments).join(' '));
  455. log.messages.push(['warn', Array.prototype.slice.call(arguments)]);
  456. },
  457. error: function () {
  458. log.errors.push(Array.prototype.slice.call(arguments).join(' '));
  459. log.messages.push(['error', Array.prototype.slice.call(arguments)]);
  460. }
  461. };
  462. var body = makeElement('body');
  463. var documentElement = makeElement('html');
  464. documentElement.appendChild(body);
  465. var doc = {
  466. nodeType: 9,
  467. readyState: 'loading',
  468. cookie: '',
  469. body: body,
  470. head: makeElement('head'),
  471. documentElement: documentElement,
  472. createElement: function (tag) { return tag.toLowerCase() === 'form' ? makeForm() : makeElement(tag); },
  473. createTextNode: function (t) { return { nodeType: 3, textContent: t }; },
  474. getElementById: function (id) {
  475. return qsa(documentElement, '*').filter(function (el) { return el.getAttribute('id') === id; })[0] || null;
  476. },
  477. querySelectorAll: function (sel) { return qsa(documentElement, sel); },
  478. querySelector: function (sel) { return qsa(documentElement, sel)[0] || null; }
  479. };
  480. addEventTarget(doc);
  481. globalThis.document = doc;
  482. // Event propagation continues through the document node (browsers run
  483. // document-level capture listeners before the target), so the tree root's
  484. // parent IS the document. qsa/closest must stop at element boundaries.
  485. documentElement.parentNode = doc;
  486. globalThis.window = globalThis;
  487. globalThis.location = { href: 'http://localhost/', protocol: 'http:', addEventListener: function () {} };
  488. globalThis.navigator = { userAgent: 'qjs' };
  489. globalThis.addEventListener = function () {};
  490. globalThis.removeEventListener = function () {};
  491. globalThis.alert = function () {};
  492. globalThis.confirm = function () { return true; };
  493. globalThis.Headers = Headers;
  494. globalThis.URLSearchParams = URLSearchParams;
  495. globalThis.FormData = FormData;
  496. globalThis.localStorage = makeStorage();
  497. globalThis.sessionStorage = makeStorage();
  498. globalThis.TextEncoder = function () {
  499. this.encode = function (s) {
  500. var out = [];
  501. for (var i = 0; i < s.length; i++) out.push(s.charCodeAt(i) & 0xff);
  502. return out;
  503. };
  504. };
  505. globalThis.btoa = function (s) {
  506. var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
  507. var out = '';
  508. for (var i = 0; i < s.length; i += 3) {
  509. var b1 = s.charCodeAt(i), b2 = s.charCodeAt(i + 1), b3 = s.charCodeAt(i + 2);
  510. out += chars.charAt(b1 >> 2);
  511. out += chars.charAt(((b1 & 3) << 4) | ((b2 || 0) >> 4));
  512. out += b2 !== undefined ? chars.charAt(((b2 & 15) << 2) | ((b3 || 0) >> 6)) : '=';
  513. out += b3 !== undefined ? chars.charAt(b3 & 63) : '=';
  514. }
  515. return out;
  516. };
  517. /** Canned responses for the fetch stub. */
  518. function okResponse(directives) {
  519. return {
  520. ok: true,
  521. status: 200,
  522. headers: { get: function (n) { return String(n).toLowerCase() === 'content-type' ? 'application/vnd.statum+json' : null; } },
  523. json: function () { return Promise.resolve(directives || []); }
  524. };
  525. }
  526. function failResponse(status) {
  527. return {
  528. ok: false,
  529. status: status || 500,
  530. headers: { get: function () { return null; } },
  531. json: function () { return Promise.resolve([]); }
  532. };
  533. }
  534. globalThis.fetch = function (url, init) {
  535. fetchCalls.push({
  536. url: String(url),
  537. method: (init && init.method) || 'GET',
  538. body: init && init.body
  539. });
  540. var res = fetchHandler
  541. ? fetchHandler(String(url), init || {})
  542. : okResponse([]);
  543. return Promise.resolve(res);
  544. };
  545. globalThis.__shim = {
  546. log: log,
  547. body: body,
  548. makeElement: makeElement,
  549. makeEvent: makeEvent,
  550. fetchCalls: fetchCalls,
  551. setFetch: function (fn) { fetchHandler = fn; },
  552. okResponse: okResponse,
  553. failResponse: failResponse
  554. };
  555. globalThis.__STATUM_TEST__ = true;
  556. }