|
|
@@ -27,8 +27,11 @@
|
|
|
* 14 two sibling forms: clearing A leaves B alone
|
|
|
* 15 statum.forms.reset: resets exactly the given form
|
|
|
* 16 handler order: stm-prefill registered before stm-class.*
|
|
|
+ * 17 reenter: action triggers an in-place entrypoint refresh (fetched twice)
|
|
|
+ * 18 reenter ordering: refresh before navigate, both execute
|
|
|
+ * 19 reenter failure: logged only; navigate still runs; promise resolves
|
|
|
*
|
|
|
- * Usage: qjs tests/dom/scenarios.js <1..16>
|
|
|
+ * Usage: qjs tests/dom/scenarios.js <1..19>
|
|
|
*/
|
|
|
import * as std from 'std';
|
|
|
import { installShim } from './shim.js';
|
|
|
@@ -506,6 +509,146 @@ async function main() {
|
|
|
check(log.warns.length === 0, 'warnings: ' + JSON.stringify(log.warns));
|
|
|
check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
|
|
|
|
|
|
+ } else if (scenario === '17') {
|
|
|
+ // reenter: an action response re-fetches the entrypoint in place.
|
|
|
+ var t = el('span'); t.setAttribute('stm-text', 'm.mood');
|
|
|
+ body.appendChild(t);
|
|
|
+
|
|
|
+ sigCounter++;
|
|
|
+ var snap1 = { slot: { key: 'k-m', type: 'm', scope: 'page' }, public: { mood: 'first' } };
|
|
|
+ var snap2 = { slot: { key: 'k-m', type: 'm', scope: 'page' }, public: { mood: 'second' } };
|
|
|
+ var entrypointCount = 0;
|
|
|
+ globalThis.__shim.setFetch(function (url) {
|
|
|
+ if (String(url).indexOf('/_statum/entrypoint') !== -1) {
|
|
|
+ entrypointCount++;
|
|
|
+ return globalThis.__shim.okResponse([{
|
|
|
+ type: 'set',
|
|
|
+ snapshot_frame: {
|
|
|
+ content: JSON.stringify(entrypointCount === 1 ? snap1 : snap2),
|
|
|
+ signature: 'sig-r17-' + entrypointCount
|
|
|
+ }
|
|
|
+ }]);
|
|
|
+ }
|
|
|
+ // The action request: reply with a reenter directive.
|
|
|
+ return globalThis.__shim.okResponse([{ type: 'reenter' }]);
|
|
|
+ });
|
|
|
+
|
|
|
+ await s._bridge.loadEntrypoint();
|
|
|
+ await spin();
|
|
|
+ check(t.textContent === 'first', 'initial entrypoint state not applied: ' + JSON.stringify(t.textContent));
|
|
|
+ var initialFetches = globalThis.__shim.fetchCalls.length;
|
|
|
+
|
|
|
+ await s._bridge.performAction({ uri: '/save', method: 'POST' }, {});
|
|
|
+ await spin(50);
|
|
|
+
|
|
|
+ var entryFetches = globalThis.__shim.fetchCalls.filter(function (c) {
|
|
|
+ return c.url.indexOf('/_statum/entrypoint') !== -1;
|
|
|
+ });
|
|
|
+ check(entryFetches.length === 2, 'entrypoint fetched ' + entryFetches.length + ' time(s), expected 2 (initial + reenter)');
|
|
|
+ check(globalThis.__shim.fetchCalls.length === initialFetches + 2,
|
|
|
+ 'exactly two new requests were expected (action + refresh), got ' +
|
|
|
+ (globalThis.__shim.fetchCalls.length - initialFetches));
|
|
|
+ check(t.textContent === 'second', 'reentered entrypoint state not applied: ' + JSON.stringify(t.textContent));
|
|
|
+ check(globalThis.location.href === 'http://localhost/', 'a bare reenter must not navigate');
|
|
|
+ check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
|
|
|
+
|
|
|
+ } else if (scenario === '18') {
|
|
|
+ // reenter + navigate in one response: refresh first, then navigate wins.
|
|
|
+ var t = el('span'); t.setAttribute('stm-text', 'm.mood');
|
|
|
+ body.appendChild(t);
|
|
|
+
|
|
|
+ sigCounter++;
|
|
|
+ var snap1 = { slot: { key: 'k-m', type: 'm', scope: 'page' }, public: { mood: 'first' } };
|
|
|
+ var snap2 = { slot: { key: 'k-m', type: 'm', scope: 'page' }, public: { mood: 'second' } };
|
|
|
+ var entrypointCount = 0;
|
|
|
+ var order = []; // shared log: reenter's fetch vs the location change
|
|
|
+ globalThis.__shim.setFetch(function (url) {
|
|
|
+ if (String(url).indexOf('/_statum/entrypoint') !== -1) {
|
|
|
+ entrypointCount++;
|
|
|
+ order.push('entrypoint:' + entrypointCount);
|
|
|
+ return globalThis.__shim.okResponse([{
|
|
|
+ type: 'set',
|
|
|
+ snapshot_frame: {
|
|
|
+ content: JSON.stringify(entrypointCount === 1 ? snap1 : snap2),
|
|
|
+ signature: 'sig-r18-' + entrypointCount
|
|
|
+ }
|
|
|
+ }]);
|
|
|
+ }
|
|
|
+ return globalThis.__shim.okResponse([{ type: 'reenter' }, { type: 'navigate', uri: '/after' }]);
|
|
|
+ });
|
|
|
+ // Record the navigation in the shared order log.
|
|
|
+ var originalHref = globalThis.location.href;
|
|
|
+ Object.defineProperty(globalThis.location, 'href', {
|
|
|
+ configurable: true,
|
|
|
+ get: function () { return originalHref; },
|
|
|
+ set: function (v) { order.push('navigate:' + v); }
|
|
|
+ });
|
|
|
+
|
|
|
+ await s._bridge.loadEntrypoint();
|
|
|
+ await spin();
|
|
|
+ check(t.textContent === 'first', 'initial entrypoint state not applied');
|
|
|
+
|
|
|
+ await s._bridge.performAction({ uri: '/save', method: 'POST' }, {});
|
|
|
+ await spin(50);
|
|
|
+
|
|
|
+ check(globalThis.__shim.fetchCalls.filter(function (c) {
|
|
|
+ return c.url.indexOf('/_statum/entrypoint') !== -1;
|
|
|
+ }).length === 2, 'reenter did not refresh the entrypoint');
|
|
|
+ check(order.indexOf('navigate:/after') !== -1, 'navigate did not run');
|
|
|
+ check(order.indexOf('entrypoint:2') < order.indexOf('navigate:/after'),
|
|
|
+ 'reenter must run before navigate, order was ' + JSON.stringify(order));
|
|
|
+ check(t.textContent === 'second', 'refresh state not applied before navigating');
|
|
|
+ check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
|
|
|
+
|
|
|
+ } else if (scenario === '19') {
|
|
|
+ // A failing reenter refresh: logged, swallowed, navigate still runs, and
|
|
|
+ // the action promise resolves.
|
|
|
+ var t = el('span'); t.setAttribute('stm-text', 'm.mood');
|
|
|
+ body.appendChild(t);
|
|
|
+
|
|
|
+ sigCounter++;
|
|
|
+ var snap1 = { slot: { key: 'k-m', type: 'm', scope: 'page' }, public: { mood: 'first' } };
|
|
|
+ var entrypointCount = 0;
|
|
|
+ globalThis.__shim.setFetch(function (url) {
|
|
|
+ if (String(url).indexOf('/_statum/entrypoint') !== -1) {
|
|
|
+ entrypointCount++;
|
|
|
+ if (entrypointCount === 1) {
|
|
|
+ return globalThis.__shim.okResponse([{
|
|
|
+ type: 'set',
|
|
|
+ snapshot_frame: { content: JSON.stringify(snap1), signature: 'sig-r19-1' }
|
|
|
+ }]);
|
|
|
+ }
|
|
|
+ return Promise.reject(new Error('network down'));
|
|
|
+ }
|
|
|
+ return globalThis.__shim.okResponse([{ type: 'reenter' }, { type: 'navigate', uri: '/after' }]);
|
|
|
+ });
|
|
|
+ var navigatedTo = null;
|
|
|
+ Object.defineProperty(globalThis.location, 'href', {
|
|
|
+ configurable: true,
|
|
|
+ get: function () { return 'http://localhost/'; },
|
|
|
+ set: function (v) { navigatedTo = v; }
|
|
|
+ });
|
|
|
+
|
|
|
+ await s._bridge.loadEntrypoint();
|
|
|
+ await spin();
|
|
|
+
|
|
|
+ var resolved = false;
|
|
|
+ var rejected = null;
|
|
|
+ await s._bridge.performAction({ uri: '/save', method: 'POST' }, {}).then(function () {
|
|
|
+ resolved = true;
|
|
|
+ }, function (e) {
|
|
|
+ rejected = e;
|
|
|
+ });
|
|
|
+ await spin(50);
|
|
|
+
|
|
|
+ check(resolved && rejected === null, 'action promise should resolve after a failed reenter, rejected=' + String(rejected));
|
|
|
+ check(log.errors.some(function (m) { return m.indexOf('[statum] reenter: entrypoint refresh failed') !== -1; }),
|
|
|
+ 'reenter failure was not logged: ' + JSON.stringify(log.errors));
|
|
|
+ check(navigatedTo === '/after', 'navigate did not run after the failed reenter, navigatedTo=' + JSON.stringify(navigatedTo));
|
|
|
+ check(log.errors.length === 1, 'exactly one console.error expected: ' + JSON.stringify(log.errors));
|
|
|
+ // Core logs the swallowed reenter failure by design; nothing else may error.
|
|
|
+ log.errors.length = 0;
|
|
|
+
|
|
|
} else {
|
|
|
fail('unknown scenario: ' + scenario);
|
|
|
}
|