v0.1.1: initial release of @based-dsh-diving/dsh-plugin
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).
This commit is contained in:
commit
4ea8234e6a
8 files changed
+641
No files matched your search
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
@@ -0,0 +1,24 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 based-dsh-diving contributors
|
||||
|
||||
Portions of the client-plugin structure are derived from
|
||||
ldgeng/dsh-claudecode-tips (MIT License, Copyright (c) 2026 ldgeng).
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,203 @@
|
||||
# based-dsh-diving
|
||||
|
||||
> Deep diving was never the vibe.
|
||||
|
||||
A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) plugin for the `web` profile. It swaps the static **Deep diving...** turn-status label for 96 rotating loading phrases drawn from the terminally-online end of the English language. The elapsed-time clock beside the label is left alone.
|
||||
|
||||
Inspired by [`ldgeng/dsh-claudecode-tips`](https://github.com/ldgeng/dsh-claudecode-tips), with the vibes recalibrated.
|
||||
|
||||
```
|
||||
Before: Deep diving... 12s
|
||||
After: Nuking the database... 12s
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
Requires the `web` profile and DSH `0.1.0-rc.6` or newer (out-of-tree bundle support).
|
||||
|
||||
```sh
|
||||
# straight from Gitea
|
||||
dsh plugin --profile web add git+https://gitea.milesward.dev/mward4/based-dsh-diving.git
|
||||
|
||||
# or from a local checkout
|
||||
dsh plugin --profile web add /path/to/based-dsh-diving
|
||||
```
|
||||
|
||||
Then restart the web UI:
|
||||
|
||||
```sh
|
||||
dsh --profile web
|
||||
```
|
||||
|
||||
### Verify it works
|
||||
|
||||
1. Open a session.
|
||||
2. Send a message.
|
||||
3. While the model works, the status line should cycle phrases every ~2.2s — `Burning tokens...`, `Yapping...`, `Tracing Satoshi Nakamoto...` — with the timer still counting on the right.
|
||||
|
||||
### Disable it
|
||||
|
||||
Without uninstalling, add this to the profile's `cordis.patch.yml`:
|
||||
|
||||
```yaml
|
||||
- id: based-dsh-diving
|
||||
disabled: true
|
||||
```
|
||||
|
||||
### Coexistence with `@claude-code-tips/dsh-plugin`
|
||||
|
||||
Both plugins claim the same `role="status"` label, so whichever loads first wins and the other goes quiet. Run one at a time:
|
||||
|
||||
```yaml
|
||||
- id: claude-code-tips
|
||||
disabled: true
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Everything tunable lives at the top of `client.js`:
|
||||
|
||||
| Constant | Default | Purpose |
|
||||
| ------------------ | ------- | --------------------------------------------------- |
|
||||
| `PHRASES` | 96 | Bare phrases — the `...` is appended at render time. |
|
||||
| `ROTATE_MS` | `2200` | How often the label changes while a turn runs. |
|
||||
| `SCAN_DEBOUNCE_MS` | `120` | Debounce for DOM rescans. |
|
||||
|
||||
To add your own, drop a string into `PHRASES` and restart the web UI. No build step.
|
||||
|
||||
## The phrases
|
||||
|
||||
<details open>
|
||||
<summary><strong>The originals</strong> (22)</summary>
|
||||
|
||||
```
|
||||
Draining oceans, Killing sea turtles, Half-assing responses, Burning tokens,
|
||||
Doomscrolling, Vibecoding, Deep diving, Bullshitting, Gaslighting,
|
||||
Procrastinating, Guessing, Glazing, Crashing out, Deleting all files,
|
||||
Melting glaciers, Evaporating a lake, Frying the power grid,
|
||||
Strip-mining for lithium, Nuking the database, Malding, Spiraling,
|
||||
Hallucinating
|
||||
```
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><strong>Chronically online</strong> (20)</summary>
|
||||
|
||||
```
|
||||
Yapping, Rizzing up the context window, Aura farming, Losing aura,
|
||||
Letting him cook, Locking in, Fumbling the bag, Securing the bag,
|
||||
Standing on business, Thugging it out, Schizoposting, Touching grass,
|
||||
Catching feelings, Having a whole episode, Doing donuts in the parking lot,
|
||||
Rage-quitting mid-thought, Silently judging you, Speedrunning your codebase,
|
||||
Pretending to work, Panicking quietly
|
||||
```
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><strong>Engineering crimes</strong> (9)</summary>
|
||||
|
||||
```
|
||||
Refactoring everything for no reason, Renaming everything to data,
|
||||
Force-pushing to prod, Blaming the intern, Gaslighting the compiler,
|
||||
Copy-pasting from Stack Overflow, Blaming DNS, Consulting the rubber duck,
|
||||
Shipping it anyway
|
||||
```
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><strong>Crashed out</strong> (10)</summary>
|
||||
|
||||
```
|
||||
Dissociating, Blacking out, Losing the plot, Watching paint dry,
|
||||
Staring at the wall, Buffering, Texting your ex, Buying more RAM,
|
||||
Downloading more RAM, Mining bitcoin
|
||||
```
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><strong>Superstition</strong> (5)</summary>
|
||||
|
||||
```
|
||||
Reading your horoscope, Blaming Mercury retrograde, Praying,
|
||||
Flipping a coin, Rerolling the dice
|
||||
```
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><strong>Hardware abuse</strong> (4)</summary>
|
||||
|
||||
```
|
||||
Shoveling coal into the GPU, Blowing on the cartridge, Wiggling the cable,
|
||||
Reseating the RAM
|
||||
```
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><strong>Eco-villain arc</strong> (13)</summary>
|
||||
|
||||
```
|
||||
Boiling the ocean, Siphoning the water table, Deforesting the Amazon,
|
||||
Burning down the server farm, Farting in the datacenter, Blowing a transformer,
|
||||
Draining the reservoir, Murdering the ozone layer, Starting a land war in Asia,
|
||||
Running the AC with the window open, Idling a jet engine,
|
||||
Launching a rocket for no reason, Causing a minor international incident
|
||||
```
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><strong>Financial damage</strong> (7)</summary>
|
||||
|
||||
```
|
||||
Embezzling tokens, Burning your API budget, Costing you $47 in one turn,
|
||||
Draining your credit card, Burning the rent money, Money-laundering,
|
||||
Overdrafting the account
|
||||
```
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><strong>Federal crimes</strong> (6)</summary>
|
||||
|
||||
```
|
||||
Selling data to China, Exporting APIs to China, Selling your data to advertisers,
|
||||
Parsing Hunter Biden's laptop, Installing key-logger, Tracing Satoshi Nakamoto
|
||||
```
|
||||
</details>
|
||||
|
||||
## How it works
|
||||
|
||||
A DSH plugin bundle is three small pieces:
|
||||
|
||||
| File | Role |
|
||||
| ------------------- | ------------------------------------------------------------------------ |
|
||||
| `index.js` | Host-side half. Deliberately a no-op — the work happens in the browser. |
|
||||
| `client.js` | Browser-half bundle: finds the status label and rewrites its text node. |
|
||||
| `cordis.patch.yml` | Inserts the `based-dsh-diving` row into the `web` profile's plugin tree. |
|
||||
|
||||
`client.js` locates the label via `[role="status"]` and matches its text against the stock label in every shipped locale (`Deep diving...` / `深度求索中...`). It then:
|
||||
|
||||
- **claims** the text node and swaps in a random phrase,
|
||||
- **re-asserts** the claim if React reconciles the label back to the stock string,
|
||||
- **rotates** the phrase on a timer, so it keeps moving even when streaming never re-renders the label.
|
||||
|
||||
Only the label text node is touched. The clock span, `aria-live`, and the shimmer styling are untouched.
|
||||
|
||||
No server code, no model prompt changes, no fork of DSH.
|
||||
|
||||
## Development
|
||||
|
||||
No build step, no runtime dependencies.
|
||||
|
||||
```sh
|
||||
node test/client.test.mjs
|
||||
```
|
||||
|
||||
The test runs `client.js` against a minimal DOM stub and checks six behaviours: initial claim, clock untouched, React revert re-asserted, timer rotation, localized-label detection, and clean disposal. All checks print `PASS`/`FAIL`.
|
||||
|
||||
## Credits
|
||||
|
||||
- Architecture inspired by [`ldgeng/dsh-claudecode-tips`](https://github.com/ldgeng/dsh-claudecode-tips) (MIT).
|
||||
- Built for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness).
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
@@ -0,0 +1,272 @@
|
||||
window.__ModuleLoader__.load({
|
||||
id: "@based-dsh-diving/dsh-plugin",
|
||||
factory: (require) => {
|
||||
var module = { exports: {} };
|
||||
var exports = module.exports;
|
||||
|
||||
// Phrases shown while a turn is running, in place of the static
|
||||
// "Deep diving..." status label. Bare phrases: the ellipsis is appended at
|
||||
// render time so the list stays easy to edit.
|
||||
var PHRASES = [
|
||||
// ---- the originals ----
|
||||
"Draining oceans",
|
||||
"Killing sea turtles",
|
||||
"Half-assing responses",
|
||||
"Burning tokens",
|
||||
"Doomscrolling",
|
||||
"Vibecoding",
|
||||
"Deep diving",
|
||||
"Bullshitting",
|
||||
"Gaslighting",
|
||||
"Procrastinating",
|
||||
"Guessing",
|
||||
"Glazing",
|
||||
"Crashing out",
|
||||
"Deleting all files",
|
||||
"Melting glaciers",
|
||||
"Evaporating a lake",
|
||||
"Frying the power grid",
|
||||
"Strip-mining for lithium",
|
||||
"Nuking the database",
|
||||
"Malding",
|
||||
"Spiraling",
|
||||
"Hallucinating",
|
||||
|
||||
// ---- chronically online ----
|
||||
"Yapping",
|
||||
"Rizzing up the context window",
|
||||
"Aura farming",
|
||||
"Losing aura",
|
||||
"Letting him cook",
|
||||
"Locking in",
|
||||
"Fumbling the bag",
|
||||
"Securing the bag",
|
||||
"Standing on business",
|
||||
"Thugging it out",
|
||||
"Schizoposting",
|
||||
"Touching grass",
|
||||
"Catching feelings",
|
||||
"Having a whole episode",
|
||||
"Doing donuts in the parking lot",
|
||||
"Rage-quitting mid-thought",
|
||||
"Silently judging you",
|
||||
"Speedrunning your codebase",
|
||||
"Pretending to work",
|
||||
"Panicking quietly",
|
||||
|
||||
// ---- engineering crimes ----
|
||||
"Refactoring everything for no reason",
|
||||
"Renaming everything to data",
|
||||
"Force-pushing to prod",
|
||||
"Blaming the intern",
|
||||
"Gaslighting the compiler",
|
||||
"Copy-pasting from Stack Overflow",
|
||||
"Blaming DNS",
|
||||
"Consulting the rubber duck",
|
||||
"Shipping it anyway",
|
||||
|
||||
// ---- crashed out ----
|
||||
"Dissociating",
|
||||
"Blacking out",
|
||||
"Losing the plot",
|
||||
"Watching paint dry",
|
||||
"Staring at the wall",
|
||||
"Buffering",
|
||||
"Texting your ex",
|
||||
"Buying more RAM",
|
||||
"Downloading more RAM",
|
||||
"Mining bitcoin",
|
||||
|
||||
// ---- superstition ----
|
||||
"Reading your horoscope",
|
||||
"Blaming Mercury retrograde",
|
||||
"Praying",
|
||||
"Flipping a coin",
|
||||
"Rerolling the dice",
|
||||
|
||||
// ---- hardware abuse ----
|
||||
"Shoveling coal into the GPU",
|
||||
"Blowing on the cartridge",
|
||||
"Wiggling the cable",
|
||||
"Reseating the RAM",
|
||||
|
||||
// ---- eco-villain arc ----
|
||||
"Boiling the ocean",
|
||||
"Siphoning the water table",
|
||||
"Deforesting the Amazon",
|
||||
"Burning down the server farm",
|
||||
"Farting in the datacenter",
|
||||
"Blowing a transformer",
|
||||
"Draining the reservoir",
|
||||
"Murdering the ozone layer",
|
||||
"Starting a land war in Asia",
|
||||
"Running the AC with the window open",
|
||||
"Idling a jet engine",
|
||||
"Launching a rocket for no reason",
|
||||
"Causing a minor international incident",
|
||||
|
||||
// ---- financial damage ----
|
||||
"Embezzling tokens",
|
||||
"Burning your API budget",
|
||||
"Costing you $47 in one turn",
|
||||
"Draining your credit card",
|
||||
"Burning the rent money",
|
||||
"Money-laundering",
|
||||
"Overdrafting the account",
|
||||
|
||||
// ---- federal crimes ----
|
||||
"Selling data to China",
|
||||
"Exporting APIs to China",
|
||||
"Selling your data to advertisers",
|
||||
"Parsing Hunter Biden's laptop",
|
||||
"Installing key-logger",
|
||||
"Tracing Satoshi Nakamoto"
|
||||
];
|
||||
|
||||
var SUFFIX = "...";
|
||||
var ROTATE_MS = 2200;
|
||||
var SCAN_DEBOUNCE_MS = 120;
|
||||
|
||||
// The official status label, in every shipped locale. React re-renders the
|
||||
// label back to this text, which is how we recognize a node worth claiming.
|
||||
var LABEL_PATTERN = /^\s*(?:deep\s*diving|深度求索中)\s*(?:\.{3}|…)?\s*$/i;
|
||||
|
||||
// element -> { node, text } for every status label we currently own.
|
||||
var owned = new WeakMap();
|
||||
|
||||
var lastIndex = -1;
|
||||
|
||||
function nextPhrase() {
|
||||
var index;
|
||||
do {
|
||||
index = Math.floor(Math.random() * PHRASES.length);
|
||||
} while (index === lastIndex && PHRASES.length > 1);
|
||||
lastIndex = index;
|
||||
return PHRASES[index] + SUFFIX;
|
||||
}
|
||||
|
||||
function isLive(node) {
|
||||
return !!node && node.isConnected === true;
|
||||
}
|
||||
|
||||
// First direct text child that either looks like the stock label or is the
|
||||
// node we already claimed. Its siblings (the elapsed-time clock) are left
|
||||
// untouched.
|
||||
function findLabelNode(element) {
|
||||
var record = owned.get(element);
|
||||
if (record && isLive(record.node) && element.contains(record.node)) {
|
||||
return record.node;
|
||||
}
|
||||
for (var node = element.firstChild; node; node = node.nextSibling) {
|
||||
if (node.nodeType === Node.TEXT_NODE && LABEL_PATTERN.test(node.nodeValue || "")) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Claim an unclaimed status element, or re-assert a claim React reverted.
|
||||
function claim(element) {
|
||||
var record = owned.get(element);
|
||||
|
||||
if (!record) {
|
||||
var fresh = findLabelNode(element);
|
||||
if (!fresh) return;
|
||||
var text = nextPhrase();
|
||||
fresh.nodeValue = text;
|
||||
owned.set(element, { node: fresh, text: text });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isLive(record.node) || !element.contains(record.node)) {
|
||||
owned.delete(element);
|
||||
claim(element);
|
||||
return;
|
||||
}
|
||||
|
||||
// React reconciled the label back to "Deep diving...": put our text back.
|
||||
if (record.node.nodeValue !== record.text) {
|
||||
record.node.nodeValue = record.text;
|
||||
}
|
||||
}
|
||||
|
||||
function statusElements() {
|
||||
return document.querySelectorAll('[role="status"]');
|
||||
}
|
||||
|
||||
function scan() {
|
||||
var elements = statusElements();
|
||||
for (var i = 0; i < elements.length; i += 1) {
|
||||
claim(elements[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Rotate every label we own so the text keeps moving even when React never
|
||||
// re-renders the label node (streaming updates usually leave it alone).
|
||||
function rotate() {
|
||||
var elements = statusElements();
|
||||
for (var i = 0; i < elements.length; i += 1) {
|
||||
var element = elements[i];
|
||||
var record = owned.get(element);
|
||||
if (!record) {
|
||||
claim(element);
|
||||
continue;
|
||||
}
|
||||
if (!isLive(record.node) || !element.contains(record.node)) {
|
||||
owned.delete(element);
|
||||
claim(element);
|
||||
continue;
|
||||
}
|
||||
var text = nextPhrase();
|
||||
record.text = text;
|
||||
record.node.nodeValue = text;
|
||||
}
|
||||
}
|
||||
|
||||
function apply(ctx) {
|
||||
ctx.effect(function () {
|
||||
if (typeof document === "undefined" || typeof MutationObserver === "undefined") {
|
||||
return function () {};
|
||||
}
|
||||
|
||||
scan();
|
||||
|
||||
var pending = null;
|
||||
|
||||
function scheduleScan() {
|
||||
if (pending !== null) return;
|
||||
pending = setTimeout(function () {
|
||||
pending = null;
|
||||
scan();
|
||||
}, SCAN_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
// Debounced: streaming turns mutate the transcript constantly, and we
|
||||
// only care about the handful of status labels.
|
||||
var observer = new MutationObserver(scheduleScan);
|
||||
observer.observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true
|
||||
});
|
||||
|
||||
// React's label re-renders land right after the sweep; re-claim once
|
||||
// more so a fresh turn never flashes the stock text.
|
||||
var interval = setInterval(rotate, ROTATE_MS);
|
||||
|
||||
return function () {
|
||||
observer.disconnect();
|
||||
clearInterval(interval);
|
||||
if (pending !== null) {
|
||||
clearTimeout(pending);
|
||||
pending = null;
|
||||
}
|
||||
owned = new WeakMap();
|
||||
};
|
||||
}, "based-dsh-diving: rotate turn-status loading text");
|
||||
}
|
||||
|
||||
exports.apply = apply;
|
||||
return module.exports;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
# @based-dsh-diving/dsh-plugin
|
||||
#
|
||||
# A client-side bundle layer for the web profile: it inserts a browser plugin
|
||||
# that watches the chat turn status and replaces the static "Deep diving..."
|
||||
# label with rotating, chronically online loading text while a turn is running.
|
||||
# The elapsed-time clock beside the label is preserved.
|
||||
#
|
||||
# Later layers (profile cordis.patch.yml, $DSH_HOME/cordis.patch.yml, --patch)
|
||||
# can re-patch or disable this row with:
|
||||
#
|
||||
# - id: based-dsh-diving
|
||||
# disabled: true
|
||||
|
||||
- insert:
|
||||
- id: based-dsh-diving
|
||||
name: '@based-dsh-diving/dsh-plugin'
|
||||
@@ -0,0 +1,6 @@
|
||||
// Host-side half of @based-dsh-diving/dsh-plugin.
|
||||
//
|
||||
// This package is deliberately a no-op on the host: the plugin's entire job is
|
||||
// to ship the browser-side `client.js` bundle to the web UI. The cordis row
|
||||
// still needs a valid host module, so this stub satisfies the loader.
|
||||
export function apply() {}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@based-dsh-diving/dsh-plugin",
|
||||
"description": "Replace DeepSeek Harness's 'Deep diving...' turn status with rotating, chronically online loading text.",
|
||||
"version": "0.1.1",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"exports": {
|
||||
".": "./index.js",
|
||||
"./client": "./client.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"client.js",
|
||||
"cordis.patch.yml",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "node test/client.test.mjs"
|
||||
},
|
||||
"dsh": {
|
||||
"bundle": {
|
||||
"patch": "./cordis.patch.yml"
|
||||
},
|
||||
"client": {
|
||||
"platform": "web"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// 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);
|
||||
Reference in new issue
Block a user