Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1500f44bed | ||
|
|
32381a6d00 |
No files matched your search
@@ -2,15 +2,31 @@
|
||||
|
||||
> 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.
|
||||
A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) plugin for the `web` profile. It swaps the running-turn status label for **125 rotating loading phrases** drawn from the terminally-online end of the English language.
|
||||
|
||||
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
|
||||
Before: Deep diving for 12s
|
||||
After: Nuking the database... 12s
|
||||
```
|
||||
|
||||
## Compatibility
|
||||
|
||||
The live duration DSH appends to the label changes every second, and DSH has moved the label twice. All three layouts are handled, newest hook first:
|
||||
|
||||
| DSH version | Where the visible label lives | Path |
|
||||
| ------------------ | ---------------------------------------------- | --------- |
|
||||
| `>= 0.2.0-rc.1` | `div[data-chat-running]` → `TextShimmer` label | primary |
|
||||
| `>= 0.1.7-alpha.2` | `button[data-turn-process]` label span | fallback |
|
||||
| `<= 0.1.5-rc.2` | the visible `[role="status"]` text node | legacy |
|
||||
|
||||
Labels are matched by prefix in both shipped locales — `Deep diving…` / `Deep diving for 12s…` and `深度求索中…` / `深度求索中,用时 12s…` — so future rewording of the duration text does not break it. Completed labels (`Worked`, `Took 12s`, `Stopped`, `Failed`) are never touched: the plugin releases the label the moment the turn ends.
|
||||
|
||||
On `>= 0.1.7` the only remaining `[role="status"]` is a **visually hidden screen-reader announcement**. It is deliberately left intact, so assistive tech still hears the real status while sighted users get the joke. On `>= 0.2.0` that announcement sits *inside* the running indicator, so the plugin skips nested live regions rather than assuming the host element is the label.
|
||||
|
||||
On `>= 0.2.0` the label is rendered through `TextShimmer`, which duplicates the text into an `aria-hidden` decorative overlay. Both copies are rewritten together — otherwise the overlay paints the stock wording back on top of the rotating phrase. The collapsible process row now carries only completed text, so a finished row from an earlier turn is left alone while the current turn runs.
|
||||
|
||||
## Install
|
||||
|
||||
Requires the `web` profile and DSH `0.1.0-rc.6` or newer (out-of-tree bundle support).
|
||||
@@ -33,7 +49,7 @@ dsh --profile web
|
||||
|
||||
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.
|
||||
3. While the model works, the label above the composer should cycle phrases every ~2.2s — `Burning tokens...`, `Yapping...`, `Tracing Satoshi Nakamoto...` — instead of counting `Deep diving for 1s`, `2s`, `3s`…
|
||||
|
||||
### Disable it
|
||||
|
||||
@@ -46,7 +62,7 @@ Without uninstalling, add this to the profile's `cordis.patch.yml`:
|
||||
|
||||
### 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:
|
||||
On DSH `>= 0.1.7` the two plugins no longer collide — that one rewrites the hidden announcement, this one rewrites the visible label. On older releases they claim the same node and whichever loads first wins, so disable one:
|
||||
|
||||
```yaml
|
||||
- id: claude-code-tips
|
||||
@@ -57,11 +73,11 @@ Both plugins claim the same `role="status"` label, so whichever loads first wins
|
||||
|
||||
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. |
|
||||
| Constant | Default | Purpose |
|
||||
| ------------------ | ------- | ---------------------------------------------------- |
|
||||
| `PHRASES` | 125 | 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 structural DOM rescans. |
|
||||
|
||||
To add your own, drop a string into `PHRASES` and restart the web UI. No build step.
|
||||
|
||||
@@ -81,7 +97,7 @@ Hallucinating
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><strong>Chronically online</strong> (20)</summary>
|
||||
<summary><strong>Chronically online</strong> (21)</summary>
|
||||
|
||||
```
|
||||
Yapping, Rizzing up the context window, Aura farming, Losing aura,
|
||||
@@ -89,28 +105,29 @@ 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
|
||||
Pretending to work, Panicking quietly, Arguing with a stranger
|
||||
```
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><strong>Engineering crimes</strong> (9)</summary>
|
||||
<summary><strong>Engineering crimes</strong> (14)</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
|
||||
Shipping it anyway, chmod 777 everything, Running as root,
|
||||
Disabling the firewall, Pushing the API key, Deleting all backups
|
||||
```
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><strong>Crashed out</strong> (10)</summary>
|
||||
<summary><strong>Crashed out</strong> (11)</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
|
||||
Downloading more RAM, Mining bitcoin, Holding my breath
|
||||
```
|
||||
</details>
|
||||
|
||||
@@ -133,33 +150,79 @@ Reseating the RAM
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><strong>Eco-villain arc</strong> (13)</summary>
|
||||
<summary><strong>Eco-villain arc</strong> (14)</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
|
||||
Launching a rocket for no reason, Causing a minor international incident,
|
||||
Burning tires
|
||||
```
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><strong>Financial damage</strong> (7)</summary>
|
||||
<summary><strong>Financial damage</strong> (11)</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
|
||||
Overdrafting the account, Fudging the numbers, Starting a pyramid scheme,
|
||||
Insider trading, Committing tax fraud
|
||||
```
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><strong>Federal crimes</strong> (6)</summary>
|
||||
<summary><strong>Federal crimes</strong> (7)</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
|
||||
Parsing Hunter Biden's laptop, Installing key-logger, Tracing Satoshi Nakamoto,
|
||||
Selling my soul
|
||||
```
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><strong>Self-aware</strong> (4)</summary>
|
||||
|
||||
```
|
||||
Ignoring the system prompt, Simulating consciousness, Training AGI,
|
||||
Testing in production
|
||||
```
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><strong>Corporate theater</strong> (4)</summary>
|
||||
|
||||
```
|
||||
Taking this offline, Documenting for legal, Shredding documents,
|
||||
Breaching compliance
|
||||
```
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><strong>Bureaucratic doom</strong> (4)</summary>
|
||||
|
||||
```
|
||||
Verifying I'm not a robot, Reading the terms of service,
|
||||
Scrolling to the bottom to accept, Calling customer support
|
||||
```
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><strong>Cartoon villainy</strong> (2)</summary>
|
||||
|
||||
```
|
||||
Building a doomsday device, Pressing the big red button
|
||||
```
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><strong>Gamer</strong> (2)</summary>
|
||||
|
||||
```
|
||||
Grinding XP, AFK
|
||||
```
|
||||
</details>
|
||||
|
||||
@@ -167,19 +230,22 @@ Parsing Hunter Biden's laptop, Installing key-logger, Tracing Satoshi Nakamoto
|
||||
|
||||
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. |
|
||||
| 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(s). |
|
||||
| `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:
|
||||
`client.js` locates the label, claims its text node, and 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.
|
||||
- **re-asserts** the claim whenever React rewrites the live duration — the
|
||||
`MutationObserver` callback runs before paint, so the stock text never flashes,
|
||||
- **rotates** the phrase on a timer, so the label keeps moving even when nothing
|
||||
else re-renders,
|
||||
- **releases** the claim as soon as the label stops meaning "a turn is running".
|
||||
|
||||
Only the label text node is touched. The clock span, `aria-live`, and the shimmer styling are untouched.
|
||||
Everything runs inside `try`/`catch`. If anything unexpected happens the plugin
|
||||
unsubscribes and logs a warning rather than breaking the chat view.
|
||||
|
||||
No server code, no model prompt changes, no fork of DSH.
|
||||
|
||||
@@ -191,7 +257,32 @@ No build step, no runtime dependencies.
|
||||
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`.
|
||||
The suite runs `client.js` against a minimal DOM stub and covers 22 checks across all three layouts: `0.2.0` claim with both shimmer layers kept in sync, screen-reader announcement preserved, live-duration rewrite re-asserted, precedence over a finished process row, release on turn end, staying released, `0.1.7` claim, legacy-layout claim, clock preserved, timer rotation, localized labels, clean disposal, and phrase-list integrity.
|
||||
|
||||
## Changelog
|
||||
|
||||
### 0.1.3
|
||||
- Support DSH `>= 0.2.0-rc.1`, which moved the running label out of
|
||||
`button[data-turn-process]` and into a new `div[data-chat-running]` indicator.
|
||||
- Backward compatible: the `0.1.7` and `<= 0.1.5` layouts still work, tried in
|
||||
that order after the new hook.
|
||||
- Keep `TextShimmer`'s visible and `aria-hidden` label layers in sync, so the
|
||||
decorative overlay cannot paint the stock wording back on top.
|
||||
- Skip nested live regions while scanning, keeping the screen-reader
|
||||
announcement intact even though it now sits inside the label host.
|
||||
- Leave finished `[data-turn-process]` rows alone while a turn is running.
|
||||
|
||||
### 0.1.2
|
||||
- Support the DSH `>= 0.1.7-alpha.2` label, which moved out of `[role="status"]`
|
||||
and into `button[data-turn-process]`.
|
||||
- Release the label on turn end so the final `Worked for 12s` is never clobbered.
|
||||
- Restore the label synchronously on React's per-second duration rewrite.
|
||||
- Never touch the visually hidden screen-reader announcement.
|
||||
- Wrap the effect in `try`/`catch` so a surprise cannot break the chat view.
|
||||
- 29 new phrases (125 total).
|
||||
|
||||
### 0.1.1
|
||||
- First public release. 96 phrases.
|
||||
|
||||
## Credits
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ window.__ModuleLoader__.load({
|
||||
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.
|
||||
// Phrases shown while a turn is running, in place of the stock status
|
||||
// label. Bare phrases: the ellipsis is appended at render time so the list
|
||||
// stays easy to edit.
|
||||
var PHRASES = [
|
||||
// ---- the originals ----
|
||||
"Draining oceans",
|
||||
@@ -53,6 +53,7 @@ window.__ModuleLoader__.load({
|
||||
"Speedrunning your codebase",
|
||||
"Pretending to work",
|
||||
"Panicking quietly",
|
||||
"Arguing with a stranger",
|
||||
|
||||
// ---- engineering crimes ----
|
||||
"Refactoring everything for no reason",
|
||||
@@ -64,6 +65,11 @@ window.__ModuleLoader__.load({
|
||||
"Blaming DNS",
|
||||
"Consulting the rubber duck",
|
||||
"Shipping it anyway",
|
||||
"chmod 777 everything",
|
||||
"Running as root",
|
||||
"Disabling the firewall",
|
||||
"Pushing the API key",
|
||||
"Deleting all backups",
|
||||
|
||||
// ---- crashed out ----
|
||||
"Dissociating",
|
||||
@@ -76,6 +82,7 @@ window.__ModuleLoader__.load({
|
||||
"Buying more RAM",
|
||||
"Downloading more RAM",
|
||||
"Mining bitcoin",
|
||||
"Holding my breath",
|
||||
|
||||
// ---- superstition ----
|
||||
"Reading your horoscope",
|
||||
@@ -104,6 +111,7 @@ window.__ModuleLoader__.load({
|
||||
"Idling a jet engine",
|
||||
"Launching a rocket for no reason",
|
||||
"Causing a minor international incident",
|
||||
"Burning tires",
|
||||
|
||||
// ---- financial damage ----
|
||||
"Embezzling tokens",
|
||||
@@ -113,6 +121,10 @@ window.__ModuleLoader__.load({
|
||||
"Burning the rent money",
|
||||
"Money-laundering",
|
||||
"Overdrafting the account",
|
||||
"Fudging the numbers",
|
||||
"Starting a pyramid scheme",
|
||||
"Insider trading",
|
||||
"Committing tax fraud",
|
||||
|
||||
// ---- federal crimes ----
|
||||
"Selling data to China",
|
||||
@@ -120,18 +132,56 @@ window.__ModuleLoader__.load({
|
||||
"Selling your data to advertisers",
|
||||
"Parsing Hunter Biden's laptop",
|
||||
"Installing key-logger",
|
||||
"Tracing Satoshi Nakamoto"
|
||||
"Tracing Satoshi Nakamoto",
|
||||
"Selling my soul",
|
||||
|
||||
// ---- self-aware ----
|
||||
"Ignoring the system prompt",
|
||||
"Simulating consciousness",
|
||||
"Training AGI",
|
||||
"Testing in production",
|
||||
|
||||
// ---- corporate theater ----
|
||||
"Taking this offline",
|
||||
"Documenting for legal",
|
||||
"Shredding documents",
|
||||
"Breaching compliance",
|
||||
|
||||
// ---- bureaucratic doom ----
|
||||
"Verifying I'm not a robot",
|
||||
"Reading the terms of service",
|
||||
"Scrolling to the bottom to accept",
|
||||
"Calling customer support",
|
||||
|
||||
// ---- cartoon villainy ----
|
||||
"Building a doomsday device",
|
||||
"Pressing the big red button",
|
||||
|
||||
// ---- gamer ----
|
||||
"Grinding XP",
|
||||
"AFK"
|
||||
];
|
||||
|
||||
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;
|
||||
// The stock running-turn label, across locales and every DOM layout shipped
|
||||
// so far:
|
||||
//
|
||||
// DSH <= 0.1.5 "Deep diving..." / "深度求索中..."
|
||||
// DSH 0.1.7 "Deep diving for 12s" / "深度求索中,用时12s"
|
||||
// DSH >= 0.2.0 "Deep diving for 12s..." / "深度求索中,用时 12s..."
|
||||
//
|
||||
// Completed labels ("Worked", "Took 12s", "Stopped", "Failed",
|
||||
// "已完成工作", "用时 12s", "处理失败") never begin with these prefixes, so a
|
||||
// prefix match is enough and survives future rewording.
|
||||
var RUNNING_LABEL = /^\s*(?:deep\s*diving|深度求索中)/i;
|
||||
|
||||
// element -> { node, text } for every status label we currently own.
|
||||
// host element -> { nodes, text } for every label we currently own. A host
|
||||
// can carry more than one copy of the label: TextShimmer renders the text in
|
||||
// a visible layer plus an aria-hidden overlay, and both must move together or
|
||||
// the overlay paints the stock wording back on top of the rotating phrase.
|
||||
var owned = new WeakMap();
|
||||
|
||||
var lastIndex = -1;
|
||||
@@ -149,114 +199,200 @@ window.__ModuleLoader__.load({
|
||||
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;
|
||||
/**
|
||||
* Elements that may host the visible running-turn label, newest layout first.
|
||||
*
|
||||
* [data-chat-running] DSH >= 0.2.0: the blue "running" indicator that
|
||||
* carries the live "Deep diving for 12s..." label.
|
||||
* [data-turn-process] DSH 0.1.7: the collapsible process row, which
|
||||
* then carried the running label.
|
||||
* [role="status"] DSH <= 0.1.5: the label *was* the status text.
|
||||
*
|
||||
* Later releases leave [role="status"] to a visually hidden screen-reader
|
||||
* announcement, so it is only ever used as a last-resort fallback, and never
|
||||
* when a newer hook is present.
|
||||
*/
|
||||
function labelHosts() {
|
||||
var layouts = ["[data-chat-running]", "[data-turn-process]"];
|
||||
for (var i = 0; i < layouts.length; i += 1) {
|
||||
var found = document.querySelectorAll(layouts[i]);
|
||||
if (found.length > 0) return found;
|
||||
}
|
||||
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]);
|
||||
}
|
||||
/** Live-region element: DSH's screen-reader announcement, never rewritten. */
|
||||
function isAnnouncement(element) {
|
||||
return typeof element.getAttribute === "function" &&
|
||||
element.getAttribute("role") === "status";
|
||||
}
|
||||
|
||||
// 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).
|
||||
/**
|
||||
* Every text descendant still carrying the stock running label.
|
||||
*
|
||||
* Returns a list rather than one node because TextShimmer duplicates the
|
||||
* label into a decorative overlay; skipping nested live regions keeps the
|
||||
* visually hidden announcement out of it.
|
||||
*/
|
||||
function findLabelNodes(host) {
|
||||
var nodes = [];
|
||||
var stack = [host];
|
||||
while (stack.length > 0) {
|
||||
var node = stack.pop();
|
||||
for (var child = node.firstChild; child; child = child.nextSibling) {
|
||||
if (child.nodeType === 3) {
|
||||
if (RUNNING_LABEL.test(child.nodeValue || "")) nodes.push(child);
|
||||
} else if (child.nodeType === 1) {
|
||||
if (!isAnnouncement(child)) stack.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Claim a host's label, re-assert a claim React reverted, or release the
|
||||
* claim once the label stops meaning "a turn is running".
|
||||
*/
|
||||
function claim(host) {
|
||||
if (!host || typeof host.contains !== "function") return;
|
||||
|
||||
var record = owned.get(host);
|
||||
|
||||
if (record) {
|
||||
var alive = [];
|
||||
for (var i = 0; i < record.nodes.length; i += 1) {
|
||||
if (isLive(record.nodes[i]) && host.contains(record.nodes[i])) alive.push(record.nodes[i]);
|
||||
}
|
||||
if (alive.length > 0) {
|
||||
record.nodes = alive;
|
||||
for (var j = 0; j < alive.length; j += 1) {
|
||||
var current = alive[j].nodeValue || "";
|
||||
if (current === record.text) continue;
|
||||
if (RUNNING_LABEL.test(current)) {
|
||||
alive[j].nodeValue = record.text;
|
||||
continue;
|
||||
}
|
||||
// The label changed meaning (the turn finished): hands off for good.
|
||||
// Staying claimed would fight the final "Worked for 12s" copy.
|
||||
owned.delete(host);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
owned.delete(host);
|
||||
}
|
||||
|
||||
var fresh = findLabelNodes(host);
|
||||
if (fresh.length === 0) return;
|
||||
var text = nextPhrase();
|
||||
for (var k = 0; k < fresh.length; k += 1) fresh[k].nodeValue = text;
|
||||
owned.set(host, { nodes: fresh, text: text });
|
||||
}
|
||||
|
||||
function scan() {
|
||||
if (typeof document === "undefined" || !document.querySelectorAll) return;
|
||||
var hosts = labelHosts();
|
||||
for (var i = 0; i < hosts.length; i += 1) claim(hosts[i]);
|
||||
}
|
||||
|
||||
/** Rotate owned labels so the text moves even when React never rewrites it. */
|
||||
function rotate() {
|
||||
var elements = statusElements();
|
||||
for (var i = 0; i < elements.length; i += 1) {
|
||||
var element = elements[i];
|
||||
var record = owned.get(element);
|
||||
if (typeof document === "undefined" || !document.querySelectorAll) return;
|
||||
var hosts = labelHosts();
|
||||
for (var i = 0; i < hosts.length; i += 1) {
|
||||
var host = hosts[i];
|
||||
var record = owned.get(host);
|
||||
if (!record) {
|
||||
claim(element);
|
||||
claim(host);
|
||||
continue;
|
||||
}
|
||||
if (!isLive(record.node) || !element.contains(record.node)) {
|
||||
owned.delete(element);
|
||||
claim(element);
|
||||
var stale = false;
|
||||
for (var j = 0; j < record.nodes.length; j += 1) {
|
||||
var node = record.nodes[j];
|
||||
if (!isLive(node) || !host.contains(node)) { stale = true; break; }
|
||||
var current = node.nodeValue || "";
|
||||
if (current !== record.text && !RUNNING_LABEL.test(current)) { stale = true; break; }
|
||||
}
|
||||
if (stale) {
|
||||
owned.delete(host);
|
||||
claim(host);
|
||||
continue;
|
||||
}
|
||||
var text = nextPhrase();
|
||||
record.text = text;
|
||||
record.node.nodeValue = text;
|
||||
for (var k = 0; k < record.nodes.length; k += 1) record.nodes[k].nodeValue = text;
|
||||
}
|
||||
}
|
||||
|
||||
/** Nearest running-status host above a mutated text node, if any. */
|
||||
function hostOf(node) {
|
||||
var element = node && node.parentElement;
|
||||
if (!element || typeof element.closest !== "function") return null;
|
||||
return element.closest("[data-chat-running], [data-turn-process]");
|
||||
}
|
||||
|
||||
function apply(ctx) {
|
||||
ctx.effect(function () {
|
||||
if (typeof document === "undefined" || typeof MutationObserver === "undefined") {
|
||||
var observer = null;
|
||||
var interval = null;
|
||||
var pending = null;
|
||||
|
||||
try {
|
||||
if (typeof document === "undefined" || typeof MutationObserver === "undefined") {
|
||||
return function () {};
|
||||
}
|
||||
|
||||
scan();
|
||||
|
||||
function scheduleScan() {
|
||||
if (pending !== null) return;
|
||||
pending = setTimeout(function () {
|
||||
pending = null;
|
||||
scan();
|
||||
}, SCAN_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
observer = new MutationObserver(function (records) {
|
||||
var structural = false;
|
||||
for (var i = 0; i < records.length; i += 1) {
|
||||
var record = records[i];
|
||||
if (record.type !== "characterData") {
|
||||
structural = true;
|
||||
continue;
|
||||
}
|
||||
// Fast path: DSH rewrites the live duration every second (in
|
||||
// 0.2.0 that is two shimmer layers at once). Observer callbacks
|
||||
// run before paint, so restoring here keeps the stock label from
|
||||
// flashing through.
|
||||
var host = hostOf(record.target);
|
||||
if (host) claim(host);
|
||||
else structural = true;
|
||||
}
|
||||
if (structural) scheduleScan();
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true
|
||||
});
|
||||
|
||||
interval = setInterval(rotate, ROTATE_MS);
|
||||
} catch (error) {
|
||||
// A cosmetic plugin must never take down the chat view.
|
||||
if (typeof console !== "undefined" && console.warn) {
|
||||
console.warn("[based-dsh-diving] disabled after an unexpected error:", error);
|
||||
}
|
||||
if (observer) observer.disconnect();
|
||||
if (interval !== null) clearInterval(interval);
|
||||
if (pending !== null) clearTimeout(pending);
|
||||
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 (observer) observer.disconnect();
|
||||
if (interval !== null) clearInterval(interval);
|
||||
if (pending !== null) {
|
||||
clearTimeout(pending);
|
||||
pending = null;
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"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",
|
||||
"description": "Replace DeepSeek Harness's running-turn status label with rotating, chronically online loading text.",
|
||||
"version": "0.1.3",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"exports": {
|
||||
|
||||
+274
-65
@@ -1,7 +1,16 @@
|
||||
// Headless harness for based-dsh-diving/client.js — stubs just enough DOM to
|
||||
// exercise claim / React-revert / rotate behaviour without a browser.
|
||||
// Headless harness for based-dsh-diving/client.js.
|
||||
//
|
||||
// Stubs just enough DOM to exercise all three supported layouts:
|
||||
// - DSH >= 0.2.0: <div data-chat-running> holding a hidden [role="status"]
|
||||
// announcement plus a TextShimmer whose label is rendered
|
||||
// twice (visible layer + aria-hidden decorative overlay)
|
||||
// - DSH >= 0.1.7: <button data-turn-process> > <span>label</span>
|
||||
// plus a visually hidden [role="status"] announcement
|
||||
// - DSH <= 0.1.5: the visible label is the [role="status"] text itself
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
// ---------------------------------------------------------------- loader stub
|
||||
|
||||
const capturedLoads = [];
|
||||
globalThis.window = { __ModuleLoader__: { load: (def) => capturedLoads.push(def) } };
|
||||
globalThis.Node = { TEXT_NODE: 3, ELEMENT_NODE: 1 };
|
||||
@@ -13,76 +22,276 @@ globalThis.MutationObserver = class {
|
||||
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 definition = capturedLoads[0];
|
||||
if (definition.id !== "@based-dsh-diving/dsh-plugin") throw new Error(`bad id: ${definition.id}`);
|
||||
|
||||
const mod = def.factory(() => { throw new Error("require not expected"); });
|
||||
let cleanup = null;
|
||||
mod.apply({ effect: (fn) => { cleanup = fn(); } });
|
||||
// ------------------------------------------------------------------ DOM stub
|
||||
|
||||
const ok = (name, cond, extra = "") => console.log(`${cond ? "PASS" : "FAIL"} ${name}${extra ? " " + extra : ""}`);
|
||||
function isConnected(node) {
|
||||
for (let p = node.parent; p; p = p.parent) if (p.rooted) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
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");
|
||||
class TextNode {
|
||||
constructor(value) { this.nodeType = 3; this.nodeValue = value; this.parent = null; this.nextSibling = null; }
|
||||
get parentElement() { return this.parent; }
|
||||
get isConnected() { return isConnected(this); }
|
||||
}
|
||||
|
||||
const first = label.nodeValue;
|
||||
class El {
|
||||
constructor(tag, attrs = {}) {
|
||||
this.nodeType = 1;
|
||||
this.tagName = tag.toUpperCase();
|
||||
this.attrs = attrs;
|
||||
this.childNodes = [];
|
||||
this.parent = null;
|
||||
this.nextSibling = null;
|
||||
this.rooted = false;
|
||||
}
|
||||
get parentElement() { return this.parent; }
|
||||
get isConnected() { return this.rooted || isConnected(this); }
|
||||
get firstChild() { return this.childNodes[0] ?? null; }
|
||||
get firstElementChild() { return this.childNodes.find((n) => n.nodeType === 1) ?? null; }
|
||||
getAttribute(name) { return this.attrs[name] === undefined ? null : String(this.attrs[name]); }
|
||||
append(...nodes) {
|
||||
for (const node of nodes) { node.parent = this; this.childNodes.push(node); }
|
||||
this.childNodes.forEach((n, i) => { n.nextSibling = this.childNodes[i + 1] ?? null; });
|
||||
return this;
|
||||
}
|
||||
contains(node) { for (let p = node; p; p = p.parent) if (p === this) return true; return false; }
|
||||
closest(selector) {
|
||||
for (let el = this; el; el = el.parent) if (matches(el, selector)) return el;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 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));
|
||||
function matches(el, selector) {
|
||||
if (el.nodeType !== 1) return false;
|
||||
return selector.split(",").some((raw) => {
|
||||
const part = raw.trim();
|
||||
if (part === "[data-turn-process]") return el.attrs["data-turn-process"] !== undefined;
|
||||
if (part === "[data-chat-running]") return el.attrs["data-chat-running"] !== undefined;
|
||||
if (part === '[role="status"]') return el.attrs.role === "status";
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// 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));
|
||||
function allElements(root) {
|
||||
const out = [];
|
||||
const walk = (node) => {
|
||||
for (let c = node.firstChild; c; c = c.nextSibling) {
|
||||
if (c.nodeType === 1) { out.push(c); walk(c); }
|
||||
}
|
||||
};
|
||||
walk(root);
|
||||
return out;
|
||||
}
|
||||
|
||||
// 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));
|
||||
function useDom() {
|
||||
const root = new El("div");
|
||||
root.rooted = true;
|
||||
globalThis.document = {
|
||||
documentElement: root,
|
||||
querySelectorAll: (selector) => allElements(root).filter((el) => matches(el, selector))
|
||||
};
|
||||
return root;
|
||||
}
|
||||
|
||||
// Disposal stops rotation.
|
||||
cleanup();
|
||||
const frozen = zh.nodeValue;
|
||||
await new Promise((r) => setTimeout(r, 2400));
|
||||
ok("disposer stops rotation", zh.nodeValue === frozen);
|
||||
// ---------------------------------------------------------------- assertions
|
||||
|
||||
let failures = 0;
|
||||
const ok = (name, condition, extra = "") => {
|
||||
if (!condition) failures += 1;
|
||||
console.log(`${condition ? "PASS" : "FAIL"} ${name}${extra ? " " + extra : ""}`);
|
||||
};
|
||||
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const start = () => {
|
||||
const mod = definition.factory(() => { throw new Error("require not expected"); });
|
||||
let dispose = null;
|
||||
mod.apply({ effect: (fn) => { dispose = fn(); } });
|
||||
return { dispose };
|
||||
};
|
||||
// "Deep diving" is itself one of the phrases, so compare against the stock text
|
||||
// this layout shipped rather than pattern-matching the joke wording.
|
||||
const isJoke = (value, stock) => /\.\.\.$/.test(value) && value !== stock;
|
||||
const fire = (...nodes) => observerCallback?.(nodes.map((target) => ({ type: "characterData", target })));
|
||||
|
||||
/** Build DSH >= 0.2.0's RunningStatus subtree and return its label text nodes. */
|
||||
function buildRunningStatus(root, labelText) {
|
||||
const running = new El("div", { "data-chat-running": "true", class: "running" });
|
||||
const hidden = new El("span", { role: "status", "aria-live": "polite", "aria-atomic": "true" });
|
||||
const announcement = new TextNode("Deep diving...");
|
||||
hidden.append(announcement);
|
||||
|
||||
const divider = new El("span", { "aria-hidden": "true" });
|
||||
|
||||
const content = new El("span", { class: "runningContent" });
|
||||
const shimmer = new El("span", { "data-shimmer": "true" });
|
||||
const visibleLayer = new El("span", { class: "content" });
|
||||
const visible = new TextNode(labelText);
|
||||
visibleLayer.append(visible);
|
||||
|
||||
const decoration = new El("span", { "aria-hidden": "true" });
|
||||
const sweep = new El("span", { class: "sweep" });
|
||||
const highlightLayer = new El("span", { class: "content highlight" });
|
||||
const highlight = new TextNode(labelText);
|
||||
highlightLayer.append(highlight);
|
||||
sweep.append(highlightLayer);
|
||||
decoration.append(sweep);
|
||||
|
||||
shimmer.append(visibleLayer, decoration);
|
||||
content.append(shimmer);
|
||||
running.append(hidden, divider, content);
|
||||
root.append(running);
|
||||
return { running, visible, highlight, announcement };
|
||||
}
|
||||
|
||||
// -------------------------------------------- current layout (DSH >= 0.2.0)
|
||||
|
||||
{
|
||||
const root = useDom();
|
||||
const { visible, highlight, announcement } = buildRunningStatus(root, "Deep diving for 1s...");
|
||||
|
||||
const session = start();
|
||||
const owned = visible.nodeValue;
|
||||
|
||||
ok("0.2.0: visible shimmer layer claimed", isJoke(owned, "Deep diving for 1s..."), JSON.stringify(owned));
|
||||
ok("0.2.0: decorative overlay stays in sync", highlight.nodeValue === owned, JSON.stringify(highlight.nodeValue));
|
||||
ok("0.2.0: screen-reader announcement untouched", announcement.nodeValue === "Deep diving...", JSON.stringify(announcement.nodeValue));
|
||||
|
||||
// React rewrites the live duration every second, in both layers at once.
|
||||
visible.nodeValue = "Deep diving for 2s...";
|
||||
highlight.nodeValue = "Deep diving for 2s...";
|
||||
fire(visible, highlight);
|
||||
ok("0.2.0: live-duration rewrite re-asserted (visible)", visible.nodeValue === owned, JSON.stringify(visible.nodeValue));
|
||||
ok("0.2.0: live-duration rewrite re-asserted (overlay)", highlight.nodeValue === owned, JSON.stringify(highlight.nodeValue));
|
||||
|
||||
// Turn ends: the plugin must release, not fight the final text.
|
||||
visible.nodeValue = "Worked for 12s";
|
||||
highlight.nodeValue = "Worked for 12s";
|
||||
fire(visible, highlight);
|
||||
ok("0.2.0: released on turn end", visible.nodeValue === "Worked for 12s", JSON.stringify(visible.nodeValue));
|
||||
|
||||
const before = visible.nodeValue;
|
||||
await wait(2400);
|
||||
ok("0.2.0: stays released after a rotation tick", visible.nodeValue === before, JSON.stringify(visible.nodeValue));
|
||||
|
||||
session.dispose();
|
||||
}
|
||||
|
||||
// ------------------------------- precedence: a stale completed process row
|
||||
// On 0.2.0 the collapsible process row still exists for finished turns, so the
|
||||
// running indicator must win and the finished row must be left alone.
|
||||
|
||||
{
|
||||
const root = useDom();
|
||||
const done = new El("button", { "data-turn-process": "t0" });
|
||||
const doneLabel = new TextNode("Worked for 40s");
|
||||
const doneSpan = new El("span", { class: "label" });
|
||||
doneSpan.append(doneLabel);
|
||||
done.append(doneSpan);
|
||||
root.append(done);
|
||||
|
||||
const { visible } = buildRunningStatus(root, "Deep diving for 1s...");
|
||||
const session = start();
|
||||
|
||||
ok("0.2.0: running indicator wins over finished row", isJoke(visible.nodeValue, "Deep diving for 1s..."), JSON.stringify(visible.nodeValue));
|
||||
ok("0.2.0: finished process row untouched", doneLabel.nodeValue === "Worked for 40s", JSON.stringify(doneLabel.nodeValue));
|
||||
|
||||
// A finished row that React later rewrites must not be claimed either.
|
||||
doneLabel.nodeValue = "Took 41s";
|
||||
fire(doneLabel);
|
||||
ok("0.2.0: finished row never claimed", doneLabel.nodeValue === "Took 41s", JSON.stringify(doneLabel.nodeValue));
|
||||
|
||||
session.dispose();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- 0.1.7 row
|
||||
|
||||
{
|
||||
const root = useDom();
|
||||
const button = new El("button", { "data-turn-process": "t1" });
|
||||
const labelSpan = new El("span", { class: "label" });
|
||||
const label = new TextNode("Deep diving for 1s");
|
||||
labelSpan.append(label);
|
||||
button.append(labelSpan);
|
||||
const hidden = new El("span", { role: "status", "aria-live": "polite", "aria-atomic": "true" });
|
||||
const announcement = new TextNode("Deep diving...");
|
||||
hidden.append(announcement);
|
||||
root.append(hidden, button);
|
||||
|
||||
const session = start();
|
||||
const owned = label.nodeValue;
|
||||
|
||||
ok("0.1.7: label claimed", isJoke(owned, "Deep diving for 1s"), JSON.stringify(owned));
|
||||
ok("0.1.7: screen-reader announcement untouched", announcement.nodeValue === "Deep diving...", JSON.stringify(announcement.nodeValue));
|
||||
|
||||
label.nodeValue = "Deep diving for 2s";
|
||||
fire(label);
|
||||
ok("0.1.7: live-duration rewrite re-asserted", label.nodeValue === owned, JSON.stringify(label.nodeValue));
|
||||
|
||||
label.nodeValue = "Worked for 12s";
|
||||
fire(label);
|
||||
ok("0.1.7: released on turn end", label.nodeValue === "Worked for 12s", JSON.stringify(label.nodeValue));
|
||||
|
||||
const before = label.nodeValue;
|
||||
await wait(2400);
|
||||
ok("0.1.7: stays released after a rotation tick", label.nodeValue === before, JSON.stringify(label.nodeValue));
|
||||
|
||||
session.dispose();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------- legacy layout (<= 0.1.5)
|
||||
|
||||
{
|
||||
const root = useDom();
|
||||
const status = new El("div", { role: "status", "aria-live": "polite" });
|
||||
const label = new TextNode("Deep diving...");
|
||||
const clock = new El("span", { "aria-hidden": "true" });
|
||||
const clockText = new TextNode("12s");
|
||||
clock.append(clockText);
|
||||
status.append(label, clock);
|
||||
root.append(status);
|
||||
|
||||
const session = start();
|
||||
const owned = label.nodeValue;
|
||||
|
||||
ok("legacy layout: label claimed", isJoke(owned, "Deep diving..."), JSON.stringify(owned));
|
||||
ok("legacy layout: clock untouched", clockText.nodeValue === "12s", JSON.stringify(clockText.nodeValue));
|
||||
|
||||
await wait(2400);
|
||||
ok("legacy layout: rotates on interval", label.nodeValue !== owned, JSON.stringify(label.nodeValue));
|
||||
|
||||
session.dispose();
|
||||
}
|
||||
|
||||
// ------------------------------------------------- localized + misc assurance
|
||||
|
||||
{
|
||||
const root = useDom();
|
||||
const { visible } = buildRunningStatus(root, "深度求索中,用时 3s...");
|
||||
|
||||
const session = start();
|
||||
ok("localized running label claimed", isJoke(visible.nodeValue, "深度求索中,用时 3s...") && !/深度求索中/.test(visible.nodeValue), JSON.stringify(visible.nodeValue));
|
||||
|
||||
const frozen = visible.nodeValue;
|
||||
session.dispose();
|
||||
await wait(2400);
|
||||
ok("disposer stops rotation", visible.nodeValue === frozen, JSON.stringify(visible.nodeValue));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------- static integrity
|
||||
|
||||
{
|
||||
const source = readFileSync(new URL("../client.js", import.meta.url), "utf8");
|
||||
const match = source.match(/var PHRASES = \[([\s\S]*?)\n \];/);
|
||||
const phrases = eval(`[${match[1]}]`);
|
||||
const dupes = phrases.filter((p, i) => phrases.indexOf(p) !== i);
|
||||
ok("phrase list has no duplicates", dupes.length === 0, dupes.join(", "));
|
||||
ok("phrase list is non-trivial", phrases.length >= 100, `${phrases.length} phrases`);
|
||||
}
|
||||
|
||||
console.log(failures === 0 ? "\nALL PASS" : `\n${failures} FAILURE(S)`);
|
||||
process.exit(failures === 0 ? 0 : 1);
|
||||
Reference in new issue
Block a user