// reset_node drives the race between a date field's blur commit and a // form reset, in plain Node on a stub page with queued timers, so each // ordering of blur, accepted reset and cancelled reset within one task // can be laid out exactly and the timers run afterwards in the order // a browser would run them. // // node node/reset_node.mjs // // check/datetime_test.go is the only caller. It reads back one JSON // object with the field's wire value and the box's text after each // scenario, plus "shownLoad", what the box showed before anything // happened, which is what it must show again after an accepted reset: // // single blur, then an accepted reset // cancelled text typed, blur, then a cancelled reset // blurAcceptedCancelled blur, an accepted reset, a cancelled reset // cancelledBlurAccepted a cancelled reset, blur, an accepted reset // // The page is the least DOM enhance() touches: elements with // attributes, children, a class list, listeners and a form; a document // that creates them, holds the field, and dispatches a reset to the // listeners the script put on it; and setTimeout that queues. A reset // here does what a browser's does: the event is dispatched, and if no // listener cancelled it every control goes back to its default, the // field to its value attribute and the box to the empty string. import { readFileSync } from "node:fs"; import vm from "node:vm"; const src = readFileSync(new URL("../../js/datetime.js", import.meta.url), "utf8"); // page builds a fresh context holding one armed date field with a // default of 2026-10-20 inside a form, loads the script (which scans at // once, the document being complete), and returns the handles the // scenarios drive. function page() { const timers = []; const listenersOf = new WeakMap(); const element = (tag) => { const attrs = {}; const classes = new Set(); const el = { tagName: tag.toUpperCase(), children: [], parentNode: null, dataset: {}, hidden: false, value: "", id: "", type: "", disabled: false, readOnly: false, form: null, textContent: "", innerHTML: "", getAttribute: (k) => (k in attrs ? attrs[k] : null), hasAttribute: (k) => k in attrs, setAttribute(k, v) { attrs[k] = String(v); if (k === "id") el.id = String(v); }, removeAttribute(k) { delete attrs[k]; }, toggleAttribute(k, force) { const on = force === undefined ? !(k in attrs) : !!force; if (on) attrs[k] = ""; else delete attrs[k]; return on; }, classList: { add: (...c) => c.forEach((x) => classes.add(x)), remove: (...c) => c.forEach((x) => classes.delete(x)), toggle: (c, f) => (f ? classes.add(c) : classes.delete(c), !!f), contains: (c) => classes.has(c) }, appendChild(c) { if (c.parentNode) c.remove(); el.children.push(c); c.parentNode = el; return c; }, append(...cs) { cs.forEach((c) => el.appendChild(c)); }, insertBefore(c, ref) { const i = el.children.indexOf(ref); el.children.splice(i < 0 ? el.children.length : i, 0, c); c.parentNode = el; return c; }, remove() { if (el.parentNode) { const p = el.parentNode; p.children.splice(p.children.indexOf(el), 1); el.parentNode = null; } }, contains(other) { for (let n = other; n; n = n.parentNode) if (n === el) return true; return false; }, closest(sel) { for (let n = el; n; n = n.parentNode) if (n.matches && n.matches(sel)) return n; return null; }, matches(sel) { if (sel === ":disabled") { for (let n = el; n; n = n.parentNode) if (n.disabled && (n === el || n.tagName === "FIELDSET")) return true; return false; } if (sel.startsWith("[") && sel.endsWith("]")) return sel.slice(1, -1) in attrs; return false; }, querySelector: () => null, querySelectorAll: () => [], getBoundingClientRect: () => ({ top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0 }), focus() { document.activeElement = el; }, blur() { if (document.activeElement === el) document.activeElement = document.body; }, select() {}, addEventListener(type, fn) { const m = listenersOf.get(el) || {}; (m[type] = m[type] || []).push(fn); listenersOf.set(el, m); }, removeEventListener() {}, // A real Event's target is read-only, so the script's own // new Event("change") is wrapped rather than written to. dispatchEvent(ev) { const e = ev instanceof Event ? { type: ev.type, bubbles: ev.bubbles, target: el, defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } } : Object.assign({ target: el }, ev); for (let n = el; n; n = n.parentNode) { const fns = (listenersOf.get(n) || {})[e.type] || []; for (const fn of fns) fn.call(n, e); if (!e.bubbles) break; } return !e.defaultPrevented; }, get offsetTop() { return 0; }, get clientHeight() { return 0; }, scrollTop: 0, }; return el; }; const form = element("form"); const native = element("input"); native.type = "date"; native.setAttribute("type", "date"); native.setAttribute("id", "d"); native.setAttribute("name", "d"); native.setAttribute("value", "2026-10-20"); native.setAttribute("data-rst-date", ""); native.value = "2026-10-20"; native.form = form; const field = element("div"); field.appendChild(native); form.appendChild(field); const body = element("body"); body.appendChild(form); const root = element("html"); root.setAttribute("lang", "en"); root.lang = "en"; root.appendChild(body); const document = Object.assign(element("#document"), { readyState: "complete", documentElement: root, body, activeElement: body, createElement: (tag) => element(tag), querySelector: () => null, querySelectorAll: (sel) => (sel.includes("data-rst-date") ? [native] : []), }); root.parentNode = document; const window = { innerHeight: 600 }; const context = { window, self: window, document, Event, CustomEvent, Intl, console, MutationObserver: class { observe() {} disconnect() {} }, setTimeout: (fn) => { timers.push(fn); return timers.length; }, clearTimeout() {}, }; vm.runInNewContext(src, context, { filename: "datetime.js" }); const input = field.children.find((c) => c.tagName === "DIV").children.find((c) => c.tagName === "INPUT" && c !== native); if (!input) throw new Error("enhance did not put a box beside the field"); const fire = (el, type, extra) => el.dispatchEvent(Object.assign({ type, bubbles: true, defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } }, extra || {})); return { native, input, // Types text into the box and commits it with Enter. commit(text) { input.focus(); input.value = text; fire(input, "input"); fire(input, "keydown", { key: "Enter" }); }, // Types text into the box without committing it. type(text) { input.focus(); input.value = text; fire(input, "input"); }, // Focus leaves the box: focus goes to the body, and focusout bubbles // from the box through the wrap the script listens on. blur() { document.activeElement = body; fire(input, "focusout", { bubbles: true }); }, // A form reset, accepted or cancelled by a listener registered for the purpose. reset(cancel) { if (cancel) form.addEventListener("reset", function once(e) { e.preventDefault(); }); const ev = { type: "reset", bubbles: true, target: form, defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } }; // The form's own listeners (the cancelling one), then the document's, as a bubbling event reaches them. for (const fn of (listenersOf.get(form) || {}).reset || []) fn.call(form, ev); for (const fn of (listenersOf.get(document) || {}).reset || []) fn.call(document, ev); listenersOf.set(form, {}); if (!ev.defaultPrevented) { native.value = native.getAttribute("value"); input.value = ""; } }, // Runs every queued timer in order, including ones queued while running. run() { while (timers.length) timers.shift()(); }, state() { return { native: native.value, shown: input.value }; }, }; } const out = {}; { const p = page(); out.shownLoad = p.input.value; p.commit("25 Dec 2026"); p.run(); out.changed = p.native.value; p.blur(); p.reset(false); p.run(); out.single = p.state(); } { const p = page(); p.commit("25 Dec 2026"); p.run(); p.type("31 Dec 2026"); p.blur(); p.reset(true); p.run(); out.cancelled = p.state(); } { const p = page(); p.commit("25 Dec 2026"); p.run(); p.blur(); p.reset(false); p.reset(true); p.run(); out.blurAcceptedCancelled = p.state(); } { const p = page(); p.commit("25 Dec 2026"); p.run(); p.reset(true); p.blur(); p.reset(false); p.run(); out.cancelledBlurAccepted = p.state(); } process.stdout.write(JSON.stringify(out));