DSH web-profile plugin bundle that replaces the static "Deep diving..." turn-status label with 96 rotating loading phrases. Entirely browser-side: it claims the [role="status"] label text node, re-asserts the claim when React reconciles the stock string back, and rotates phrases on a 2.2s timer so the label keeps moving even when streaming never re-renders it. The elapsed-time clock, aria-live, and shimmer styling are untouched. Includes a headless DOM-stub test covering initial claim, clock preservation, React revert, timer rotation, localized-label detection (Deep diving... / 深度求索中...), and clean disposal. Architecture inspired by ldgeng/dsh-claudecode-tips (MIT).
89 lines
3.5 KiB
JavaScript
89 lines
3.5 KiB
JavaScript
// Headless harness for based-dsh-diving/client.js — stubs just enough DOM to
|
|
// exercise claim / React-revert / rotate behaviour without a browser.
|
|
import { readFileSync } from "node:fs";
|
|
|
|
const capturedLoads = [];
|
|
globalThis.window = { __ModuleLoader__: { load: (def) => capturedLoads.push(def) } };
|
|
globalThis.Node = { TEXT_NODE: 3, ELEMENT_NODE: 1 };
|
|
|
|
let observerCallback = null;
|
|
globalThis.MutationObserver = class {
|
|
constructor(cb) { observerCallback = cb; }
|
|
observe() {}
|
|
disconnect() { observerCallback = null; }
|
|
};
|
|
|
|
class TextNode {
|
|
constructor(value) { this.nodeType = 3; this.nodeValue = value; this.parent = null; this.nextSibling = null; }
|
|
get isConnected() { return this.parent !== null; }
|
|
}
|
|
class El {
|
|
constructor() { this.nodeType = 1; this.children = []; this.parent = null; this.nextSibling = null; }
|
|
get isConnected() { return this.parent !== null || this.rooted; }
|
|
get firstChild() { return this.children[0] ?? null; }
|
|
append(child) { child.parent = this; this.children.push(child); child.nextSibling = null; this._relink(); }
|
|
contains(node) { let p = node; while (p) { if (p === this) return true; p = p.parent; } return false; }
|
|
_relink() { this.children.forEach((c, i) => { c.nextSibling = this.children[i + 1] ?? null; }); }
|
|
}
|
|
|
|
const root = new El();
|
|
root.rooted = true;
|
|
const status = new El();
|
|
status.rooted = true;
|
|
const label = new TextNode("Deep diving...");
|
|
const clock = new El();
|
|
clock.append(new TextNode("12s"));
|
|
status.append(label);
|
|
status.append(clock);
|
|
root.append(status);
|
|
|
|
let statuses = [status];
|
|
globalThis.document = {
|
|
documentElement: root,
|
|
querySelectorAll: (sel) => (sel === '[role="status"]' ? statuses : [])
|
|
};
|
|
|
|
await import(new URL("../client.js", import.meta.url));
|
|
if (capturedLoads.length !== 1) throw new Error(`expected 1 load, got ${capturedLoads.length}`);
|
|
const def = capturedLoads[0];
|
|
if (def.id !== "@based-dsh-diving/dsh-plugin") throw new Error(`bad id: ${def.id}`);
|
|
|
|
const mod = def.factory(() => { throw new Error("require not expected"); });
|
|
let cleanup = null;
|
|
mod.apply({ effect: (fn) => { cleanup = fn(); } });
|
|
|
|
const ok = (name, cond, extra = "") => console.log(`${cond ? "PASS" : "FAIL"} ${name}${extra ? " " + extra : ""}`);
|
|
|
|
ok("label claimed on apply", /\.\.\.$/.test(label.nodeValue) && !/deep diving/i.test(label.nodeValue), JSON.stringify(label.nodeValue));
|
|
ok("clock untouched", clock.children[0].nodeValue === "12s");
|
|
|
|
const first = label.nodeValue;
|
|
|
|
// React re-renders the stock label back onto the same text node.
|
|
label.nodeValue = "Deep diving...";
|
|
observerCallback?.();
|
|
await new Promise((r) => setTimeout(r, 200));
|
|
ok("React revert re-asserted by debounced scan", label.nodeValue === first, JSON.stringify(label.nodeValue));
|
|
|
|
// Rotation moves the text on its own.
|
|
await new Promise((r) => setTimeout(r, 2400));
|
|
ok("phrase rotates on interval", label.nodeValue !== first, JSON.stringify(label.nodeValue));
|
|
|
|
// Chinese locale label is recognized too (fresh status element, as React would
|
|
// mount for the next turn).
|
|
const zhStatus = new El();
|
|
zhStatus.rooted = true;
|
|
const zh = new TextNode("深度求索中...");
|
|
zh.parent = zhStatus;
|
|
zhStatus.children = [zh];
|
|
statuses = [zhStatus];
|
|
observerCallback?.();
|
|
await new Promise((r) => setTimeout(r, 200));
|
|
ok("zh label claimed", /\.\.\.$/.test(zh.nodeValue) && !/深度求索中/.test(zh.nodeValue), JSON.stringify(zh.nodeValue));
|
|
|
|
// Disposal stops rotation.
|
|
cleanup();
|
|
const frozen = zh.nodeValue;
|
|
await new Promise((r) => setTimeout(r, 2400));
|
|
ok("disposer stops rotation", zh.nodeValue === frozen);
|