shim.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  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. if (this.__stmObservers) {
  183. this.__stmObservers.forEach(function (o) { o.cb([]); });
  184. }
  185. };
  186. el.removeAttribute = function (n) {
  187. this.attributes = this.attributes.filter(function (a) { return a.name !== n; });
  188. if (this.__stmObservers) {
  189. this.__stmObservers.forEach(function (o) { o.cb([]); });
  190. }
  191. };
  192. el.hasAttribute = function (n) {
  193. for (var i = 0; i < this.attributes.length; i++) {
  194. if (this.attributes[i].name === n) return true;
  195. }
  196. return false;
  197. };
  198. el.getAttribute = function (n) {
  199. for (var i = 0; i < this.attributes.length; i++) {
  200. if (this.attributes[i].name === n) return this.attributes[i].value;
  201. }
  202. return null;
  203. };
  204. // ----- classList (backed by the `class` attribute) -----
  205. el.classList = {
  206. add: function (c) {
  207. var cur = el.getAttribute('class') || '';
  208. var parts = cur ? cur.split(/\s+/).filter(Boolean) : [];
  209. if (parts.indexOf(c) === -1) { parts.push(c); el.setAttribute('class', parts.join(' ')); }
  210. },
  211. remove: function (c) {
  212. var cur = el.getAttribute('class') || '';
  213. var parts = cur ? cur.split(/\s+/).filter(function (p) { return p && p !== c; }) : [];
  214. if (parts.length) el.setAttribute('class', parts.join(' '));
  215. else el.removeAttribute('class');
  216. },
  217. contains: function (c) { return hasClass(el, c); }
  218. };
  219. // ----- tree -----
  220. el.appendChild = function (c) {
  221. if (c.parentNode) c.parentNode.removeChild(c);
  222. c.parentNode = this;
  223. this.children.push(c);
  224. return c;
  225. };
  226. el.insertBefore = function (c, ref) {
  227. if (c.parentNode) c.parentNode.removeChild(c);
  228. c.parentNode = this;
  229. var i = ref ? this.children.indexOf(ref) : this.children.length;
  230. if (i < 0) i = this.children.length;
  231. this.children.splice(i, 0, c);
  232. return c;
  233. };
  234. el.removeChild = function (c) {
  235. var i = this.children.indexOf(c);
  236. if (i >= 0) this.children.splice(i, 1);
  237. c.parentNode = null;
  238. return c;
  239. };
  240. el.remove = function () { if (this.parentNode) this.parentNode.removeChild(this); };
  241. Object.defineProperty(el, 'nextElementSibling', {
  242. get: function () {
  243. if (!this.parentNode) return null;
  244. var i = this.parentNode.children.indexOf(this);
  245. return this.parentNode.children[i + 1] || null;
  246. }
  247. });
  248. Object.defineProperty(el, 'nextSibling', {
  249. get: function () { return this.nextElementSibling; }
  250. });
  251. el.contains = function (other) {
  252. while (other) { if (other === this) return true; other = other.parentNode; }
  253. return false;
  254. };
  255. Object.defineProperty(el, 'firstChild', { get: function () { return this.children[0] || null; } });
  256. el.cloneNode = function (deep) {
  257. var copy = makeElement(this.tagName);
  258. copy.attributes = this.attributes.map(function (a) { return { name: a.name, value: a.value }; });
  259. copy.innerHTML = this.innerHTML;
  260. if (this.textContent !== undefined && this.textContent !== '') copy.textContent = this.textContent;
  261. // value/checked/selected are live state, not attributes: NOT copied
  262. // (fresh controls read as clean, exactly like the browser).
  263. if (deep) {
  264. for (var i = 0; i < this.children.length; i++) copy.appendChild(this.children[i].cloneNode(true));
  265. }
  266. return copy;
  267. };
  268. el.querySelectorAll = function (sel) { return qsa(this, sel); };
  269. el.querySelector = function (sel) { return qsa(this, sel)[0] || null; };
  270. el.closest = function (sel) {
  271. var complexes = String(sel).split(',').map(function (c) { return parseComplex(c); });
  272. var n = this;
  273. while (n && n.nodeType === 1) {
  274. for (var j = 0; j < complexes.length; j++) {
  275. if (complexes[j] && matches(n, complexes[j])) return n;
  276. }
  277. n = n.parentNode;
  278. }
  279. return null;
  280. };
  281. Object.defineProperty(el, 'form', {
  282. get: function () {
  283. var n = this.parentNode;
  284. while (n) { if (n.tagName === 'FORM') return n; n = n.parentNode; }
  285. return null;
  286. }
  287. });
  288. // ----- control-type property semantics -----
  289. if (el.tagName === 'INPUT') {
  290. var live = { value: false, checked: false, _v: '', _c: false };
  291. Object.defineProperty(el, 'type', {
  292. get: function () { return this.getAttribute('type') || 'text'; },
  293. set: function (v) { this.setAttribute('type', v); }
  294. });
  295. Object.defineProperty(el, 'value', {
  296. get: function () {
  297. if (live.value) return live._v;
  298. var a = this.getAttribute('value');
  299. return a !== null ? a : '';
  300. },
  301. set: function (v) { live.value = true; live._v = v; }
  302. });
  303. Object.defineProperty(el, 'checked', {
  304. get: function () { return live.checked ? live._c : this.hasAttribute('checked'); },
  305. set: function (v) { live.checked = true; live._c = !!v; }
  306. });
  307. } else if (el.tagName === 'TEXTAREA') {
  308. var ta = { value: false, _v: '' };
  309. Object.defineProperty(el, 'value', {
  310. get: function () { return ta.value ? ta._v : (this.textContent || ''); },
  311. set: function (v) { ta.value = true; ta._v = v; }
  312. });
  313. } else if (el.tagName === 'SELECT') {
  314. Object.defineProperty(el, 'type', {
  315. get: function () { return this.multiple ? 'select-multiple' : 'select-one'; }
  316. });
  317. Object.defineProperty(el, 'multiple', {
  318. get: function () { return this.hasAttribute('multiple'); },
  319. set: function (v) { if (v) this.setAttribute('multiple', ''); else this.removeAttribute('multiple'); }
  320. });
  321. Object.defineProperty(el, 'value', {
  322. get: function () {
  323. var opts = qsa(this, 'option');
  324. for (var i = 0; i < opts.length; i++) {
  325. if (opts[i].selected) return opts[i].value;
  326. }
  327. return '';
  328. },
  329. set: function (v) {
  330. var opts = qsa(this, 'option');
  331. var s = v == null ? '' : String(v);
  332. for (var i = 0; i < opts.length; i++) opts[i].selected = opts[i].value === s;
  333. }
  334. });
  335. } else if (el.tagName === 'OPTION') {
  336. Object.defineProperty(el, 'value', {
  337. get: function () {
  338. var a = this.getAttribute('value');
  339. return a !== null ? a : (this.textContent || '');
  340. },
  341. set: function (v) { this.setAttribute('value', v); }
  342. });
  343. }
  344. addEventTarget(el);
  345. return el;
  346. }
  347. /** Restore a form's controls to their attribute-declared defaults. */
  348. function restoreDefaults(form) {
  349. var controls = qsa(form, 'input,select,textarea');
  350. for (var i = 0; i < controls.length; i++) {
  351. var c = controls[i];
  352. if (c.tagName === 'INPUT') {
  353. var t = c.getAttribute('type') || 'text';
  354. if (t === 'checkbox' || t === 'radio') {
  355. c.checked = c.hasAttribute('checked');
  356. } else if (t !== 'file') {
  357. c.value = c.getAttribute('value') !== null ? c.getAttribute('value') : '';
  358. }
  359. } else if (c.tagName === 'TEXTAREA') {
  360. c.value = c.getAttribute('value') !== null ? c.getAttribute('value') : (c.textContent || '');
  361. } else if (c.tagName === 'SELECT') {
  362. var opts = qsa(c, 'option');
  363. for (var j = 0; j < opts.length; j++) opts[j].selected = opts[j].hasAttribute('selected');
  364. }
  365. }
  366. }
  367. function makeForm() {
  368. var f = makeElement('_form'); // generic path; '_form' is not special-cased
  369. f.tagName = 'FORM';
  370. f.reset = function () {
  371. var evt = makeEvent('reset');
  372. this.dispatchEvent(evt);
  373. if (evt.defaultPrevented) return;
  374. restoreDefaults(this);
  375. };
  376. f.submit = function () {};
  377. return f;
  378. }
  379. // -------------------------------------------------------------------------
  380. // FormData / Headers / URLSearchParams
  381. // -------------------------------------------------------------------------
  382. function FormData(form) {
  383. this._pairs = [];
  384. if (!form) return;
  385. var controls = qsa(form, 'input,select,textarea');
  386. for (var i = 0; i < controls.length; i++) {
  387. var c = controls[i];
  388. var name = c.getAttribute('name');
  389. if (!name) continue;
  390. if (c.tagName === 'INPUT') {
  391. var t = c.getAttribute('type') || 'text';
  392. if (t === 'checkbox' || t === 'radio') { if (c.checked) this._pairs.push([name, c.value == null ? '' : String(c.value)]); }
  393. else if (t === 'file') { /* no file state in the shim */ }
  394. else this._pairs.push([name, c.value == null ? '' : String(c.value)]);
  395. } else if (c.tagName === 'TEXTAREA') {
  396. this._pairs.push([name, c.value == null ? '' : String(c.value)]);
  397. } else if (c.tagName === 'SELECT') {
  398. var opts = qsa(c, 'option');
  399. for (var j = 0; j < opts.length; j++) {
  400. if (opts[j].selected) this._pairs.push([name, opts[j].value]);
  401. }
  402. }
  403. }
  404. }
  405. FormData.prototype.forEach = function (fn) {
  406. for (var i = 0; i < this._pairs.length; i++) fn(this._pairs[i][1], this._pairs[i][0]);
  407. };
  408. /** Minimal File stand-in so `value instanceof File` checks in core pass. */
  409. function File() {}
  410. globalThis.File = File;
  411. function Headers(init) {
  412. this._h = {};
  413. if (init && typeof init.forEach === 'function') {
  414. var self = this;
  415. init.forEach(function (v, k) { self._h[String(k).toLowerCase()] = String(v); });
  416. }
  417. }
  418. Headers.prototype.set = function (n, v) { this._h[String(n).toLowerCase()] = String(v); };
  419. Headers.prototype.append = function (n, v) {
  420. var k = String(n).toLowerCase();
  421. this._h[k] = this._h[k] ? this._h[k] + ', ' + v : String(v);
  422. };
  423. Headers.prototype.get = function (n) {
  424. var k = Object.keys(this._h).filter(function (x) { return x === String(n).toLowerCase(); })[0];
  425. return k !== undefined ? this._h[k] : null;
  426. };
  427. function URLSearchParams(params) {
  428. this._pairs = [];
  429. if (params && typeof params === 'object') {
  430. var self = this;
  431. Object.keys(params).forEach(function (k) {
  432. var v = params[k];
  433. self._pairs.push([k, v == null ? '' : String(v)]);
  434. });
  435. }
  436. }
  437. URLSearchParams.prototype.toString = function () {
  438. return this._pairs.map(function (p) {
  439. return encodeURIComponent(p[0]) + '=' + encodeURIComponent(p[1]);
  440. }).join('&');
  441. };
  442. function makeStorage() {
  443. var m = {};
  444. return {
  445. get length() { return Object.keys(m).length; },
  446. key: function (i) { return Object.keys(m)[i] || null; },
  447. getItem: function (k) { return k in m ? m[k] : null; },
  448. setItem: function (k, v) { m[k] = String(v); },
  449. removeItem: function (k) { delete m[k]; }
  450. };
  451. }
  452. // -------------------------------------------------------------------------
  453. // Globals
  454. // -------------------------------------------------------------------------
  455. globalThis.console = {
  456. log: function () { log.messages.push(['log', Array.prototype.slice.call(arguments)]); },
  457. info: function () { log.messages.push(['info', Array.prototype.slice.call(arguments)]); },
  458. debug: function () { log.messages.push(['debug', Array.prototype.slice.call(arguments)]); },
  459. warn: function () {
  460. log.warns.push(Array.prototype.slice.call(arguments).join(' '));
  461. log.messages.push(['warn', Array.prototype.slice.call(arguments)]);
  462. },
  463. error: function () {
  464. log.errors.push(Array.prototype.slice.call(arguments).join(' '));
  465. log.messages.push(['error', Array.prototype.slice.call(arguments)]);
  466. }
  467. };
  468. var body = makeElement('body');
  469. var documentElement = makeElement('html');
  470. documentElement.appendChild(body);
  471. var doc = {
  472. nodeType: 9,
  473. readyState: 'loading',
  474. cookie: '',
  475. body: body,
  476. head: makeElement('head'),
  477. documentElement: documentElement,
  478. createElement: function (tag) { return tag.toLowerCase() === 'form' ? makeForm() : makeElement(tag); },
  479. createTextNode: function (t) { return { nodeType: 3, textContent: t }; },
  480. getElementById: function (id) {
  481. return qsa(documentElement, '*').filter(function (el) { return el.getAttribute('id') === id; })[0] || null;
  482. },
  483. querySelectorAll: function (sel) { return qsa(documentElement, sel); },
  484. querySelector: function (sel) { return qsa(documentElement, sel)[0] || null; }
  485. };
  486. addEventTarget(doc);
  487. globalThis.document = doc;
  488. // Event propagation continues through the document node (browsers run
  489. // document-level capture listeners before the target), so the tree root's
  490. // parent IS the document. qsa/closest must stop at element boundaries.
  491. documentElement.parentNode = doc;
  492. globalThis.window = globalThis;
  493. globalThis.location = { href: 'http://localhost/', protocol: 'http:', addEventListener: function () {} };
  494. globalThis.navigator = { userAgent: 'qjs' };
  495. globalThis.addEventListener = function () {};
  496. globalThis.removeEventListener = function () {};
  497. globalThis.alert = function () {};
  498. globalThis.confirm = function () { return true; };
  499. globalThis.Headers = Headers;
  500. globalThis.URLSearchParams = URLSearchParams;
  501. globalThis.FormData = FormData;
  502. globalThis.localStorage = makeStorage();
  503. globalThis.sessionStorage = makeStorage();
  504. globalThis.TextEncoder = function () {
  505. this.encode = function (s) {
  506. var out = [];
  507. for (var i = 0; i < s.length; i++) out.push(s.charCodeAt(i) & 0xff);
  508. return out;
  509. };
  510. };
  511. globalThis.btoa = function (s) {
  512. var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
  513. var out = '';
  514. for (var i = 0; i < s.length; i += 3) {
  515. var b1 = s.charCodeAt(i), b2 = s.charCodeAt(i + 1), b3 = s.charCodeAt(i + 2);
  516. out += chars.charAt(b1 >> 2);
  517. out += chars.charAt(((b1 & 3) << 4) | ((b2 || 0) >> 4));
  518. out += b2 !== undefined ? chars.charAt(((b2 & 15) << 2) | ((b3 || 0) >> 6)) : '=';
  519. out += b3 !== undefined ? chars.charAt(b3 & 63) : '=';
  520. }
  521. return out;
  522. };
  523. /** Canned responses for the fetch stub. */
  524. function okResponse(directives) {
  525. return {
  526. ok: true,
  527. status: 200,
  528. headers: { get: function (n) { return String(n).toLowerCase() === 'content-type' ? 'application/vnd.statum+json' : null; } },
  529. json: function () { return Promise.resolve(directives || []); }
  530. };
  531. }
  532. function failResponse(status) {
  533. return {
  534. ok: false,
  535. status: status || 500,
  536. headers: { get: function () { return null; } },
  537. json: function () { return Promise.resolve([]); }
  538. };
  539. }
  540. globalThis.fetch = function (url, init) {
  541. fetchCalls.push({
  542. url: String(url),
  543. method: (init && init.method) || 'GET',
  544. body: init && init.body
  545. });
  546. var res = fetchHandler
  547. ? fetchHandler(String(url), init || {})
  548. : okResponse([]);
  549. return Promise.resolve(res);
  550. };
  551. // --- MutationObserver stub ---------------------------------------------
  552. // Real browsers deliver attribute mutations asynchronously; the shim runs
  553. // them SYNCHRONOUSLY inside setAttribute/removeAttribute (patched below
  554. // once, when the first element is created — the patch lives on the
  555. // prototype-less makeElement factory by wrapping every element's methods
  556. // would be too invasive, so we wrap at creation time instead: see
  557. // makeElement's setAttribute/removeAttribute above — they call
  558. // notifyObservers(el) after mutating the attribute list).
  559. var observers = new WeakMap(); // element -> [{filter, cb}]
  560. function notifyObservers(el) {
  561. if (el.__stmObservers) {
  562. el.__stmObservers.forEach(function (o) { o.cb([]); });
  563. }
  564. }
  565. globalThis.MutationObserver = function (callback) {
  566. this._cb = callback;
  567. this._targets = [];
  568. };
  569. globalThis.MutationObserver.prototype.observe = function (target, opts) {
  570. var list = target.__stmObservers || (target.__stmObservers = []);
  571. var self = this;
  572. list.push({ cb: function () { self._cb([]); } });
  573. this._targets.push(target);
  574. };
  575. globalThis.MutationObserver.prototype.disconnect = function () {
  576. var self = this;
  577. this._targets.forEach(function (t) {
  578. if (t.__stmObservers) {
  579. t.__stmObservers = t.__stmObservers.filter(function (o) {
  580. return o.cb._owner !== self;
  581. });
  582. }
  583. });
  584. };
  585. globalThis.__shim = {
  586. log: log,
  587. body: body,
  588. makeElement: makeElement,
  589. makeEvent: makeEvent,
  590. fetchCalls: fetchCalls,
  591. setFetch: function (fn) { fetchHandler = fn; },
  592. okResponse: okResponse,
  593. failResponse: failResponse,
  594. flushObservers: notifyObservers
  595. };
  596. globalThis.__STATUM_TEST__ = true;
  597. }