@yfwu2020/dsh-selection-explain 0.10.3 → 0.10.5
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.md +13 -11
- package/cordis.patch.yml +1 -1
- package/lib/client.js +10694 -12424
- package/lib/client.js.map +7 -0
- package/lib/config-values.js +55 -0
- package/lib/config-values.js.map +1 -0
- package/lib/host/analyze.js +795 -0
- package/lib/host/analyze.js.map +1 -0
- package/lib/host/config.js +81 -0
- package/lib/host/config.js.map +1 -0
- package/lib/host/constants.js +25 -0
- package/lib/host/constants.js.map +1 -0
- package/lib/host/context.js +237 -0
- package/lib/host/context.js.map +1 -0
- package/lib/host/design-skill.js +42 -0
- package/lib/host/design-skill.js.map +1 -0
- package/lib/host/effort.js +239 -0
- package/lib/host/effort.js.map +1 -0
- package/lib/host/filters.js +132 -0
- package/lib/host/filters.js.map +1 -0
- package/lib/host/history-store.js +404 -0
- package/lib/host/history-store.js.map +1 -0
- package/lib/host/http.js +46 -0
- package/lib/host/http.js.map +1 -0
- package/lib/host/messages.js +14 -0
- package/lib/host/messages.js.map +1 -0
- package/lib/host/mic.js +246 -0
- package/lib/host/mic.js.map +1 -0
- package/lib/host/models.js +246 -0
- package/lib/host/models.js.map +1 -0
- package/lib/host/plugin.js +41 -0
- package/lib/host/plugin.js.map +1 -0
- package/lib/host/promote.js +219 -0
- package/lib/host/promote.js.map +1 -0
- package/lib/host/routes.js +237 -0
- package/lib/host/routes.js.map +1 -0
- package/lib/host/sessions.js +181 -0
- package/lib/host/sessions.js.map +1 -0
- package/lib/host/settings-store.js +140 -0
- package/lib/host/settings-store.js.map +1 -0
- package/lib/host/speech.js +320 -0
- package/lib/host/speech.js.map +1 -0
- package/lib/host/tools.js +179 -0
- package/lib/host/tools.js.map +1 -0
- package/lib/host/types.js +2 -0
- package/lib/host/types.js.map +1 -0
- package/lib/index.js +10 -3691
- package/lib/index.js.map +1 -1
- package/lib/prompts.js +1 -1
- package/lib/settings.js +17 -27
- package/lib/settings.js.map +1 -1
- package/lib/types/config-values.d.ts +175 -0
- package/lib/types/host/analyze.d.ts +10 -0
- package/lib/types/host/config.d.ts +147 -0
- package/lib/types/host/constants.d.ts +24 -0
- package/lib/types/host/context.d.ts +41 -0
- package/lib/types/host/design-skill.d.ts +1 -0
- package/lib/types/host/effort.d.ts +61 -0
- package/lib/types/host/filters.d.ts +20 -0
- package/lib/types/host/history-store.d.ts +13 -0
- package/lib/types/host/http.d.ts +8 -0
- package/lib/types/host/messages.d.ts +13 -0
- package/lib/types/host/mic.d.ts +17 -0
- package/lib/types/host/models.d.ts +57 -0
- package/lib/types/host/plugin.d.ts +3 -0
- package/lib/types/host/promote.d.ts +9 -0
- package/lib/types/host/routes.d.ts +14 -0
- package/lib/types/host/sessions.d.ts +14 -0
- package/lib/types/host/settings-store.d.ts +8 -0
- package/lib/types/host/speech.d.ts +23 -0
- package/lib/types/host/tools.d.ts +39 -0
- package/lib/types/host/types.d.ts +89 -0
- package/lib/types/index.d.ts +12 -441
- package/lib/types/prompts.d.ts +1 -1
- package/lib/types/settings.d.ts +1 -1
- package/package.json +31 -21
- package/assets/code.png +0 -0
- package/assets/detail.png +0 -0
- package/assets/quotes.png +0 -0
- package/assets/stage1.png +0 -0
- package/assets/stage2.png +0 -0
- package/assets/translation.png +0 -0
- package/assets/voice-card-light.png +0 -0
- package/assets/voice-inserted.png +0 -0
- package/assets/voice-settings.png +0 -0
- package/assets/voice.png +0 -0
- package/assets/web.png +0 -0
- package/scripts/build-demo-pages.mjs +0 -350
- package/scripts/build-vcard-lab.mjs +0 -834
- package/scripts/build.sh +0 -147
- package/scripts/crop-png.mjs +0 -142
- package/scripts/dump-prompt.mjs +0 -143
- package/scripts/fixtures/fake-mic-probe.sh +0 -9
- package/scripts/gen-star-path.mjs +0 -71
- package/scripts/measure-cost.mjs +0 -251
- package/scripts/smoke-bridge.mjs +0 -365
- package/scripts/smoke-panel.mjs +0 -1903
- package/scripts/smoke-voice.mjs +0 -774
- package/scripts/test-bridge.mjs +0 -412
- package/scripts/test-client.mjs +0 -6175
- package/scripts/test-effort-map.mjs +0 -129
- package/scripts/test-effort-retry.mjs +0 -109
- package/scripts/test-escape-focus.mjs +0 -101
- package/scripts/test-filters.mjs +0 -146
- package/scripts/test-format.mjs +0 -93
- package/scripts/test-guard.mjs +0 -48
- package/scripts/test-mic-idle.mjs +0 -158
- package/scripts/test-model-follow.mjs +0 -193
- package/scripts/test-prompt.mjs +0 -193
- package/scripts/test-route-cache.mjs +0 -138
- package/scripts/test-settings-behavior.mjs +0 -188
- package/scripts/test-settings-host.mjs +0 -48
- package/scripts/test-settings.mjs +0 -329
- package/scripts/test-speech.mjs +0 -220
- package/scripts/test-transcript.mjs +0 -151
- package/src/client/index.js +0 -12580
- package/src/index.ts +0 -4179
- package/src/prompts.ts +0 -144
- package/src/settings.ts +0 -313
package/scripts/test-bridge.mjs
DELETED
|
@@ -1,412 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 侧边栏网页划词桥的纯逻辑测试(无浏览器、无宿主)。
|
|
3
|
-
*
|
|
4
|
-
* 覆盖两块最容易写坏、又最难人工发现的东西:
|
|
5
|
-
* ① 注入本身:桥脚本插在哪、CSP 只放宽哪一条、沙箱补了哪个 token;
|
|
6
|
-
* ② 帧内脚本行为:给它一个"帧里的 DOM + 一段选区",它报给父页面的消息
|
|
7
|
-
* 对不对(文字 / 带【】的上下文窗口 / keyContext / 帧内坐标)。
|
|
8
|
-
*
|
|
9
|
-
* 做法:从 lib/client.js 里把 bridgeBody / bridgeIntoHtml / insertIntoHtml
|
|
10
|
-
* 三个**纯函数**抠出来单独跑 —— 不加载整个 client bundle,也就不需要 DOM 桩。
|
|
11
|
-
*
|
|
12
|
-
* 用法:node scripts/test-bridge.mjs [lib/client.js 路径]
|
|
13
|
-
* 退出码:全部 PASS = 0。
|
|
14
|
-
*/
|
|
15
|
-
import { readFileSync } from 'node:fs'
|
|
16
|
-
import { dirname, resolve } from 'node:path'
|
|
17
|
-
import { fileURLToPath } from 'node:url'
|
|
18
|
-
|
|
19
|
-
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
20
|
-
const BUNDLE = process.argv[2] || resolve(HERE, '..', 'lib', 'client.js')
|
|
21
|
-
const source = readFileSync(BUNDLE, 'utf8')
|
|
22
|
-
|
|
23
|
-
let failed = 0
|
|
24
|
-
const assert = (label, ok, extra) => {
|
|
25
|
-
console.log(`${ok ? 'PASS' : 'FAIL'} ${label}${extra ? ' — ' + extra : ''}`)
|
|
26
|
-
if (!ok) failed += 1
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** 从 bundle 里抠一个具名函数(按花括号配平找结尾,字符串/注释里的花括号不参与计数)。 */
|
|
30
|
-
function sliceFunction(text, name) {
|
|
31
|
-
const start = text.indexOf('function ' + name + '(')
|
|
32
|
-
if (start < 0) throw new Error('bundle 里找不到函数:' + name)
|
|
33
|
-
let depth = 0
|
|
34
|
-
let index = text.indexOf('{', start)
|
|
35
|
-
for (let i = index; i < text.length; i += 1) {
|
|
36
|
-
const ch = text[i]
|
|
37
|
-
if (ch === '{') depth += 1
|
|
38
|
-
else if (ch === '}') {
|
|
39
|
-
depth -= 1
|
|
40
|
-
if (depth === 0) return text.slice(start, i + 1)
|
|
41
|
-
} else if (ch === "'" || ch === '"' || ch === '`') {
|
|
42
|
-
// 跳过字符串字面量(里面的花括号不能算进配平)
|
|
43
|
-
for (i += 1; i < text.length; i += 1) {
|
|
44
|
-
if (text[i] === '\\') i += 1
|
|
45
|
-
else if (text[i] === ch) break
|
|
46
|
-
}
|
|
47
|
-
} else if (ch === '/' && text[i + 1] === '/') {
|
|
48
|
-
while (i < text.length && text[i] !== '\n') i += 1
|
|
49
|
-
} else if (ch === '/' && text[i + 1] === '*') {
|
|
50
|
-
i = text.indexOf('*/', i) + 1
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
throw new Error('函数没配平:' + name)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// ───────────────────────── 抠出纯函数 ─────────────────────────
|
|
57
|
-
const MARK = 'data-dsh-sel-bridge'
|
|
58
|
-
const api = new Function(
|
|
59
|
-
'BRIDGE_MARK',
|
|
60
|
-
[
|
|
61
|
-
sliceFunction(source, 'bridgeBody'),
|
|
62
|
-
sliceFunction(source, 'bridgeIntoHtml'),
|
|
63
|
-
sliceFunction(source, 'insertIntoHtml'),
|
|
64
|
-
'return { bridgeBody: bridgeBody, bridgeIntoHtml: bridgeIntoHtml, insertIntoHtml: insertIntoHtml }',
|
|
65
|
-
].join('\n'),
|
|
66
|
-
)(MARK)
|
|
67
|
-
|
|
68
|
-
assert('从 bundle 抠出 bridgeBody / bridgeIntoHtml / insertIntoHtml', typeof api.bridgeIntoHtml === 'function')
|
|
69
|
-
|
|
70
|
-
// ───────────────────────── ① 注入 ─────────────────────────
|
|
71
|
-
|
|
72
|
-
/** 宿主基础预览(ui-sidebar-documentpreview 的 BasicHtmlFrame)长这样:
|
|
73
|
-
* DOMPurify 清洗 + 一条 script-src 'none' 的 CSP,装进 srcdoc + sandbox=""。 */
|
|
74
|
-
const BASIC =
|
|
75
|
-
'<!doctype html><html><head><meta http-equiv="Content-Security-Policy" content="default-src \'none\'; ' +
|
|
76
|
-
"script-src 'none'; style-src 'unsafe-inline'; img-src data:; connect-src 'none'; frame-src 'none'; " +
|
|
77
|
-
'form-action \'none\'; base-uri \'none\'"><style>body{color:red}</style></head>' +
|
|
78
|
-
'<body><p>the migration ran long, so we ship Wednesday</p></body></html>'
|
|
79
|
-
|
|
80
|
-
const basicOut = api.bridgeIntoHtml(BASIC)
|
|
81
|
-
assert('基础预览:注入成功', typeof basicOut === 'string' && basicOut.indexOf(MARK) > 0)
|
|
82
|
-
assert('基础预览:只放宽 script-src', /script-src 'unsafe-inline'/.test(basicOut) && !/script-src 'none'/.test(basicOut))
|
|
83
|
-
assert(
|
|
84
|
-
'基础预览:其余 CSP 一条不动(不许联网/外链)',
|
|
85
|
-
/default-src 'none'/.test(basicOut) && /connect-src 'none'/.test(basicOut) && /img-src data:/.test(basicOut) && /frame-src 'none'/.test(basicOut),
|
|
86
|
-
)
|
|
87
|
-
assert('基础预览:原文一字不少', basicOut.indexOf('the migration ran long, so we ship Wednesday') > 0)
|
|
88
|
-
assert('基础预览:只注入一段脚本', basicOut.split(MARK).length - 1 === 1, String(basicOut.split(MARK).length - 1))
|
|
89
|
-
assert('基础预览:脚本落在 <head> 内(head 之后)', basicOut.indexOf('<head>') < basicOut.indexOf(MARK) && basicOut.indexOf(MARK) < basicOut.indexOf('</head>'))
|
|
90
|
-
assert('基础预览:重复注入返回 null(幂等)', api.bridgeIntoHtml(basicOut) === null)
|
|
91
|
-
|
|
92
|
-
// 交互式预览:外层是宿主的 bootstrap(charset → script),桥必须插在 charset 之后、bootstrap 之前
|
|
93
|
-
const BOOTSTRAP = '<!doctype html><meta charset="utf-8"><script>(()=>{document.open();document.write("<p>x</p>");document.close()})()</scr' + 'ipt>'
|
|
94
|
-
const bootstrapOut = api.bridgeIntoHtml(BOOTSTRAP)
|
|
95
|
-
assert(
|
|
96
|
-
'交互式 bootstrap:插在 charset 之后、bootstrap 之前',
|
|
97
|
-
bootstrapOut.indexOf('<meta charset="utf-8">') < bootstrapOut.indexOf(MARK) && bootstrapOut.indexOf(MARK) < bootstrapOut.indexOf('<script>'),
|
|
98
|
-
)
|
|
99
|
-
assert('交互式 bootstrap:没有 CSP 时不硬塞 CSP', bootstrapOut.indexOf('Content-Security-Policy') < 0)
|
|
100
|
-
|
|
101
|
-
// 没有 head / html / charset 的碎片:插在最前(不能因为找不到锚点就丢掉)
|
|
102
|
-
const fragmentOut = api.bridgeIntoHtml('<p>裸片段</p>')
|
|
103
|
-
assert('碎片文档:插在最前', fragmentOut.indexOf('<script') === 0 && fragmentOut.indexOf('<p>裸片段</p>') > 0, fragmentOut.slice(0, 30))
|
|
104
|
-
assert('空串 / null 不注入', api.bridgeIntoHtml('') === null && api.bridgeIntoHtml(null) === null)
|
|
105
|
-
|
|
106
|
-
// 插入位置:doctype 之后(有 doctype 但没 html/head/charset 时)
|
|
107
|
-
const doctypeOut = api.insertIntoHtml('<!doctype html><p>t</p>', '<X>')
|
|
108
|
-
assert('只有 doctype 时插在 doctype 之后(不破坏怪异模式判断)', doctypeOut === '<!doctype html><X><p>t</p>', doctypeOut)
|
|
109
|
-
|
|
110
|
-
// ───────────────────────── ② 帧内脚本行为 ─────────────────────────
|
|
111
|
-
|
|
112
|
-
/** 把 bridgeBody 变成注入用的脚本本体(与 bridgeIntoHtml 里同一形态)。 */
|
|
113
|
-
const FRAME_SCRIPT = '(' + String(api.bridgeBody) + ')();'
|
|
114
|
-
|
|
115
|
-
/** 帧内的最小 DOM:够 getSelection + createRange + closest + innerText 用。 */
|
|
116
|
-
function makeFrameEnv(htmlText, selected, rect, containerTag, muteDocListeners) {
|
|
117
|
-
const sent = []
|
|
118
|
-
const timers = []
|
|
119
|
-
|
|
120
|
-
class Node {
|
|
121
|
-
constructor(tag, text) {
|
|
122
|
-
this.tagName = tag
|
|
123
|
-
this.nodeType = tag === '#text' ? 3 : 1
|
|
124
|
-
this.children = []
|
|
125
|
-
this.parentElement = null
|
|
126
|
-
this.parentNode = null
|
|
127
|
-
this._text = text || ''
|
|
128
|
-
this.className = ''
|
|
129
|
-
this.attrs = {}
|
|
130
|
-
}
|
|
131
|
-
appendChild(child) {
|
|
132
|
-
child.parentElement = this
|
|
133
|
-
child.parentNode = this
|
|
134
|
-
this.children.push(child)
|
|
135
|
-
return child
|
|
136
|
-
}
|
|
137
|
-
/** 桥里的 collectText 走 childNodes / nodeValue(真实 DOM 的文本节点属性)。 */
|
|
138
|
-
get childNodes() {
|
|
139
|
-
return this.children
|
|
140
|
-
}
|
|
141
|
-
get nodeValue() {
|
|
142
|
-
return this._text
|
|
143
|
-
}
|
|
144
|
-
get textContent() {
|
|
145
|
-
return (this._text || '') + this.children.map((c) => c.textContent).join('')
|
|
146
|
-
}
|
|
147
|
-
get innerText() {
|
|
148
|
-
return this.textContent
|
|
149
|
-
}
|
|
150
|
-
getAttribute(key) {
|
|
151
|
-
return this.attrs[key] ?? null
|
|
152
|
-
}
|
|
153
|
-
setAttribute(key, value) {
|
|
154
|
-
this.attrs[key] = String(value)
|
|
155
|
-
}
|
|
156
|
-
closest(selector) {
|
|
157
|
-
const want = String(selector).split(',').map((s) => s.trim().toUpperCase())
|
|
158
|
-
let node = this
|
|
159
|
-
while (node) {
|
|
160
|
-
if (want.includes(node.tagName)) return node
|
|
161
|
-
node = node.parentElement
|
|
162
|
-
}
|
|
163
|
-
return null
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const body = new Node('BODY')
|
|
168
|
-
const paragraph = new Node(containerTag || 'P')
|
|
169
|
-
const textNode = new Node('#text', htmlText)
|
|
170
|
-
paragraph.appendChild(textNode)
|
|
171
|
-
body.appendChild(paragraph)
|
|
172
|
-
|
|
173
|
-
// 选区是可变的:拖拽用例要在"按住鼠标"期间改变它
|
|
174
|
-
const state = { text: selected, collapsed: false, rect: rect }
|
|
175
|
-
const range = {
|
|
176
|
-
startContainer: textNode,
|
|
177
|
-
get startOffset() {
|
|
178
|
-
return htmlText.indexOf(state.text)
|
|
179
|
-
},
|
|
180
|
-
toString: () => state.text,
|
|
181
|
-
getClientRects: () => [state.rect],
|
|
182
|
-
getBoundingClientRect: () => state.rect,
|
|
183
|
-
}
|
|
184
|
-
const selection = {
|
|
185
|
-
rangeCount: 1,
|
|
186
|
-
get isCollapsed() {
|
|
187
|
-
return state.collapsed
|
|
188
|
-
},
|
|
189
|
-
toString: () => state.text,
|
|
190
|
-
getRangeAt: () => range,
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const doc = {
|
|
194
|
-
body,
|
|
195
|
-
title: 'fixture',
|
|
196
|
-
_listeners: new Map(),
|
|
197
|
-
addEventListener(type, handler) {
|
|
198
|
-
// muteDocListeners:模拟"document.write 把 document 上的监听冲掉了"的交互式预览,
|
|
199
|
-
// 这时只剩 window 上的监听 + 250ms 轮询兜底
|
|
200
|
-
if (muteDocListeners) return
|
|
201
|
-
if (!this._listeners.has(type)) this._listeners.set(type, [])
|
|
202
|
-
this._listeners.get(type).push(handler)
|
|
203
|
-
},
|
|
204
|
-
dispatch(type, event) {
|
|
205
|
-
for (const handler of [...(this._listeners.get(type) || [])]) handler(event)
|
|
206
|
-
},
|
|
207
|
-
hasFocus: () => true,
|
|
208
|
-
getSelection: () => selection,
|
|
209
|
-
createRange() {
|
|
210
|
-
return {
|
|
211
|
-
_node: null,
|
|
212
|
-
_end: null,
|
|
213
|
-
selectNodeContents(node) {
|
|
214
|
-
this._node = node
|
|
215
|
-
},
|
|
216
|
-
setEnd(node, offset) {
|
|
217
|
-
this._end = { node, offset }
|
|
218
|
-
},
|
|
219
|
-
toString() {
|
|
220
|
-
const full = this._node.textContent
|
|
221
|
-
return this._end ? full.slice(0, this._end.offset) : full
|
|
222
|
-
},
|
|
223
|
-
}
|
|
224
|
-
},
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const win = {
|
|
228
|
-
_listeners: new Map(),
|
|
229
|
-
addEventListener(type, handler) {
|
|
230
|
-
if (!this._listeners.has(type)) this._listeners.set(type, [])
|
|
231
|
-
this._listeners.get(type).push(handler)
|
|
232
|
-
},
|
|
233
|
-
dispatch(type, event) {
|
|
234
|
-
for (const handler of [...(this._listeners.get(type) || [])]) handler(event)
|
|
235
|
-
},
|
|
236
|
-
getSelection: () => selection,
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const parent = {
|
|
240
|
-
postMessage(message) {
|
|
241
|
-
sent.push(message)
|
|
242
|
-
},
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const intervals = []
|
|
246
|
-
const run = new Function('window', 'document', 'parent', 'setTimeout', 'setInterval', FRAME_SCRIPT)
|
|
247
|
-
run(win, doc, parent, (fn, ms) => timers.push({ fn, ms }), (fn) => intervals.push(fn))
|
|
248
|
-
// 注入后脚本自己排了 setTimeout(report, 60):手动跑掉所有排队的定时器
|
|
249
|
-
for (const timer of timers.splice(0)) timer.fn()
|
|
250
|
-
return {
|
|
251
|
-
win,
|
|
252
|
-
doc,
|
|
253
|
-
sent,
|
|
254
|
-
timers,
|
|
255
|
-
intervals,
|
|
256
|
-
selection,
|
|
257
|
-
/** 跑掉排队的 setTimeout(mouseup 之后那次 report 就在里面)。 */
|
|
258
|
-
flushTimers() {
|
|
259
|
-
for (const timer of timers.splice(0)) timer.fn()
|
|
260
|
-
},
|
|
261
|
-
/** 跑一拍轮询(250ms 那次)。 */
|
|
262
|
-
tick() {
|
|
263
|
-
for (const fn of intervals) fn()
|
|
264
|
-
},
|
|
265
|
-
/** 改选区(拖拽中每动一下都算一次变化)。 */
|
|
266
|
-
setSelection(text, nextRect) {
|
|
267
|
-
state.text = text
|
|
268
|
-
state.collapsed = !text
|
|
269
|
-
if (nextRect) state.rect = nextRect
|
|
270
|
-
doc.dispatch('selectionchange', {})
|
|
271
|
-
},
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const FIXTURE = 'PM: 这周能发布吗? Dev: the migration ran long, so we ship Wednesday.'
|
|
276
|
-
const RECT = { left: 12, top: 34, right: 190, bottom: 52, width: 178, height: 18 }
|
|
277
|
-
const longSelection = '长'.repeat(4500)
|
|
278
|
-
const longFrame = makeFrameEnv(longSelection, longSelection, RECT)
|
|
279
|
-
assert('帧内不以旧的 4000 字默认限制提前拦截(实际限额由父页面设置判断)',
|
|
280
|
-
longFrame.sent.some(m => m.kind === 'selection' && m.sel.text.length === 4500))
|
|
281
|
-
const env = makeFrameEnv(FIXTURE, 'the migration ran long', RECT)
|
|
282
|
-
const first = env.sent[env.sent.length - 1]
|
|
283
|
-
assert('帧内脚本:注入后立刻上报一次', !!first && first.__dshSel === 1 && first.kind === 'selection')
|
|
284
|
-
assert('帧内脚本:报的文字就是选中文字', first && first.sel && first.sel.text === 'the migration ran long', first && first.sel && first.sel.text)
|
|
285
|
-
assert('帧内脚本:报的是帧内视口坐标', first && first.sel.rect.x === 12 && first.sel.rect.bottom === 52 && first.sel.rect.w === 178, JSON.stringify(first && first.sel.rect))
|
|
286
|
-
assert(
|
|
287
|
-
'帧内脚本:上下文用【】标出选中部分,前后文都在',
|
|
288
|
-
first && first.sel.context === 'PM: 这周能发布吗? Dev: 【the migration ran long】, so we ship Wednesday.',
|
|
289
|
-
first && first.sel.context,
|
|
290
|
-
)
|
|
291
|
-
assert('帧内脚本:keyContext 只取选中之前那一小段', first && first.sel.keyContext === 'PM: 这周能发布吗? Dev:', first && first.sel.keyContext)
|
|
292
|
-
assert('帧内脚本:普通段落不给标签(标签由父页面按帧来源定)', first && first.sel.label === '', first && first.sel.label)
|
|
293
|
-
|
|
294
|
-
// 重复 report 不应该重复发(轮询每 250ms 一次)
|
|
295
|
-
env.win.dispatch('mouseup', {})
|
|
296
|
-
env.flushTimers()
|
|
297
|
-
assert('帧内脚本:同一选区 + 同一位置不重复上报', env.sent.filter((m) => m.kind === 'selection').length === 1, String(env.sent.filter((m) => m.kind === 'selection').length))
|
|
298
|
-
|
|
299
|
-
// 选区清空 → 发 clear(父页面据此收浮标)
|
|
300
|
-
env.setSelection('')
|
|
301
|
-
env.win.dispatch('mouseup', {})
|
|
302
|
-
env.flushTimers()
|
|
303
|
-
assert('帧内脚本:选区清空后发 clear', env.sent[env.sent.length - 1].kind === 'clear', env.sent[env.sent.length - 1].kind)
|
|
304
|
-
|
|
305
|
-
// ── 浮现时机(拖拽中不许冒浮标)──
|
|
306
|
-
const dragEnv = makeFrameEnv(FIXTURE, '', RECT)
|
|
307
|
-
dragEnv.setSelection('')
|
|
308
|
-
const dragSent = () => dragEnv.sent.filter((m) => m.kind === 'selection').length
|
|
309
|
-
dragEnv.win.dispatch('mousedown', {})
|
|
310
|
-
assert('帧内脚本:按下先发一条 press(父页面据此收浮标)', dragEnv.sent[dragEnv.sent.length - 1].kind === 'press', JSON.stringify(dragEnv.sent.map((m) => m.kind)))
|
|
311
|
-
dragEnv.setSelection('the')
|
|
312
|
-
dragEnv.setSelection('the migration')
|
|
313
|
-
dragEnv.setSelection('the migration ran long')
|
|
314
|
-
dragEnv.flushTimers()
|
|
315
|
-
assert('帧内脚本:拖拽进行中(按住鼠标)不上报', dragSent() === 0, String(dragSent()))
|
|
316
|
-
dragEnv.win.dispatch('mouseup', {})
|
|
317
|
-
dragEnv.flushTimers()
|
|
318
|
-
assert('帧内脚本:松手后才上报一次', dragSent() === 1, String(dragSent()))
|
|
319
|
-
assert('帧内脚本:松手那次报的就是最终选区', dragEnv.sent[dragEnv.sent.length - 1].sel.text === 'the migration ran long')
|
|
320
|
-
|
|
321
|
-
// 松手落在帧外(收不到 mouseup):轮询不能永久卡死
|
|
322
|
-
dragEnv.win.dispatch('mousedown', {})
|
|
323
|
-
const staleEnv = dragEnv
|
|
324
|
-
staleEnv.setSelection('ran long')
|
|
325
|
-
staleEnv.tick()
|
|
326
|
-
assert('帧内脚本:按住时轮询也不报', dragSent() === 1, String(dragSent()))
|
|
327
|
-
// 把"按下时间"往前拨:模拟松手发生在帧外、已经过了 STALE_PRESS_MS
|
|
328
|
-
const realNow = Date.now
|
|
329
|
-
Date.now = () => realNow() + 7000
|
|
330
|
-
staleEnv.tick()
|
|
331
|
-
staleEnv.tick()
|
|
332
|
-
Date.now = realNow
|
|
333
|
-
assert('帧内脚本:超时后轮询恢复上报(松手落在帧外也能自愈)', dragSent() === 2, String(dragSent()))
|
|
334
|
-
|
|
335
|
-
// 轮询的"稳定一拍"(交互式预览里 document 上的监听被 document.write 冲掉后,只剩这条路)
|
|
336
|
-
const pollEnv = makeFrameEnv(FIXTURE, '', RECT, 'P', true)
|
|
337
|
-
pollEnv.setSelection('migration')
|
|
338
|
-
pollEnv.tick()
|
|
339
|
-
assert('帧内脚本:轮询第一拍看到变化只记账、不报', pollEnv.sent.filter((m) => m.kind === 'selection').length === 0, String(pollEnv.sent.length))
|
|
340
|
-
pollEnv.tick()
|
|
341
|
-
assert('帧内脚本:稳定一拍后轮询上报', pollEnv.sent.filter((m) => m.kind === 'selection').length === 1, String(pollEnv.sent.length))
|
|
342
|
-
|
|
343
|
-
// 键盘扩选(Shift+方向键):没有 mousedown,selectionchange 立刻上报
|
|
344
|
-
const keyEnv = makeFrameEnv(FIXTURE, '', RECT)
|
|
345
|
-
keyEnv.setSelection('')
|
|
346
|
-
keyEnv.setSelection('the migration')
|
|
347
|
-
keyEnv.win.dispatch('keyup', { key: 'ArrowRight' })
|
|
348
|
-
keyEnv.flushTimers()
|
|
349
|
-
assert('帧内脚本:键盘扩选即时上报(不受拖拽闸门影响)', keyEnv.sent.filter((m) => m.kind === 'selection').length === 1, String(keyEnv.sent.length))
|
|
350
|
-
|
|
351
|
-
// ── 消失时机:别"自己消失" ──
|
|
352
|
-
// 页面重绘/动画期间 getClientRects() 会短暂拿不到矩形 → read() 返回 null。
|
|
353
|
-
// 这种瞬时读不到不能当成"选区没了",否则用户看到的就是浮标自己消失。
|
|
354
|
-
const flapEnv = makeFrameEnv(FIXTURE, '', RECT, 'P', true) // 关掉 document 监听,逼它走轮询
|
|
355
|
-
flapEnv.setSelection('migration')
|
|
356
|
-
flapEnv.tick()
|
|
357
|
-
flapEnv.tick()
|
|
358
|
-
assert('(准备)瞬时抖动前浮标是亮的', flapEnv.sent.filter((m) => m.kind === 'selection').length === 1)
|
|
359
|
-
flapEnv.setSelection('', null) // 读不到了(瞬时)
|
|
360
|
-
flapEnv.tick()
|
|
361
|
-
flapEnv.tick()
|
|
362
|
-
assert('帧内脚本:连续两拍读不到选区也不 clear(页面重绘的瞬时抖动)', flapEnv.sent.filter((m) => m.kind === 'clear').length === 0, JSON.stringify(flapEnv.sent.map((m) => m.kind)))
|
|
363
|
-
for (let i = 0; i < 6; i += 1) flapEnv.tick()
|
|
364
|
-
assert('帧内脚本:连续 6 拍都读不到才 clear(选区真的没了)', flapEnv.sent.filter((m) => m.kind === 'clear').length === 1, JSON.stringify(flapEnv.sent.map((m) => m.kind)))
|
|
365
|
-
|
|
366
|
-
// 滚动重报(帧内滚动挪浮标)不许有清空权
|
|
367
|
-
const scrollEnv = makeFrameEnv(FIXTURE, '', RECT, 'P', true)
|
|
368
|
-
scrollEnv.setSelection('migration')
|
|
369
|
-
scrollEnv.tick()
|
|
370
|
-
scrollEnv.tick()
|
|
371
|
-
scrollEnv.setSelection('')
|
|
372
|
-
scrollEnv.win.dispatch('scroll', {})
|
|
373
|
-
scrollEnv.flushTimers()
|
|
374
|
-
assert('帧内脚本:帧内滚动重报不清空(只有用户动作才有清空权)', scrollEnv.sent.filter((m) => m.kind === 'clear').length === 0, JSON.stringify(scrollEnv.sent.map((m) => m.kind)))
|
|
375
|
-
|
|
376
|
-
// 代码块里的选区 → 标签「代码块」(父页面拿它决定走「注释」提示词)
|
|
377
|
-
// 注意:容器判定与父页面 pickContainer 同规则(往上找到第一个 ≥120 字的元素),
|
|
378
|
-
// 所以 fixture 要够长,容器才会落在 <pre> 自己身上。
|
|
379
|
-
const CODE_FIXTURE = [
|
|
380
|
-
'function clampSelection(text, max) {',
|
|
381
|
-
' if (!text) return ""',
|
|
382
|
-
' return text.length > max ? text.slice(0, max) : text',
|
|
383
|
-
'}',
|
|
384
|
-
'export default clampSelection',
|
|
385
|
-
].join('\n')
|
|
386
|
-
const codeEnv = makeFrameEnv(CODE_FIXTURE, 'return text.length > max ? text.slice(0, max) : text', RECT, 'PRE')
|
|
387
|
-
assert(
|
|
388
|
-
'帧内脚本:pre 容器里的选区带「代码块」标签',
|
|
389
|
-
codeEnv.sent.length === 1 && codeEnv.sent[0].sel.label === '代码块',
|
|
390
|
-
JSON.stringify(codeEnv.sent.map((m) => m.sel && m.sel.label)),
|
|
391
|
-
)
|
|
392
|
-
|
|
393
|
-
// 表格里的选区 → 标签「表格」
|
|
394
|
-
const TABLE_FIXTURE =
|
|
395
|
-
'| 说法 | 在此处的含义 |\n| --- | --- |\n| the migration | 发布前那次数据迁移,跑完才敢发版 |\n| ran long | 耗时超出预期,所以顺延到周三 |\n| ship | 发布、上线(这里指把这一版推出去) |'
|
|
396
|
-
const tableEnv = makeFrameEnv(TABLE_FIXTURE, '耗时超出预期', RECT, 'TABLE')
|
|
397
|
-
assert('帧内脚本:table 容器里的选区带「表格」标签', tableEnv.sent[0].sel.label === '表格', tableEnv.sent[0].sel.label)
|
|
398
|
-
|
|
399
|
-
// 输入框里的选区 → 一律不报(别干扰输入)
|
|
400
|
-
const inputEnv = makeFrameEnv('hello world', 'world', RECT, 'TEXTAREA')
|
|
401
|
-
assert('帧内脚本:输入框里的选区不报', inputEnv.sent.length === 0, String(inputEnv.sent.length))
|
|
402
|
-
|
|
403
|
-
// ping 消息 → hello + 当前选区(父页面用来探测桥在不在)
|
|
404
|
-
const pinged = []
|
|
405
|
-
env.setSelection('the migration ran long')
|
|
406
|
-
env.win.dispatch('message', { data: { __dshSel: 1, kind: 'ping' } })
|
|
407
|
-
env.flushTimers()
|
|
408
|
-
assert('帧内脚本:应答 ping(hello + 当前选区)', env.sent.some((m) => m.kind === 'hello') && env.sent.some((m) => m.kind === 'selection'), env.sent.map((m) => m.kind).join(','))
|
|
409
|
-
void pinged
|
|
410
|
-
|
|
411
|
-
console.log(`\n=== 划词桥单测结束:${failed === 0 ? '全部通过' : failed + ' 项失败'} ===`)
|
|
412
|
-
process.exit(failed === 0 ? 0 : 1)
|