scenarios.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. /**
  2. * Module-split scenarios for the Statum client.
  3. *
  4. * Each scenario runs in a FRESH QuickJS context (run.sh invokes this script
  5. * once per scenario) and loads a specific subset of the module files in a
  6. * specific order, proving:
  7. *
  8. * 1 core-alone - window.statum (headless: state/directives/boot),
  9. * overrides, driver-less render, NO registerAttribute
  10. * 2 core headless boot- stubbed fetch -> entrypoint state applied, no walk
  11. * 3 core+document - registry installed, handler order pinned
  12. * (stm-prefill first), statum.document API,
  13. * statum.forms gone
  14. * 4 core+doc+markdown - full order, statum.markdown API, test hatch
  15. * 5 core+markdown - cherry-pick without the walk (crash-free; queued
  16. * registration)
  17. * 6 document alone - guarded IIFE: console.error, nothing attached
  18. * 7 double register - warns, order unchanged
  19. * 8 unknown anchor - warns, registration position kept
  20. *
  21. * Usage: qjs tests/split/scenarios.js <1..8>
  22. */
  23. import * as std from 'std';
  24. import { installShim } from './shim.js';
  25. // Repo root, as a CWD-relative path. run.sh (like tests/markdown/run.sh)
  26. // always `cd`s to the repo root before invoking qjs, and QuickJS'
  27. // std.loadFile resolves relative paths against the process CWD — the
  28. // established in-repo idiom (see tests/markdown/spec-test.js).
  29. var ROOT = '.';
  30. var scenario = scriptArgs.length > 1 ? scriptArgs[1] : '';
  31. function fail(msg) {
  32. std.err.puts('FAIL [' + scenario + '] ' + msg + '\n');
  33. std.exit(1);
  34. }
  35. function check(cond, msg) {
  36. if (!cond) fail(msg);
  37. }
  38. function load(name) {
  39. (0, eval)(std.loadFile(ROOT + '/js/' + name));
  40. }
  41. function handlerNames() {
  42. return globalThis.statum._bridge.attributeHandlers.map(function (h) { return h.name; });
  43. }
  44. function installEntryStub() {
  45. var snap = { slot: { key: 'k-s', type: 's', scope: 'page' }, public: { v: 42 } };
  46. globalThis.__shim.setFetch(function () {
  47. return globalThis.__shim.okResponse([{
  48. type: 'set',
  49. snapshot_frame: { content: JSON.stringify(snap), signature: 'sig-s-1' }
  50. }]);
  51. });
  52. }
  53. async function spin(n) {
  54. for (var i = 0; i < (n || 30); i++) await Promise.resolve();
  55. }
  56. installShim();
  57. var log = globalThis.__shim.log;
  58. if (scenario === '1') {
  59. load('statum-core.js');
  60. var s = globalThis.statum;
  61. check(s, 'window.statum is not defined');
  62. check(s.version === '0.1.0', 'version missing');
  63. check(typeof s.config === 'object' && s.config.entrypointUrl === '/_statum/entrypoint',
  64. 'config missing');
  65. check(typeof s.registerAttribute === 'undefined',
  66. 'registerAttribute leaked into headless core (it is document API now)');
  67. check(s._bridge, '_bridge is not present');
  68. check(typeof s.Error === 'function', 'statum.Error missing');
  69. check(typeof s._bridge.registerAttribute === 'function',
  70. 'bridge.registerAttribute (the pre-document queue) is not a function');
  71. check(log.errors.length === 0, 'unexpected console.error at load: ' + JSON.stringify(log.errors));
  72. check(log.warns.length === 0, 'unexpected console.warn at load: ' + JSON.stringify(log.warns));
  73. // The three override pairs round-trip a function and null out non-functions.
  74. var fn = function () {};
  75. var pairs = ['onConfirmHandler', 'onNotifyHandler', 'onErrorHandler'];
  76. pairs.forEach(function (p) {
  77. s[p] = fn;
  78. check(s[p] === fn, p + ' did not round-trip the function');
  79. s[p] = 42;
  80. check(s[p] === null, p + ' did not null out a non-function');
  81. s[p] = 'nope';
  82. check(s[p] === null, p + ' did not null out a non-function (string)');
  83. });
  84. // No render driver installed: a render cycle must not crash, and the
  85. // pre-document registration queue stays empty.
  86. s._bridge.renderAll();
  87. check(log.errors.length === 0, 'renderAll without a driver errored: ' + JSON.stringify(log.errors));
  88. } else if (scenario === '2') {
  89. // Headless boot: stub the entrypoint fetch, init, and prove state was
  90. // applied with no DOM module loaded.
  91. load('statum-core.js');
  92. var s = globalThis.statum;
  93. installEntryStub();
  94. await s.init();
  95. await spin();
  96. check(s.read('s') && s.read('s').v === 42,
  97. 'headless init did not apply entrypoint state: ' + JSON.stringify(s.state()));
  98. check(s._bridge.entrypointLoaded(), 'entrypointLoaded is not exposed as true after boot');
  99. check(log.errors.length === 0, 'headless boot errored: ' + JSON.stringify(log.errors));
  100. check(log.warns.length === 0, 'headless boot warned: ' + JSON.stringify(log.warns));
  101. } else if (scenario === '3') {
  102. load('statum-core.js');
  103. load('statum-document.js');
  104. var s = globalThis.statum;
  105. check(typeof s.registerAttribute === 'function',
  106. 'registerAttribute is not public on the document module');
  107. var names = handlerNames();
  108. check(JSON.stringify(names) === JSON.stringify(
  109. ['stm-prefill', 'stm-class.', 'stm-attribute.', 'stm-disabled', 'stm-text', 'stm-html']),
  110. 'handler order is ' + JSON.stringify(names));
  111. check(s.document && typeof s.document.reset === 'function', 'statum.document.reset missing');
  112. check(typeof s.document.markDirty === 'function' && typeof s.document.isDirty === 'function',
  113. 'statum.document.markDirty/isDirty missing');
  114. check(s.forms === undefined, 'statum.forms must be gone');
  115. check(s._bridge.formsDirty instanceof WeakSet, 'bridge.formsDirty missing');
  116. // The driver is installed: renderAll walks document.body and applies a
  117. // binding. Scope comes from the (empty) store, so use a literal expression.
  118. var el = globalThis.__shim.makeElement('p');
  119. el.setAttribute('stm-text', "'driven'");
  120. globalThis.__shim.body.appendChild(el);
  121. s._bridge.renderAll();
  122. check(el.textContent === 'driven',
  123. 'render driver did not apply stm-text (got ' + JSON.stringify(el.textContent) + ')');
  124. check(log.errors.length === 0, 'unexpected console.error: ' + JSON.stringify(log.errors));
  125. } else if (scenario === '4') {
  126. load('statum-core.js');
  127. load('statum-document.js');
  128. load('statum-markdown.js');
  129. var s = globalThis.statum;
  130. var names = handlerNames();
  131. check(JSON.stringify(names) === JSON.stringify(
  132. ['stm-prefill', 'stm-class.', 'stm-attribute.', 'stm-disabled', 'stm-text', 'stm-html',
  133. 'stm-markdown', 'stm-h-demotion']),
  134. 'full handler order is ' + JSON.stringify(names));
  135. // "directly after" from the ordered list above.
  136. check(names.indexOf('stm-markdown') === names.indexOf('stm-html') + 1,
  137. 'stm-markdown is not directly after stm-html');
  138. check(names.indexOf('stm-h-demotion') === names.indexOf('stm-markdown') + 1,
  139. 'stm-h-demotion is not directly after stm-markdown');
  140. // Programmatic API shares the binding's engine. The engine renders blocks
  141. // with a trailing newline (CommonMark serialisation), so '# x' -> '<h1>x</h1>\n'.
  142. check(s.markdown, 'statum.markdown is not defined');
  143. check(s.markdown.render('# x') === '<h1>x</h1>\n',
  144. 'markdown.render("# x") = ' + JSON.stringify(s.markdown.render('# x')));
  145. check(typeof s.markdown.renderTo === 'function', 'markdown.renderTo is not a function');
  146. var el = globalThis.__shim.makeElement('div');
  147. s.markdown.renderTo(el, '# y');
  148. check(el.innerHTML === '<h1>y</h1>\n',
  149. 'renderTo innerHTML = ' + JSON.stringify(el.innerHTML));
  150. // The binding path and the programmatic path agree. (Attribute values are
  151. // expressions, hence the quoted string; an unparseable value yields
  152. // undefined and leaves the element untouched by design.)
  153. el.innerHTML = '';
  154. el.setAttribute('stm-markdown', "'# y'");
  155. globalThis.__shim.body.appendChild(el);
  156. // Drive the walk through the driver via a fresh render cycle.
  157. var bridge = s._bridge;
  158. bridge.renderAll();
  159. check(el.innerHTML === '<h1>y</h1>\n',
  160. 'stm-markdown binding innerHTML = ' + JSON.stringify(el.innerHTML));
  161. // Test hatch (the shim sets __STATUM_TEST__).
  162. check(typeof s._testMarkdown === 'function', '_testMarkdown is not exposed');
  163. check(log.errors.length === 0, 'unexpected console.error: ' + JSON.stringify(log.errors));
  164. } else if (scenario === '5') {
  165. // Cherry-pick: markdown without the walk module. The registration lands in
  166. // core's pre-document QUEUE (no warning — it is the designed headless
  167. // degradation), the API works, and a render cycle with no driver still runs.
  168. load('statum-core.js');
  169. load('statum-markdown.js');
  170. var s = globalThis.statum;
  171. check(s.markdown.render('# x') === '<h1>x</h1>\n', 'markdown.render failed without statum-document.js');
  172. check(s._bridge.attributeHandlers === undefined,
  173. 'attributeHandlers must be absent without statum-document.js');
  174. check(log.errors.length === 0, 'unexpected console.error: ' + JSON.stringify(log.errors));
  175. check(log.warns.length === 0, 'queued registration must not warn: ' + JSON.stringify(log.warns));
  176. s._bridge.renderAll();
  177. check(log.errors.length === 0, 'renderAll errored without a driver: ' + JSON.stringify(log.errors));
  178. } else if (scenario === '6') {
  179. // document without core: the guarded IIFE must record one console.error,
  180. // not throw, and attach nothing.
  181. load('statum-document.js');
  182. check(log.errors.length === 1, 'expected exactly one console.error, got ' + JSON.stringify(log.errors));
  183. check(log.errors[0].indexOf('statum-document.js requires statum-core.js') !== -1,
  184. 'error message: ' + JSON.stringify(log.errors[0]));
  185. check(!globalThis.statum, 'document attached window.statum without core');
  186. // Same for markdown.
  187. log.errors.length = 0;
  188. load('statum-markdown.js');
  189. check(log.errors.length === 1, 'expected one console.error from markdown, got ' + JSON.stringify(log.errors));
  190. check(log.errors[0].indexOf('statum-markdown.js requires statum-core.js') !== -1,
  191. 'error message: ' + JSON.stringify(log.errors[0]));
  192. check(!globalThis.statum, 'markdown attached window.statum without core');
  193. } else if (scenario === '7') {
  194. load('statum-core.js');
  195. load('statum-document.js');
  196. var before = handlerNames();
  197. globalThis.statum.registerAttribute('stm-text', function () {});
  198. check(log.warns.length === 1, 'double registration did not warn: ' + JSON.stringify(log.warns));
  199. check(log.warns[0].indexOf('already registered') !== -1, 'warn text: ' + JSON.stringify(log.warns[0]));
  200. var after = handlerNames();
  201. check(JSON.stringify(before) === JSON.stringify(after),
  202. 'order changed on double registration: ' + JSON.stringify(after));
  203. } else if (scenario === '8') {
  204. load('statum-core.js');
  205. load('statum-document.js');
  206. var before = handlerNames();
  207. globalThis.statum.registerAttribute('stm-widget', function () {}, { after: 'stm-nope' });
  208. check(log.warns.length === 1, 'unresolvable constraint did not warn: ' + JSON.stringify(log.warns));
  209. check(log.warns[0].indexOf('stm-nope') !== -1, 'warn names the missing anchor: ' + JSON.stringify(log.warns[0]));
  210. var after = handlerNames();
  211. check(after.length === before.length + 1, 'handler was not registered');
  212. check(after[after.length - 1] === 'stm-widget',
  213. 'handler did not keep registration position: ' + JSON.stringify(after));
  214. } else {
  215. fail('unknown scenario: ' + scenario);
  216. }
  217. print('scenario ' + scenario + ': OK');