@linxin666/dsh-remote-web-ui 0.3.5 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +77 -468
- package/README.zh.md +83 -113
- package/lib/client.js +955 -203
- package/lib/client.js.map +1 -1
- package/lib/index.js +1136 -1466
- package/lib/types/client/FooterRemoteEntry.d.ts +6 -7
- package/lib/types/client/FooterRemoteEntry.d.ts.map +1 -1
- package/lib/types/client/FooterRemoteEntry.js +1 -1
- package/lib/types/client/RemoteEntry.d.ts +7 -4
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +7 -10
- package/lib/types/client/RemotePanel.d.ts +2 -2
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts +9 -5
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +56 -3
- package/lib/types/client/deep-link.d.ts +10 -13
- package/lib/types/client/deep-link.d.ts.map +1 -1
- package/lib/types/client/deep-link.js +14 -74
- package/lib/types/client/index.d.ts +2 -6
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +73 -39
- package/lib/types/client/locales.d.ts +26 -12
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +36 -22
- package/lib/types/client/mobile-adapt.d.ts +40 -0
- package/lib/types/client/mobile-adapt.d.ts.map +1 -0
- package/lib/types/client/mobile-adapt.js +967 -0
- package/lib/types/client/pair-api.d.ts +24 -6
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +9 -14
- package/lib/types/client/remote-channel.d.ts +3 -0
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +35 -3
- package/lib/types/client/settings-form.d.ts +35 -43
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +60 -53
- package/lib/types/firewall.d.ts +75 -0
- package/lib/types/firewall.d.ts.map +1 -0
- package/lib/types/firewall.js +184 -0
- package/lib/types/gate.d.ts +11 -5
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +11 -5
- package/lib/types/index.d.ts +39 -10
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +206 -58
- package/lib/types/inner-auth.d.ts +32 -0
- package/lib/types/inner-auth.d.ts.map +1 -0
- package/lib/types/inner-auth.js +72 -0
- package/lib/types/lan-bind-plan.d.ts +55 -0
- package/lib/types/lan-bind-plan.d.ts.map +1 -0
- package/lib/types/lan-bind-plan.js +60 -0
- package/lib/types/lan-bind.d.ts +69 -0
- package/lib/types/lan-bind.d.ts.map +1 -0
- package/lib/types/lan-bind.js +128 -0
- package/lib/types/loopback-proxy.d.ts +9 -2
- package/lib/types/loopback-proxy.d.ts.map +1 -1
- package/lib/types/loopback-proxy.js +24 -2
- package/lib/types/pairing.d.ts +15 -3
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +20 -6
- package/lib/types/remote-api.d.ts +30 -13
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +55 -42
- package/lib/types/remote-channel-boot.d.ts +21 -2
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +65 -3
- package/lib/types/remote-channel-rules.d.ts +8 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +9 -2
- package/lib/types/remote-methods.d.ts +45 -11
- package/lib/types/remote-methods.d.ts.map +1 -1
- package/lib/types/remote-methods.js +56 -27
- package/lib/types/remote-presence-pet.d.ts +51 -0
- package/lib/types/remote-presence-pet.d.ts.map +1 -0
- package/lib/types/remote-presence-pet.js +101 -0
- package/lib/types/routes.d.ts +76 -4
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +336 -19
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +7 -1
- package/package.json +20 -18
- package/src/client/FooterRemoteEntry.tsx +9 -10
- package/src/client/RemoteEntry.tsx +13 -14
- package/src/client/RemotePanel.tsx +5 -16
- package/src/client/RemoteSettingsCard.tsx +72 -13
- package/src/client/deep-link.ts +14 -75
- package/src/client/index.ts +77 -39
- package/src/client/locales.ts +36 -22
- package/src/client/mobile-adapt.ts +913 -0
- package/src/client/pair-api.ts +26 -15
- package/src/client/remote-channel.ts +31 -3
- package/src/client/settings-form.ts +78 -92
- package/src/dsh-home.ts +1 -1
- package/src/firewall.ts +230 -0
- package/src/gate.ts +11 -5
- package/src/index.ts +235 -65
- package/src/inner-auth.ts +85 -0
- package/src/lan-bind-plan.ts +81 -0
- package/src/lan-bind.ts +145 -0
- package/src/loopback-proxy.ts +34 -0
- package/src/pairing.persist.test.ts +1 -1
- package/src/pairing.ts +20 -6
- package/src/remote-api.ts +67 -45
- package/src/remote-channel-boot.ts +70 -3
- package/src/remote-channel-rules.ts +17 -2
- package/src/remote-methods.ts +58 -27
- package/src/remote-presence-pet.ts +129 -0
- package/src/routes.ts +358 -19
- package/src/tunnel.ts +7 -1
- package/assets/apple-touch-icon.png +0 -0
- package/assets/mobile-icon-192.png +0 -0
- package/assets/mobile-icon-512.png +0 -0
- package/assets/mobile-service-worker.js +0 -63
- package/lib/mobile.js +0 -17621
- package/lib/mobile.js.map +0 -1
- package/lib/types/mobile/PairRequiredView.d.ts +0 -7
- package/lib/types/mobile/PairRequiredView.d.ts.map +0 -1
- package/lib/types/mobile/PairRequiredView.js +0 -28
- package/lib/types/mobile/PairRequiredView.test.d.ts +0 -2
- package/lib/types/mobile/PairRequiredView.test.d.ts.map +0 -1
- package/lib/types/mobile/PairRequiredView.test.js +0 -24
- package/lib/types/mobile/api.d.ts +0 -145
- package/lib/types/mobile/api.d.ts.map +0 -1
- package/lib/types/mobile/api.js +0 -103
- package/lib/types/mobile/index.d.ts +0 -10
- package/lib/types/mobile/index.d.ts.map +0 -1
- package/lib/types/mobile/index.js +0 -38
- package/lib/types/mobile/markdown.d.ts +0 -34
- package/lib/types/mobile/markdown.d.ts.map +0 -1
- package/lib/types/mobile/markdown.js +0 -285
- package/lib/types/mobile/markdown.test.d.ts +0 -2
- package/lib/types/mobile/markdown.test.d.ts.map +0 -1
- package/lib/types/mobile/markdown.test.js +0 -79
- package/lib/types/mobile/messages.d.ts +0 -141
- package/lib/types/mobile/messages.d.ts.map +0 -1
- package/lib/types/mobile/messages.js +0 -553
- package/lib/types/mobile/messages.test.d.ts +0 -2
- package/lib/types/mobile/messages.test.d.ts.map +0 -1
- package/lib/types/mobile/messages.test.js +0 -356
- package/lib/types/mobile/mobile-styles.d.ts +0 -6
- package/lib/types/mobile/mobile-styles.d.ts.map +0 -1
- package/lib/types/mobile/mobile-styles.js +0 -1438
- package/lib/types/mobile/mobile-theme.d.ts +0 -22
- package/lib/types/mobile/mobile-theme.d.ts.map +0 -1
- package/lib/types/mobile/mobile-theme.js +0 -70
- package/lib/types/mobile/mobile-theme.test.d.ts +0 -2
- package/lib/types/mobile/mobile-theme.test.d.ts.map +0 -1
- package/lib/types/mobile/mobile-theme.test.js +0 -71
- package/lib/types/mobile/mux.d.ts +0 -128
- package/lib/types/mobile/mux.d.ts.map +0 -1
- package/lib/types/mobile/mux.js +0 -305
- package/lib/types/mobile/mux.test.d.ts +0 -2
- package/lib/types/mobile/mux.test.d.ts.map +0 -1
- package/lib/types/mobile/mux.test.js +0 -267
- package/lib/types/mobile/pairing.d.ts +0 -31
- package/lib/types/mobile/pairing.d.ts.map +0 -1
- package/lib/types/mobile/pairing.js +0 -62
- package/lib/types/mobile/rpc.d.ts +0 -38
- package/lib/types/mobile/rpc.d.ts.map +0 -1
- package/lib/types/mobile/rpc.js +0 -83
- package/lib/types/mobile/theme-toggle.d.ts +0 -12
- package/lib/types/mobile/theme-toggle.d.ts.map +0 -1
- package/lib/types/mobile/theme-toggle.js +0 -26
- package/lib/types/mobile/views/App.d.ts +0 -57
- package/lib/types/mobile/views/App.d.ts.map +0 -1
- package/lib/types/mobile/views/App.js +0 -150
- package/lib/types/mobile/views/App.test.d.ts +0 -2
- package/lib/types/mobile/views/App.test.d.ts.map +0 -1
- package/lib/types/mobile/views/App.test.js +0 -52
- package/lib/types/mobile/views/ChatView.d.ts +0 -56
- package/lib/types/mobile/views/ChatView.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.js +0 -773
- package/lib/types/mobile/views/ChatView.stream.test.d.ts +0 -2
- package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.stream.test.js +0 -231
- package/lib/types/mobile/views/ChatView.test.d.ts +0 -2
- package/lib/types/mobile/views/ChatView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.test.js +0 -718
- package/lib/types/mobile/views/SessionListView.d.ts +0 -27
- package/lib/types/mobile/views/SessionListView.d.ts.map +0 -1
- package/lib/types/mobile/views/SessionListView.js +0 -139
- package/lib/types/mobile/views/SessionListView.test.d.ts +0 -2
- package/lib/types/mobile/views/SessionListView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/SessionListView.test.js +0 -167
- package/lib/types/mobile/views/WorkspaceView.d.ts +0 -21
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +0 -1
- package/lib/types/mobile/views/WorkspaceView.js +0 -93
- package/lib/types/mobile/views/WorkspaceView.test.d.ts +0 -2
- package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/WorkspaceView.test.js +0 -183
- package/lib/types/mobile-api.d.ts +0 -65
- package/lib/types/mobile-api.d.ts.map +0 -1
- package/lib/types/mobile-api.js +0 -404
- package/lib/types/mobile-pending.d.ts +0 -38
- package/lib/types/mobile-pending.d.ts.map +0 -1
- package/lib/types/mobile-pending.js +0 -59
- package/lib/types/mobile-pwa.d.ts +0 -6
- package/lib/types/mobile-pwa.d.ts.map +0 -1
- package/lib/types/mobile-pwa.js +0 -17
- package/lib/types/mobile-routes.d.ts +0 -13
- package/lib/types/mobile-routes.d.ts.map +0 -1
- package/lib/types/mobile-routes.js +0 -217
- package/lib/types/paired-model-catalog.d.ts +0 -17
- package/lib/types/paired-model-catalog.d.ts.map +0 -1
- package/lib/types/paired-model-catalog.js +0 -311
- package/src/mobile/PairRequiredView.test.tsx +0 -28
- package/src/mobile/PairRequiredView.tsx +0 -58
- package/src/mobile/api.ts +0 -229
- package/src/mobile/index.tsx +0 -41
- package/src/mobile/markdown.test.ts +0 -91
- package/src/mobile/markdown.ts +0 -291
- package/src/mobile/messages.test.ts +0 -402
- package/src/mobile/messages.ts +0 -677
- package/src/mobile/mobile-styles.ts +0 -1439
- package/src/mobile/mobile-theme.test.ts +0 -84
- package/src/mobile/mobile-theme.ts +0 -76
- package/src/mobile/mux.test.ts +0 -314
- package/src/mobile/mux.ts +0 -336
- package/src/mobile/pairing.ts +0 -77
- package/src/mobile/rpc.ts +0 -90
- package/src/mobile/theme-toggle.tsx +0 -47
- package/src/mobile/views/App.test.tsx +0 -62
- package/src/mobile/views/App.tsx +0 -213
- package/src/mobile/views/ChatView.stream.test.tsx +0 -263
- package/src/mobile/views/ChatView.test.tsx +0 -816
- package/src/mobile/views/ChatView.tsx +0 -1295
- package/src/mobile/views/SessionListView.test.tsx +0 -207
- package/src/mobile/views/SessionListView.tsx +0 -244
- package/src/mobile/views/WorkspaceView.test.tsx +0 -227
- package/src/mobile/views/WorkspaceView.tsx +0 -245
- package/src/mobile-api.ts +0 -418
- package/src/mobile-pending.ts +0 -88
- package/src/mobile-pwa.ts +0 -19
- package/src/mobile-routes.ts +0 -231
- package/src/paired-model-catalog.ts +0 -272
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ChatView.stream.test.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/ChatView.stream.test.tsx"],"names":[],"mappings":""}
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
// @vitest-environment jsdom
|
|
3
|
-
/**
|
|
4
|
-
* ChatView streaming markdown throttle: a pending assistant message must not
|
|
5
|
-
* re-parse its whole accumulated text on every chunk. Streaming text
|
|
6
|
-
* re-parses at most once per STREAM_RENDER_INTERVAL_MS, shows the last
|
|
7
|
-
* parsed result in between, and renders the exact final HTML (byte-identical
|
|
8
|
-
* to the plain renderMarkdown output) the moment the turn closes.
|
|
9
|
-
*/
|
|
10
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
11
|
-
import { act, cleanup, render, screen } from '@testing-library/react';
|
|
12
|
-
import { ChatView, STREAM_RENDER_INTERVAL_MS, LONG_TEXT_LIMIT } from "./ChatView.js";
|
|
13
|
-
import { renderMarkdown } from "../markdown.js";
|
|
14
|
-
// The api module is fully mocked; App.tsx's history wrapper is overridden to
|
|
15
|
-
// feed a fixed empty history page, its pure helpers stay real.
|
|
16
|
-
vi.mock('../api.ts', () => ({
|
|
17
|
-
fetchMobilePreferences: vi.fn(),
|
|
18
|
-
models: vi.fn(),
|
|
19
|
-
selectModel: vi.fn(),
|
|
20
|
-
sendCommand: vi.fn(),
|
|
21
|
-
cancelSession: vi.fn(),
|
|
22
|
-
fetchPending: vi.fn(),
|
|
23
|
-
}));
|
|
24
|
-
vi.mock('./App.tsx', async (importOriginal) => {
|
|
25
|
-
const actual = await importOriginal();
|
|
26
|
-
return {
|
|
27
|
-
...actual,
|
|
28
|
-
loadHistory: vi.fn(),
|
|
29
|
-
prompt: vi.fn(async () => { }),
|
|
30
|
-
};
|
|
31
|
-
});
|
|
32
|
-
// Wrap the real renderer with a call counter: the throttle must cut the
|
|
33
|
-
// number of full-text parses during a chunk stream (each parse is O(n)).
|
|
34
|
-
vi.mock('../markdown.ts', async (importOriginal) => {
|
|
35
|
-
const actual = await importOriginal();
|
|
36
|
-
return { ...actual, renderMarkdown: vi.fn(actual.renderMarkdown) };
|
|
37
|
-
});
|
|
38
|
-
import { fetchMobilePreferences, models } from "../api.js";
|
|
39
|
-
import { loadHistory } from "./App.js";
|
|
40
|
-
const loadHistoryMock = vi.mocked(loadHistory);
|
|
41
|
-
const fetchMobilePreferencesMock = vi.mocked(fetchMobilePreferences);
|
|
42
|
-
const modelsMock = vi.mocked(models);
|
|
43
|
-
const renderMarkdownMock = vi.mocked(renderMarkdown);
|
|
44
|
-
const session = {
|
|
45
|
-
sessionId: 's-1',
|
|
46
|
-
title: '测试会话',
|
|
47
|
-
updatedAt: 1_700_000_000_000,
|
|
48
|
-
running: false,
|
|
49
|
-
blank: false,
|
|
50
|
-
};
|
|
51
|
-
/** Minimal mux stand-in: captures the ChatView's frame listener for hand-off. */
|
|
52
|
-
class FakeMux {
|
|
53
|
-
listeners = new Set();
|
|
54
|
-
onFrame(listener) {
|
|
55
|
-
this.listeners.add(listener);
|
|
56
|
-
return () => { this.listeners.delete(listener); };
|
|
57
|
-
}
|
|
58
|
-
emit(frame) {
|
|
59
|
-
for (const listener of this.listeners)
|
|
60
|
-
listener(frame);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
/** One history entry wrapping a WireEvent (host history-page shape). */
|
|
64
|
-
function makeEntry(type, data, seq) {
|
|
65
|
-
return { event: { type, seq, time: seq * 1_000, data } };
|
|
66
|
-
}
|
|
67
|
-
/** Build an empty history page (the live stream supplies all content). */
|
|
68
|
-
function historyPage() {
|
|
69
|
-
return { events: [], hasMore: false };
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Emit one live text-delta chunk bound to the stable message id 'a-1' (the
|
|
73
|
-
* mobile wire shape). A stable id keeps the message row mounted across the
|
|
74
|
-
* fold, so the tests observe the throttle effect path only.
|
|
75
|
-
*/
|
|
76
|
-
function chunk(mux, text, seq) {
|
|
77
|
-
mux.emit({
|
|
78
|
-
type: 'session/event',
|
|
79
|
-
sessionId: 's-1',
|
|
80
|
-
event: makeEntry('assistant/chunk', { messageId: 'a-1', turn: 0, step: 0, text }, seq).event,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
/** Emit the final assistant message (authoritative text, closes the turn). */
|
|
84
|
-
function finalMessage(mux, text, seq) {
|
|
85
|
-
mux.emit({
|
|
86
|
-
type: 'session/event',
|
|
87
|
-
sessionId: 's-1',
|
|
88
|
-
event: makeEntry('assistant/message', {
|
|
89
|
-
turn: 0,
|
|
90
|
-
step: 0,
|
|
91
|
-
message: { id: 'a-1', role: 'assistant', content: [{ type: 'text', text }] },
|
|
92
|
-
}, seq).event,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
/** Round-trip HTML through the DOM so a byte comparison is symmetric. */
|
|
96
|
-
function normalize(html) {
|
|
97
|
-
const element = document.createElement('div');
|
|
98
|
-
element.innerHTML = html;
|
|
99
|
-
return element.innerHTML;
|
|
100
|
-
}
|
|
101
|
-
/** The assistant message's markdown body (dangerouslySetInnerHTML target). */
|
|
102
|
-
function body() {
|
|
103
|
-
const element = document.querySelector('.chat-msg.chat-msg-assistant .chat-md-body');
|
|
104
|
-
if (element === null)
|
|
105
|
-
throw new Error('assistant markdown body not found');
|
|
106
|
-
return element;
|
|
107
|
-
}
|
|
108
|
-
beforeEach(() => {
|
|
109
|
-
fetchMobilePreferencesMock.mockResolvedValue({ mobileEnterToSend: true });
|
|
110
|
-
modelsMock.mockResolvedValue({
|
|
111
|
-
current: { provider: 'fx', model: 'fx-1' },
|
|
112
|
-
routable: true,
|
|
113
|
-
groups: [],
|
|
114
|
-
failures: [],
|
|
115
|
-
});
|
|
116
|
-
loadHistoryMock.mockResolvedValue(historyPage());
|
|
117
|
-
vi.useFakeTimers();
|
|
118
|
-
});
|
|
119
|
-
afterEach(() => {
|
|
120
|
-
cleanup();
|
|
121
|
-
vi.useRealTimers();
|
|
122
|
-
vi.clearAllMocks();
|
|
123
|
-
vi.restoreAllMocks();
|
|
124
|
-
});
|
|
125
|
-
describe('ChatView streaming markdown throttle', () => {
|
|
126
|
-
it('re-parses pending text at most once per interval and catches up byte-exactly', async () => {
|
|
127
|
-
const mux = new FakeMux();
|
|
128
|
-
await act(async () => {
|
|
129
|
-
render(_jsx(ChatView, { session: session, mux: mux, onBack: () => { } }));
|
|
130
|
-
});
|
|
131
|
-
const parts = [
|
|
132
|
-
'第一段 **加粗** 文本与 `行内代码`。\n\n',
|
|
133
|
-
'第二段 *斜体* 与 <标签> 内容。\n\n',
|
|
134
|
-
'第三段 ~~删除线~~ 和 [链接](https://example.com/a)。',
|
|
135
|
-
];
|
|
136
|
-
const full = parts.join('');
|
|
137
|
-
// The first chunk mounts the pending message; the mount render parses it.
|
|
138
|
-
await act(async () => { chunk(mux, parts[0] ?? '', 6); });
|
|
139
|
-
const afterMount = renderMarkdownMock.mock.calls.length;
|
|
140
|
-
expect(afterMount).toBeGreaterThanOrEqual(1);
|
|
141
|
-
// A burst of further chunks does not re-parse per chunk: at most one
|
|
142
|
-
// leading parse may run (if the wall-clock gap to the burst exceeded the
|
|
143
|
-
// window), but never one parse per chunk.
|
|
144
|
-
await act(async () => {
|
|
145
|
-
chunk(mux, parts[1] ?? '', 7);
|
|
146
|
-
chunk(mux, parts[2] ?? '', 8);
|
|
147
|
-
});
|
|
148
|
-
expect(renderMarkdownMock.mock.calls.length - afterMount).toBeLessThan(2);
|
|
149
|
-
// During the window the last parsed result stays visible: the newest
|
|
150
|
-
// chunk's text has not reached the DOM yet.
|
|
151
|
-
expect(body().innerHTML).not.toContain('第三段');
|
|
152
|
-
// The trailing render inside the window catches up to the newest text,
|
|
153
|
-
// byte-identical to a full renderMarkdown of the accumulated text.
|
|
154
|
-
await act(async () => {
|
|
155
|
-
vi.advanceTimersByTime(STREAM_RENDER_INTERVAL_MS + 5);
|
|
156
|
-
});
|
|
157
|
-
expect(normalize(body().innerHTML)).toBe(normalize(renderMarkdown(full)));
|
|
158
|
-
expect(renderMarkdownMock.mock.calls.length - afterMount).toBeLessThan(3);
|
|
159
|
-
});
|
|
160
|
-
it('renders the exact final HTML immediately when the turn closes', async () => {
|
|
161
|
-
const mux = new FakeMux();
|
|
162
|
-
await act(async () => {
|
|
163
|
-
render(_jsx(ChatView, { session: session, mux: mux, onBack: () => { } }));
|
|
164
|
-
});
|
|
165
|
-
// The final event carries the authoritative text and closes the turn
|
|
166
|
-
// (pending -> false) in the same synchronous burst as the chunks.
|
|
167
|
-
const finalText = '最终 **整篇** `渲染` 与 [链接](https://example.com/x) 文本。';
|
|
168
|
-
await act(async () => {
|
|
169
|
-
chunk(mux, '流式前缀', 6);
|
|
170
|
-
chunk(mux, '中间过程', 7);
|
|
171
|
-
finalMessage(mux, finalText, 8);
|
|
172
|
-
});
|
|
173
|
-
// No timer advance needed: the closed turn already renders the exact
|
|
174
|
-
// final HTML (terminal messages keep the immediate-render behavior).
|
|
175
|
-
expect(normalize(body().innerHTML)).toBe(normalize(renderMarkdown(finalText)));
|
|
176
|
-
});
|
|
177
|
-
it('keeps the streaming render fresh after window edges and skips the identical final parse', async () => {
|
|
178
|
-
const mux = new FakeMux();
|
|
179
|
-
await act(async () => {
|
|
180
|
-
render(_jsx(ChatView, { session: session, mux: mux, onBack: () => { } }));
|
|
181
|
-
});
|
|
182
|
-
const first = '第一期内容。';
|
|
183
|
-
const second = '第二期内容,包含 `code`。';
|
|
184
|
-
const full = first + second;
|
|
185
|
-
// The chunk and the window edge in one tick: the trailing render fires
|
|
186
|
-
// inside the window and shows the current text.
|
|
187
|
-
await act(async () => {
|
|
188
|
-
chunk(mux, first, 6);
|
|
189
|
-
vi.advanceTimersByTime(STREAM_RENDER_INTERVAL_MS + 5);
|
|
190
|
-
});
|
|
191
|
-
expect(normalize(body().innerHTML)).toBe(normalize(renderMarkdown(first)));
|
|
192
|
-
// A later chunk schedules a new trailing render; before it fires the last
|
|
193
|
-
// parsed result is still on screen.
|
|
194
|
-
await act(async () => { chunk(mux, second, 7); });
|
|
195
|
-
expect(body().innerHTML).not.toContain('第二期内容');
|
|
196
|
-
await act(async () => {
|
|
197
|
-
vi.advanceTimersByTime(STREAM_RENDER_INTERVAL_MS + 5);
|
|
198
|
-
});
|
|
199
|
-
expect(normalize(body().innerHTML)).toBe(normalize(renderMarkdown(full)));
|
|
200
|
-
// Closing the turn with the exact text already rendered must not re-parse
|
|
201
|
-
// (the row already holds the byte-identical final HTML).
|
|
202
|
-
const beforeFinal = renderMarkdownMock.mock.calls.length;
|
|
203
|
-
await act(async () => { finalMessage(mux, full, 9); });
|
|
204
|
-
expect(renderMarkdownMock.mock.calls.length - beforeFinal).toBe(0);
|
|
205
|
-
expect(normalize(body().innerHTML)).toBe(normalize(renderMarkdown(full)));
|
|
206
|
-
});
|
|
207
|
-
it('does not collapse long text while streaming (pending) and collapses once turn finishes', async () => {
|
|
208
|
-
const mux = new FakeMux();
|
|
209
|
-
let container;
|
|
210
|
-
await act(async () => {
|
|
211
|
-
const rendered = render(_jsx(ChatView, { session: session, mux: mux, onBack: () => { } }));
|
|
212
|
-
container = rendered.container;
|
|
213
|
-
});
|
|
214
|
-
const hugeChunk = 'C'.repeat(LONG_TEXT_LIMIT + 200);
|
|
215
|
-
// Stream a chunk exceeding LONG_TEXT_LIMIT
|
|
216
|
-
await act(async () => {
|
|
217
|
-
chunk(mux, hugeChunk, 6);
|
|
218
|
-
vi.advanceTimersByTime(STREAM_RENDER_INTERVAL_MS + 5);
|
|
219
|
-
});
|
|
220
|
-
// During streaming (pending), message is not collapsed
|
|
221
|
-
expect(container.querySelector('.chat-md-collapsed')).toBeNull();
|
|
222
|
-
expect(screen.queryByRole('button', { name: /展开全文/ })).toBeNull();
|
|
223
|
-
// Turn closes
|
|
224
|
-
await act(async () => {
|
|
225
|
-
finalMessage(mux, hugeChunk, 7);
|
|
226
|
-
});
|
|
227
|
-
// Once turn ends (not pending), message is collapsed and has expand button
|
|
228
|
-
expect(container.querySelector('.chat-md-collapsed')).not.toBeNull();
|
|
229
|
-
expect(screen.getByRole('button', { name: new RegExp(`展开全文(${LONG_TEXT_LIMIT + 200} 字)`) })).toBeTruthy();
|
|
230
|
-
});
|
|
231
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ChatView.test.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/ChatView.test.tsx"],"names":[],"mappings":""}
|