gis-view-webgl2 0.1.13 → 0.1.15
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/dist/index.cjs +1 -1
- package/dist/index.d.ts +140 -60
- package/dist/index.js +1 -1
- package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +73 -5
- package/docs/GisView.md +49 -49
- package/docs/ai/06-/344/274/232/350/257/235/347/264/242/345/274/225/344/270/216/345/244/215/346/237/245/346/226/271/346/263/225.md +87 -16
- package/docs/demos/ApiCoverageDemo.vue +1 -5
- package/docs/demos/BackendSyncDemo.vue +1 -2
- package/docs/demos/BackgroundDemo.vue +68 -1
- package/docs/demos/BasicDemo.vue +76 -76
- package/docs/demos/BatchAddDemo.vue +2 -1
- package/docs/demos/ChannelDemo.vue +1 -3
- package/docs/demos/ClusterDemo.vue +48 -1
- package/docs/demos/CopyMergeDemo.vue +899 -904
- package/docs/demos/DemoShell.vue +336 -336
- package/docs/demos/EventDemo.vue +300 -300
- package/docs/demos/ExtensionDemo.vue +200 -200
- package/docs/demos/GisViewDemo.vue +64 -64
- package/docs/demos/LabelDemo.vue +489 -489
- package/docs/demos/StyleDemo.vue +67 -67
- package/docs/demos/api-index.js +24 -29
- package/docs/demos/demos.js +92 -92
- package/docs/demos/gis-view-demo/useGisViewDemo.js +19 -14
- package/docs/demos/shared/demoData.js +107 -107
- package/docs/demos/shared/demoStyle.css +249 -249
- package/docs/format/02-/350/212/202/347/202/271/344/270/216/347/272/277/350/267/257.md +3 -3
- package/docs/format/03-/346/213/220/347/202/271/350/243/205/351/245/260/344/270/216/346/240/267/345/274/217.md +3 -0
- package/docs/from0/01-/345/205/210/346/212/212/345/234/260/345/233/276/346/203/263/346/270/205/346/245/232.md +8 -7
- package/docs/index.html +3 -3
- package/docs/internals/04-/347/233/270/346/234/272/345/235/220/346/240/207/344/270/216/345/203/217/347/264/240.md +6 -4
- package/docs/internals/14-/350/203/214/346/231/257/345/261/202.md +2 -2
- package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +36 -16
- package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +38 -84
- package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +36 -19
- package/docs/manual/06-/346/225/260/346/215/256/344/270/216/345/234/272/346/231/257/346/233/264/346/226/260.md +1 -1
- package/docs/manual/07-/346/214/207/351/222/210/344/270/216/350/246/201/347/264/240/347/274/226/350/276/221.md +7 -7
- package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +15 -15
- package/docs/manual/10-/351/200/232/351/201/223/347/274/226/350/276/221/344/270/216/346/226/260/345/242/236.md +1 -1
- package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +6 -0
- package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +1 -1
- package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +2 -0
- package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +10 -8
- package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +11 -7
- package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +27 -8
- package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +27 -4
- package/docs/manual/21-/350/257/264/346/230/216/344/270/216/345/270/270/350/247/201/351/227/256/351/242/230.md +3 -3
- package/docs/playground/index.html +1 -1
- package/docs/sample-document/345/255/227/346/256/265/350/257/246/350/247/243.md +2 -2
- package/docs/steps/check-doc-truth.mjs +1 -1
- package/docs/steps/selftest-actions.html +4 -4
- package/docs/steps/selftest-data.html +5 -5
- package/docs/steps/selftest-demos.html +1683 -1619
- package/docs/steps/selftest-docs.html +4 -4
- package/docs/steps/selftest-effects.html +1 -1
- package/docs/steps/selftest-features.html +548 -47
- package/docs/steps/selftest-full.html +566 -566
- package/docs/steps/selftest-labels.html +647 -647
- package/docs/steps/selftest-perf.html +2282 -2237
- package/docs/steps/selftest-select.html +53 -14
- package/docs/steps/step-10-full.js +1 -4
- package/docs/steps/step-12-events.js +1 -4
- package/package.json +43 -43
|
@@ -1,102 +1,102 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<title>demos</title>
|
|
6
|
-
</head>
|
|
7
|
-
<body>
|
|
8
|
-
<div id="app"></div>
|
|
9
|
-
<pre id="out" style="position:fixed;z-index:99;left:0;bottom:0;background:#fff;max-height:40vh;overflow:auto"></pre>
|
|
10
|
-
<script>
|
|
11
|
-
// 自测从「干净的一次访问」开始:清掉上次的页签记忆与地址栏点播。
|
|
12
|
-
// 这一页在跑的过程中若被 dev server 的一次整页热更新重载,新的一份才不会从「上次看过的演示」起步,
|
|
13
|
-
// 让开头那几条「默认进综合示例」的断言变成假失败(外壳读的正是这两个来源)。
|
|
14
|
-
try {
|
|
15
|
-
window.localStorage.removeItem('gis-view:last-demo')
|
|
16
|
-
}
|
|
17
|
-
catch { /* 存储不可用就当没记过 */ }
|
|
18
|
-
if (window.location.search) {
|
|
19
|
-
window.history.replaceState(null, '', window.location.pathname)
|
|
20
|
-
}
|
|
21
|
-
</script>
|
|
22
|
-
<script type="module">
|
|
23
|
-
// 演示外壳自测:默认进综合示例、页签能切、每个功能演示都起得来,
|
|
24
|
-
// 以及标签演示这一轮新增的交互(点选 / 右键菜单 / 批量加标签 / 点选即加)。
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>demos</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<div id="app"></div>
|
|
9
|
+
<pre id="out" style="position:fixed;z-index:99;left:0;bottom:0;background:#fff;max-height:40vh;overflow:auto"></pre>
|
|
10
|
+
<script>
|
|
11
|
+
// 自测从「干净的一次访问」开始:清掉上次的页签记忆与地址栏点播。
|
|
12
|
+
// 这一页在跑的过程中若被 dev server 的一次整页热更新重载,新的一份才不会从「上次看过的演示」起步,
|
|
13
|
+
// 让开头那几条「默认进综合示例」的断言变成假失败(外壳读的正是这两个来源)。
|
|
14
|
+
try {
|
|
15
|
+
window.localStorage.removeItem('gis-view:last-demo')
|
|
16
|
+
}
|
|
17
|
+
catch { /* 存储不可用就当没记过 */ }
|
|
18
|
+
if (window.location.search) {
|
|
19
|
+
window.history.replaceState(null, '', window.location.pathname)
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
<script type="module">
|
|
23
|
+
// 演示外壳自测:默认进综合示例、页签能切、每个功能演示都起得来,
|
|
24
|
+
// 以及标签演示这一轮新增的交互(点选 / 右键菜单 / 批量加标签 / 点选即加)。
|
|
25
25
|
import { createApp } from 'vue'
|
|
26
26
|
import App from '/src/App.vue'
|
|
27
27
|
import { channelDraftArrowPlacement } from '/src/gis-view/engine.js'
|
|
28
28
|
import { createMinimap } from '/src/gis-view/minimap.js'
|
|
29
|
-
const results = []
|
|
30
|
-
const flush = () => { document.getElementById('out').textContent = results.join('\n') }
|
|
31
|
-
const check = (name, ok, extra = '') => {
|
|
32
|
-
results.push(`${ok ? 'PASS' : 'FAIL'} ${name}${extra === '' ? '' : ` — ${extra}`}`)
|
|
33
|
-
flush()
|
|
34
|
-
}
|
|
35
|
-
const diag = (name, extra) => { results.push(`DIAG ${name} — ${extra}`); flush() }
|
|
36
|
-
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
37
|
-
const until = async (fn, timeout = 20000) => {
|
|
38
|
-
const start = performance.now()
|
|
39
|
-
while (performance.now() - start < timeout) {
|
|
40
|
-
const value = await fn()
|
|
41
|
-
if (value) {
|
|
42
|
-
return value
|
|
43
|
-
}
|
|
44
|
-
await wait(120)
|
|
45
|
-
}
|
|
46
|
-
return null
|
|
47
|
-
}
|
|
48
|
-
const tabs = () => [...document.querySelectorAll('.demo-shell__tab')]
|
|
49
|
-
const findTab = text => tabs().find(tab => tab.textContent.trim().includes(text))
|
|
50
|
-
const clickTab = async (text) => {
|
|
51
|
-
const tab = findTab(text)
|
|
52
|
-
if (!tab) {
|
|
53
|
-
return false
|
|
54
|
-
}
|
|
55
|
-
tab.click()
|
|
56
|
-
return true
|
|
57
|
-
}
|
|
58
|
-
const canvasReady = async (timeout = 20000) => until(() => document.querySelector('canvas.gis-view__canvas'), timeout)
|
|
59
|
-
const noticeText = () => document.querySelector('.demo-shell__notice')?.textContent.trim() ?? ''
|
|
60
|
-
const logText = () => [...document.querySelectorAll('.dv-log li')].map(node => node.textContent.trim()).join(' | ')
|
|
61
|
-
/** 合成一次「点一下」:引擎是按 pointerdown / pointerup 一对判的 */
|
|
62
|
-
const tap = (node, options = {}) => {
|
|
63
|
-
const rect = node.getBoundingClientRect()
|
|
64
|
-
const init = {
|
|
65
|
-
bubbles: true,
|
|
66
|
-
cancelable: true,
|
|
67
|
-
composed: true,
|
|
68
|
-
pointerId: 1,
|
|
69
|
-
pointerType: 'mouse',
|
|
70
|
-
isPrimary: true,
|
|
71
|
-
button: 0,
|
|
72
|
-
clientX: options.clientX ?? rect.left + rect.width / 2,
|
|
73
|
-
clientY: options.clientY ?? rect.top + rect.height / 2
|
|
74
|
-
}
|
|
75
|
-
node.dispatchEvent(new PointerEvent('pointerdown', init))
|
|
76
|
-
node.dispatchEvent(new PointerEvent('pointerup', init))
|
|
77
|
-
}
|
|
78
|
-
const contextMenuOn = (node, clientX, clientY) => {
|
|
79
|
-
const event = new MouseEvent('contextmenu', { bubbles: true, cancelable: true, composed: true, clientX, clientY })
|
|
80
|
-
node.dispatchEvent(event)
|
|
81
|
-
return event
|
|
82
|
-
}
|
|
83
|
-
// 合成事件下没有真实指针,setPointerCapture 由自测自己兜住(与 selftest-labels.html 同一套)
|
|
84
|
-
if (!Element.prototype.setPointerCapture) {
|
|
85
|
-
Element.prototype.setPointerCapture = () => {}
|
|
86
|
-
Element.prototype.releasePointerCapture = () => {}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
createApp(App).mount('#app')
|
|
91
|
-
// 1. 外壳:默认综合示例,页签齐全
|
|
92
|
-
const loaded = await canvasReady()
|
|
93
|
-
check('演示外壳把默认演示(综合示例)挂起来了', Boolean(loaded), loaded ? 'canvas 就绪' : '没等到 canvas')
|
|
29
|
+
const results = []
|
|
30
|
+
const flush = () => { document.getElementById('out').textContent = results.join('\n') }
|
|
31
|
+
const check = (name, ok, extra = '') => {
|
|
32
|
+
results.push(`${ok ? 'PASS' : 'FAIL'} ${name}${extra === '' ? '' : ` — ${extra}`}`)
|
|
33
|
+
flush()
|
|
34
|
+
}
|
|
35
|
+
const diag = (name, extra) => { results.push(`DIAG ${name} — ${extra}`); flush() }
|
|
36
|
+
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
37
|
+
const until = async (fn, timeout = 20000) => {
|
|
38
|
+
const start = performance.now()
|
|
39
|
+
while (performance.now() - start < timeout) {
|
|
40
|
+
const value = await fn()
|
|
41
|
+
if (value) {
|
|
42
|
+
return value
|
|
43
|
+
}
|
|
44
|
+
await wait(120)
|
|
45
|
+
}
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
const tabs = () => [...document.querySelectorAll('.demo-shell__tab')]
|
|
49
|
+
const findTab = text => tabs().find(tab => tab.textContent.trim().includes(text))
|
|
50
|
+
const clickTab = async (text) => {
|
|
51
|
+
const tab = findTab(text)
|
|
52
|
+
if (!tab) {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
tab.click()
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
const canvasReady = async (timeout = 20000) => until(() => document.querySelector('canvas.gis-view__canvas'), timeout)
|
|
59
|
+
const noticeText = () => document.querySelector('.demo-shell__notice')?.textContent.trim() ?? ''
|
|
60
|
+
const logText = () => [...document.querySelectorAll('.dv-log li')].map(node => node.textContent.trim()).join(' | ')
|
|
61
|
+
/** 合成一次「点一下」:引擎是按 pointerdown / pointerup 一对判的 */
|
|
62
|
+
const tap = (node, options = {}) => {
|
|
63
|
+
const rect = node.getBoundingClientRect()
|
|
64
|
+
const init = {
|
|
65
|
+
bubbles: true,
|
|
66
|
+
cancelable: true,
|
|
67
|
+
composed: true,
|
|
68
|
+
pointerId: 1,
|
|
69
|
+
pointerType: 'mouse',
|
|
70
|
+
isPrimary: true,
|
|
71
|
+
button: 0,
|
|
72
|
+
clientX: options.clientX ?? rect.left + rect.width / 2,
|
|
73
|
+
clientY: options.clientY ?? rect.top + rect.height / 2
|
|
74
|
+
}
|
|
75
|
+
node.dispatchEvent(new PointerEvent('pointerdown', init))
|
|
76
|
+
node.dispatchEvent(new PointerEvent('pointerup', init))
|
|
77
|
+
}
|
|
78
|
+
const contextMenuOn = (node, clientX, clientY) => {
|
|
79
|
+
const event = new MouseEvent('contextmenu', { bubbles: true, cancelable: true, composed: true, clientX, clientY })
|
|
80
|
+
node.dispatchEvent(event)
|
|
81
|
+
return event
|
|
82
|
+
}
|
|
83
|
+
// 合成事件下没有真实指针,setPointerCapture 由自测自己兜住(与 selftest-labels.html 同一套)
|
|
84
|
+
if (!Element.prototype.setPointerCapture) {
|
|
85
|
+
Element.prototype.setPointerCapture = () => {}
|
|
86
|
+
Element.prototype.releasePointerCapture = () => {}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
createApp(App).mount('#app')
|
|
91
|
+
// 1. 外壳:默认综合示例,页签齐全
|
|
92
|
+
const loaded = await canvasReady()
|
|
93
|
+
check('演示外壳把默认演示(综合示例)挂起来了', Boolean(loaded), loaded ? 'canvas 就绪' : '没等到 canvas')
|
|
94
94
|
check('页签列出全部演示(23 个:22 个功能 + 综合)', tabs().length === 23, `${tabs().length} 个页签:${tabs().map(tab => tab.textContent.trim()).join(' / ')}`)
|
|
95
95
|
check('页头的演示数量直接跟随注册表(23 个)', (document.querySelector('.page-header__count')?.textContent ?? '').includes('23 个'), document.querySelector('.page-header__count')?.textContent?.trim() ?? '没有页头计数')
|
|
96
|
-
check('默认页签是综合示例', (document.querySelector('.demo-shell__tab--active')?.textContent ?? '').includes('综合'), document.querySelector('.demo-shell__tab--active')?.textContent?.trim() ?? '')
|
|
97
|
-
check('默认页签写进地址栏(刷新还停在同一个演示)', location.search.includes('demo=full'), location.search)
|
|
98
|
-
check('综合示例的面板照旧在(拆分没有动它)', document.querySelectorAll('.demo-panel-buttons button').length >= 6, `${document.querySelectorAll('.demo-panel-buttons button').length} 个面板按钮`)
|
|
99
|
-
// 1.1 外壳的展现:分组页签、对应文档的直达链接、键盘翻页、上次看过的记在 localStorage
|
|
96
|
+
check('默认页签是综合示例', (document.querySelector('.demo-shell__tab--active')?.textContent ?? '').includes('综合'), document.querySelector('.demo-shell__tab--active')?.textContent?.trim() ?? '')
|
|
97
|
+
check('默认页签写进地址栏(刷新还停在同一个演示)', location.search.includes('demo=full'), location.search)
|
|
98
|
+
check('综合示例的面板照旧在(拆分没有动它)', document.querySelectorAll('.demo-panel-buttons button').length >= 6, `${document.querySelectorAll('.demo-panel-buttons button').length} 个面板按钮`)
|
|
99
|
+
// 1.1 外壳的展现:分组页签、对应文档的直达链接、键盘翻页、上次看过的记在 localStorage
|
|
100
100
|
const groupTabs = [...document.querySelectorAll('.demo-shell__group-tab')]
|
|
101
101
|
const groupLabels = groupTabs.map(tab => tab.querySelector('.demo-shell__group-label')?.textContent.trim() ?? '')
|
|
102
102
|
const groupLabelsVisible = groupTabs.every(tab => {
|
|
@@ -106,34 +106,34 @@
|
|
|
106
106
|
check('页签按分类分组(基础 / 交互 / 样式 / 数据 / 性能 / 综合),组名直接可读', groupLabels.join(',') === '基础,交互,样式,数据,性能,综合' && groupLabelsVisible, groupLabels.join(' / '))
|
|
107
107
|
const visibleGroups = [...document.querySelectorAll('.demo-shell__group--active')]
|
|
108
108
|
check('演示导航改成「分组 + 当前组页签」:六组按钮、一次只展开一组', groupTabs.length === 6 && visibleGroups.length === 1 && visibleGroups[0].querySelector('.demo-shell__tab--active') !== null, `${groupTabs.length} 组 / 展开 ${visibleGroups.length} 组 / ${groupTabs.map(node => node.textContent.trim()).join(' / ')}`)
|
|
109
|
-
const docLink = document.querySelector('.demo-shell__doc')
|
|
110
|
-
check('说明旁边给对应文档的直达链接(文档中心 + 文件名)', /docs\/index\.html/.test(docLink?.getAttribute('href') ?? '') && (docLink?.textContent ?? '').includes('对应文档'), docLink?.getAttribute('href') ?? '无')
|
|
111
|
-
const quickLinks = [...document.querySelectorAll('.demo-shell__quick-link')]
|
|
112
|
-
const quickLabels = quickLinks.map(link => link.textContent.trim())
|
|
113
|
-
check('演示外壳常驻文档中心 / API 速查 / 调试页 / 分步演示 / 自测五个入口', quickLinks.length === 5 && ['文档中心', 'API 速查', '调试页', '分步演示', '自测'].every(label => quickLabels.includes(label)) && quickLinks.every(link => link.getAttribute('href')?.includes('/docs/')), quickLabels.join(' / '))
|
|
114
|
-
window.dispatchEvent(new KeyboardEvent('keydown', { key: ']' }))
|
|
115
|
-
// Vue 的 DOM 更新在微任务里:派发之后等一拍再读当前页签(不然读到的还是旧的那一个)
|
|
116
|
-
await wait(60)
|
|
117
|
-
const afterBracket = document.querySelector('.demo-shell__tab--active')?.textContent.trim() ?? ''
|
|
118
|
-
check('键盘 ] 切到下一个演示(综合示例在最后,绕回基础视图)', afterBracket === '基础视图' && location.search.includes('demo=basic'), `${afterBracket} / ${location.search}`)
|
|
119
|
-
check('上次看过的演示写进 localStorage(下次打开接着看)', window.localStorage.getItem('gis-view:last-demo') === 'basic', String(window.localStorage.getItem('gis-view:last-demo')))
|
|
120
|
-
window.dispatchEvent(new KeyboardEvent('keydown', { key: '[' }))
|
|
121
|
-
await wait(60)
|
|
122
|
-
const afterBack = document.querySelector('.demo-shell__tab--active')?.textContent.trim() ?? ''
|
|
123
|
-
check('键盘 [ 往回翻(基础视图回到综合示例)', afterBack.includes('综合'), afterBack)
|
|
124
|
-
const eyebrow = document.querySelector('.demo-shell__eyebrow')?.textContent.trim() ?? ''
|
|
109
|
+
const docLink = document.querySelector('.demo-shell__doc')
|
|
110
|
+
check('说明旁边给对应文档的直达链接(文档中心 + 文件名)', /docs\/index\.html/.test(docLink?.getAttribute('href') ?? '') && (docLink?.textContent ?? '').includes('对应文档'), docLink?.getAttribute('href') ?? '无')
|
|
111
|
+
const quickLinks = [...document.querySelectorAll('.demo-shell__quick-link')]
|
|
112
|
+
const quickLabels = quickLinks.map(link => link.textContent.trim())
|
|
113
|
+
check('演示外壳常驻文档中心 / API 速查 / 调试页 / 分步演示 / 自测五个入口', quickLinks.length === 5 && ['文档中心', 'API 速查', '调试页', '分步演示', '自测'].every(label => quickLabels.includes(label)) && quickLinks.every(link => link.getAttribute('href')?.includes('/docs/')), quickLabels.join(' / '))
|
|
114
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: ']' }))
|
|
115
|
+
// Vue 的 DOM 更新在微任务里:派发之后等一拍再读当前页签(不然读到的还是旧的那一个)
|
|
116
|
+
await wait(60)
|
|
117
|
+
const afterBracket = document.querySelector('.demo-shell__tab--active')?.textContent.trim() ?? ''
|
|
118
|
+
check('键盘 ] 切到下一个演示(综合示例在最后,绕回基础视图)', afterBracket === '基础视图' && location.search.includes('demo=basic'), `${afterBracket} / ${location.search}`)
|
|
119
|
+
check('上次看过的演示写进 localStorage(下次打开接着看)', window.localStorage.getItem('gis-view:last-demo') === 'basic', String(window.localStorage.getItem('gis-view:last-demo')))
|
|
120
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: '[' }))
|
|
121
|
+
await wait(60)
|
|
122
|
+
const afterBack = document.querySelector('.demo-shell__tab--active')?.textContent.trim() ?? ''
|
|
123
|
+
check('键盘 [ 往回翻(基础视图回到综合示例)', afterBack.includes('综合'), afterBack)
|
|
124
|
+
const eyebrow = document.querySelector('.demo-shell__eyebrow')?.textContent.trim() ?? ''
|
|
125
125
|
check('当前演示显示分组与序号(综合 · 23 / 23)', eyebrow.includes('综合') && eyebrow.includes('23 / 23'), eyebrow)
|
|
126
|
-
document.querySelector('[data-demo=demo-next]')?.click()
|
|
127
|
-
await wait(60)
|
|
128
|
-
const afterStepNext = document.querySelector('.demo-shell__tab--active')?.textContent.trim() ?? ''
|
|
129
|
-
document.querySelector('[data-demo=demo-prev]')?.click()
|
|
130
|
-
await wait(60)
|
|
131
|
-
const afterStepPrev = document.querySelector('.demo-shell__tab--active')?.textContent.trim() ?? ''
|
|
132
|
-
check('上一项 / 下一项按钮与 [ ] 同一套顺序,并从头尾绕回', afterStepNext === '基础视图' && afterStepPrev.includes('综合'), `${afterStepNext} → ${afterStepPrev}`)
|
|
133
|
-
const keyGuard = document.createElement('input')
|
|
134
|
-
document.body.appendChild(keyGuard)
|
|
135
|
-
keyGuard.dispatchEvent(new KeyboardEvent('keydown', { key: ']', bubbles: true }))
|
|
136
|
-
await wait(60)
|
|
126
|
+
document.querySelector('[data-demo=demo-next]')?.click()
|
|
127
|
+
await wait(60)
|
|
128
|
+
const afterStepNext = document.querySelector('.demo-shell__tab--active')?.textContent.trim() ?? ''
|
|
129
|
+
document.querySelector('[data-demo=demo-prev]')?.click()
|
|
130
|
+
await wait(60)
|
|
131
|
+
const afterStepPrev = document.querySelector('.demo-shell__tab--active')?.textContent.trim() ?? ''
|
|
132
|
+
check('上一项 / 下一项按钮与 [ ] 同一套顺序,并从头尾绕回', afterStepNext === '基础视图' && afterStepPrev.includes('综合'), `${afterStepNext} → ${afterStepPrev}`)
|
|
133
|
+
const keyGuard = document.createElement('input')
|
|
134
|
+
document.body.appendChild(keyGuard)
|
|
135
|
+
keyGuard.dispatchEvent(new KeyboardEvent('keydown', { key: ']', bubbles: true }))
|
|
136
|
+
await wait(60)
|
|
137
137
|
const afterGuard = document.querySelector('.demo-shell__tab--active')?.textContent.trim() ?? ''
|
|
138
138
|
keyGuard.remove()
|
|
139
139
|
check('焦点在输入框里时键盘 [ / ] 不抢键', afterGuard.includes('综合'), afterGuard)
|
|
@@ -244,27 +244,27 @@
|
|
|
244
244
|
{ tab: '标签交互', id: 'labels' },
|
|
245
245
|
{ tab: '鹰眼图配置', id: 'minimap' },
|
|
246
246
|
{ tab: '样式与规则', id: 'style' },
|
|
247
|
-
{ tab: '通道与数据', id: 'channel' },
|
|
248
|
-
{ tab: '事件与方法', id: 'events' },
|
|
249
|
-
{ tab: '批量复制与合并', id: 'copy-merge' },
|
|
247
|
+
{ tab: '通道与数据', id: 'channel' },
|
|
248
|
+
{ tab: '事件与方法', id: 'events' },
|
|
249
|
+
{ tab: '批量复制与合并', id: 'copy-merge' },
|
|
250
250
|
{ tab: '拖拽落点', id: 'drag' },
|
|
251
251
|
{ tab: '可视化连线', id: 'connect' },
|
|
252
252
|
{ tab: '动态装饰器', id: 'decoration' },
|
|
253
253
|
{ tab: '批量新增', id: 'batch-add' },
|
|
254
254
|
{ tab: '搜索与筛选', id: 'search-filter' },
|
|
255
255
|
{ tab: '扩展能力', id: 'extension' },
|
|
256
|
-
{ tab: '编辑与保存', id: 'backend' },
|
|
256
|
+
{ tab: '编辑与保存', id: 'backend' },
|
|
257
257
|
{ tab: '性能与质量', id: 'perf' },
|
|
258
258
|
{ tab: '点聚合', id: 'cluster' },
|
|
259
259
|
{ tab: '动画效果', id: 'effects' },
|
|
260
260
|
{ tab: '扩展图形', id: 'graphic' }
|
|
261
|
-
]
|
|
261
|
+
]
|
|
262
262
|
for (const spec of demoSpecs) {
|
|
263
263
|
const clicked = await clickTab(spec.tab)
|
|
264
264
|
const ready = clicked ? await canvasReady() : null
|
|
265
265
|
const panel = document.querySelector('.dv-demo .dv-panel')
|
|
266
266
|
const active = document.querySelector('.demo-shell__tab--active')?.textContent.trim() ?? ''
|
|
267
|
-
check(`切到「${spec.tab}」:画布与读数面板都起来了`, Boolean(ready) && Boolean(panel) && active === spec.tab, `${ready ? 'canvas 就绪' : '没有 canvas'} / ${panel ? `${panel.querySelectorAll('.dv-group').length} 组读数` : '没有面板'} / 当前页签 ${active}`)
|
|
267
|
+
check(`切到「${spec.tab}」:画布与读数面板都起来了`, Boolean(ready) && Boolean(panel) && active === spec.tab, `${ready ? 'canvas 就绪' : '没有 canvas'} / ${panel ? `${panel.querySelectorAll('.dv-group').length} 组读数` : '没有面板'} / 当前页签 ${active}`)
|
|
268
268
|
await wait(260)
|
|
269
269
|
check(`「${spec.tab}」把演示 id 写进地址栏(?demo=${spec.id})`, location.search.includes(`demo=${spec.id}`), location.search)
|
|
270
270
|
}
|
|
@@ -782,6 +782,46 @@
|
|
|
782
782
|
check('背景与网架:旋转输入框可把 GeoJSON 角度精确设回 90 度',
|
|
783
783
|
Math.abs(setup.editRotation - 90) < 0.001 && geoRotationReset?.rotation === setup.editRotation,
|
|
784
784
|
JSON.stringify({ setup: setup.editRotation, resolved: geoRotationReset?.rotation }))
|
|
785
|
+
const saveLngLatButton = document.querySelector('[data-demo=background-save-lnglat]')
|
|
786
|
+
const clearNetworkButton = document.querySelector('[data-demo=background-clear-network]')
|
|
787
|
+
const restoreLngLatButton = document.querySelector('[data-demo=background-restore-lnglat]')
|
|
788
|
+
check('背景与网架:经纬度往返按钮齐全,GeoJSON 下保存可用、未保存时回灌禁用',
|
|
789
|
+
Boolean(saveLngLatButton && clearNetworkButton && restoreLngLatButton) &&
|
|
790
|
+
saveLngLatButton.disabled === false && clearNetworkButton.disabled === true && restoreLngLatButton.disabled === true,
|
|
791
|
+
JSON.stringify({ save: saveLngLatButton?.disabled, clear: clearNetworkButton?.disabled, restore: restoreLngLatButton?.disabled }))
|
|
792
|
+
const beforeRoundtripPoint = view.getData().points[0]
|
|
793
|
+
const beforeRoundtripScreen = view.project(beforeRoundtripPoint.x, beforeRoundtripPoint.y)
|
|
794
|
+
const beforeRoundtripCounts = [view.getData().points.length, view.getData().lines.length, view.getOptions().channels.length]
|
|
795
|
+
click('background-save-lnglat')
|
|
796
|
+
await wait(180)
|
|
797
|
+
const savedLngLat = setup.savedLngLat
|
|
798
|
+
const savedLngLatSnapshot = JSON.stringify(savedLngLat)
|
|
799
|
+
const savedLngLatReadout = text('background-lnglat-readout')
|
|
800
|
+
check('背景与网架:保存经纬度把整份网架写成经纬度,读数给出首节点世界坐标与经纬度',
|
|
801
|
+
savedLngLat?.points?.length === beforeRoundtripCounts[0] && savedLngLat?.lines?.length === beforeRoundtripCounts[1] &&
|
|
802
|
+
Array.isArray(savedLngLat?.channels) && savedLngLat.channels.length === beforeRoundtripCounts[2] &&
|
|
803
|
+
savedLngLatReadout.includes('已保存') && savedLngLatReadout.includes('经纬度') &&
|
|
804
|
+
clearNetworkButton.disabled === false && restoreLngLatButton.disabled === false,
|
|
805
|
+
savedLngLatReadout)
|
|
806
|
+
click('background-clear-network')
|
|
807
|
+
await wait(160)
|
|
808
|
+
const clearedCounts = [view.getData().points.length, view.getData().lines.length, view.getOptions().channels.length]
|
|
809
|
+
check('背景与网架:清空网架只删当前数据,已保存的经纬度仍在,回灌按钮保持可用',
|
|
810
|
+
clearedCounts.every(count => count === 0) && JSON.stringify(setup.savedLngLat) === savedLngLatSnapshot &&
|
|
811
|
+
text('background-lnglat-readout').includes('经纬度存档仍在') && restoreLngLatButton.disabled === false,
|
|
812
|
+
JSON.stringify({ clearedCounts, restored: Boolean(setup.savedLngLat) }))
|
|
813
|
+
click('background-restore-lnglat')
|
|
814
|
+
await wait(180)
|
|
815
|
+
const restoredPoint = view.getData().points.find(point => point.id === beforeRoundtripPoint.id)
|
|
816
|
+
const restoredScreen = restoredPoint ? view.project(restoredPoint.x, restoredPoint.y) : null
|
|
817
|
+
const restoredCounts = [view.getData().points.length, view.getData().lines.length, view.getOptions().channels.length]
|
|
818
|
+
check('背景与网架:mapLngLatToData 回灌恢复原数据与世界坐标,首节点屏幕落点不变,保存的经纬度不被修改',
|
|
819
|
+
restoredPoint && Math.abs(restoredPoint.x - beforeRoundtripPoint.x) < 1e-7 && Math.abs(restoredPoint.y - beforeRoundtripPoint.y) < 1e-7 &&
|
|
820
|
+
restoredScreen && Math.hypot(restoredScreen[0] - beforeRoundtripScreen[0], restoredScreen[1] - beforeRoundtripScreen[1]) < 1e-6 &&
|
|
821
|
+
restoredCounts.every((count, index) => count === beforeRoundtripCounts[index]) &&
|
|
822
|
+
JSON.stringify(setup.savedLngLat) === savedLngLatSnapshot &&
|
|
823
|
+
text('background-lnglat-readout').includes('回灌完成') && text('background-lnglat-readout').includes('与保存前一致'),
|
|
824
|
+
JSON.stringify({ before: beforeRoundtripCounts, after: restoredCounts, screen: [beforeRoundtripScreen, restoredScreen], readout: text('background-lnglat-readout') }))
|
|
785
825
|
click('background-add-image')
|
|
786
826
|
await wait(450)
|
|
787
827
|
const imageAfterRotate = view.getOptions().background?.image
|
|
@@ -828,11 +868,16 @@
|
|
|
828
868
|
const minPointsSelect = document.querySelector('[data-demo=cluster-min-points]')
|
|
829
869
|
const maxZoomSelect = document.querySelector('[data-demo=cluster-max-zoom]')
|
|
830
870
|
const countSelect = document.querySelector('[data-demo=cluster-count]')
|
|
871
|
+
const colorInput = document.querySelector('[data-demo=cluster-color]')
|
|
872
|
+
const textColorInput = document.querySelector('[data-demo=cluster-text-color]')
|
|
873
|
+
const iconSelect = document.querySelector('[data-demo=cluster-icon]')
|
|
874
|
+
const sizeScaleInput = document.querySelector('[data-demo=cluster-size-scale]')
|
|
875
|
+
const showCountInput = document.querySelector('[data-demo=cluster-show-count]')
|
|
831
876
|
const anchor = await until(() => {
|
|
832
877
|
const node = document.querySelector('[data-demo=cluster-first-client]')
|
|
833
878
|
return node?.dataset.clientX !== '' && node?.dataset.clientY !== '' ? node : null
|
|
834
879
|
}, 8000)
|
|
835
|
-
if (ready && view && canvas && toggle && radiusInput && minPointsSelect && maxZoomSelect && countSelect) {
|
|
880
|
+
if (ready && view && canvas && toggle && radiusInput && minPointsSelect && maxZoomSelect && countSelect && colorInput && textColorInput && iconSelect && sizeScaleInput && showCountInput) {
|
|
836
881
|
const initialClusters = view.getClusters().filter(item => item.count > 1)
|
|
837
882
|
check('点聚合:配置控件齐全,初始数据已聚合并给出读数',
|
|
838
883
|
initialClusters.length > 0 && text('cluster-stats').includes('原始点') && text('cluster-stats').includes('个聚合圆'),
|
|
@@ -848,6 +893,25 @@
|
|
|
848
893
|
check('点聚合:点中聚合圆只增加 clusterClick,普通 click 为 0、选中集不变',
|
|
849
894
|
text('cluster-click-count').includes('0 次普通点击 / 1 次聚合点击') && text('cluster-event').includes('clusterClick') && selectedBefore === selectedAfter,
|
|
850
895
|
`${text('cluster-click-count')} / ${text('cluster-event')} / 选中 ${selectedBefore || '空'} → ${selectedAfter || '空'}`)
|
|
896
|
+
colorInput.value = '#f05a3c'
|
|
897
|
+
colorInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
898
|
+
colorInput.dispatchEvent(new Event('change', { bubbles: true }))
|
|
899
|
+
textColorInput.value = '#ffe08a'
|
|
900
|
+
textColorInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
901
|
+
textColorInput.dispatchEvent(new Event('change', { bubbles: true }))
|
|
902
|
+
iconSelect.value = 'star'
|
|
903
|
+
iconSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
904
|
+
sizeScaleInput.value = '1.4'
|
|
905
|
+
sizeScaleInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
906
|
+
sizeScaleInput.dispatchEvent(new Event('change', { bubbles: true }))
|
|
907
|
+
showCountInput.checked = false
|
|
908
|
+
showCountInput.dispatchEvent(new Event('change', { bubbles: true }))
|
|
909
|
+
await wait(360)
|
|
910
|
+
const styled = view.getPointCluster()
|
|
911
|
+
const styledSceneCluster = view.snapshotScene({ background: false })?.clusters?.[0]
|
|
912
|
+
check('点聚合:颜色、文字色、图标、尺寸与数量显隐控件都落到展示配置',
|
|
913
|
+
styled?.color === '#f05a3c' && styled?.textColor === '#ffe08a' && styled?.symbol?.type === 'svg' && styled?.sizeScale === 1.4 && styled?.showCount === false && styledSceneCluster?.icon?.kind === 'svg' && view.el.querySelectorAll('.gis-view__cluster-label').length === 0,
|
|
914
|
+
JSON.stringify({ color: styled?.color, text: styled?.textColor, symbol: styled?.symbol?.type, scale: styled?.sizeScale, showCount: styled?.showCount, icon: styledSceneCluster?.icon?.kind, labels: view.el.querySelectorAll('.gis-view__cluster-label').length }))
|
|
851
915
|
maxZoomSelect.value = '2'
|
|
852
916
|
maxZoomSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
853
917
|
await wait(220)
|
|
@@ -882,7 +946,7 @@
|
|
|
882
946
|
JSON.stringify({ cluster: view.getPointCluster(), stats: { count: view.getStats().clusterCount, points: view.getStats().clusterPointCount } }))
|
|
883
947
|
}
|
|
884
948
|
else {
|
|
885
|
-
check('
|
|
949
|
+
check('点聚合:演示页、实例与配置控件都起来了', false, ready ? '缺少 demo / setupState.view / 控件' : '没有 canvas')
|
|
886
950
|
}
|
|
887
951
|
}
|
|
888
952
|
|
|
@@ -1436,20 +1500,20 @@
|
|
|
1436
1500
|
}
|
|
1437
1501
|
|
|
1438
1502
|
// 2.0a 搜索与筛选:limit / scope / fields / 异步 Worker / setFilter 与可见数据
|
|
1439
|
-
if (await clickTab('搜索与筛选')) {
|
|
1440
|
-
const ready = await canvasReady()
|
|
1441
|
-
await wait(220)
|
|
1442
|
-
const demo = document.querySelector('.dv-demo')
|
|
1443
|
-
const setup = demo?.__vueParentComponent?.setupState
|
|
1444
|
-
const view = await until(() => setup?.view, 8000)
|
|
1445
|
-
const click = key => document.querySelector(`[data-demo='${key}']`)?.click()
|
|
1446
|
-
const readout = key => document.querySelector(`[data-demo='${key}']`)?.textContent.trim() ?? ''
|
|
1447
|
-
if (ready && view) {
|
|
1448
|
-
const initialReady = await until(() => readout('search-filter-summary').includes('命中'), 4000)
|
|
1449
|
-
check('搜索与筛选:默认 search 命中 3 个节点并因 limit=3 截断', Boolean(initialReady) && readout('search-filter-summary').includes('节点 3') && readout('search-filter-meta').includes('limit=3') && readout('search-filter-meta').includes('truncated=是'), `${readout('search-filter-summary')} / ${readout('search-filter-meta')}`)
|
|
1450
|
-
click('search-filter-preset-owner')
|
|
1451
|
-
const ownerReady = await until(() => readout('search-filter-summary').includes('节点 8'), 4000)
|
|
1452
|
-
check('搜索与筛选:fields=[props.owner] 能命中嵌套业务字段', Boolean(ownerReady) && readout('search-filter-meta').includes('truncated=否'), `${readout('search-filter-summary')} / ${readout('search-filter-meta')}`)
|
|
1503
|
+
if (await clickTab('搜索与筛选')) {
|
|
1504
|
+
const ready = await canvasReady()
|
|
1505
|
+
await wait(220)
|
|
1506
|
+
const demo = document.querySelector('.dv-demo')
|
|
1507
|
+
const setup = demo?.__vueParentComponent?.setupState
|
|
1508
|
+
const view = await until(() => setup?.view, 8000)
|
|
1509
|
+
const click = key => document.querySelector(`[data-demo='${key}']`)?.click()
|
|
1510
|
+
const readout = key => document.querySelector(`[data-demo='${key}']`)?.textContent.trim() ?? ''
|
|
1511
|
+
if (ready && view) {
|
|
1512
|
+
const initialReady = await until(() => readout('search-filter-summary').includes('命中'), 4000)
|
|
1513
|
+
check('搜索与筛选:默认 search 命中 3 个节点并因 limit=3 截断', Boolean(initialReady) && readout('search-filter-summary').includes('节点 3') && readout('search-filter-meta').includes('limit=3') && readout('search-filter-meta').includes('truncated=是'), `${readout('search-filter-summary')} / ${readout('search-filter-meta')}`)
|
|
1514
|
+
click('search-filter-preset-owner')
|
|
1515
|
+
const ownerReady = await until(() => readout('search-filter-summary').includes('节点 8'), 4000)
|
|
1516
|
+
check('搜索与筛选:fields=[props.owner] 能命中嵌套业务字段', Boolean(ownerReady) && readout('search-filter-meta').includes('truncated=否'), `${readout('search-filter-summary')} / ${readout('search-filter-meta')}`)
|
|
1453
1517
|
click('search-filter-preset-channel')
|
|
1454
1518
|
const channelReady = await until(() => readout('search-filter-summary').includes('通道 1'), 4000)
|
|
1455
1519
|
check('搜索与筛选:scope=channels 只把通道放进命中', Boolean(channelReady) && readout('search-filter-summary').includes('节点 0') && readout('search-filter-summary').includes('线路 0'), readout('search-filter-summary'))
|
|
@@ -1525,56 +1589,56 @@
|
|
|
1525
1589
|
Boolean(dataVersionText) && view.dataVersion > dataVersionBefore,
|
|
1526
1590
|
`${dataVersionBefore} -> ${view.dataVersion} / ${dataVersionText}`)
|
|
1527
1591
|
click('search-filter-preset-symbol')
|
|
1528
|
-
const symbolReady = await until(() => readout('search-filter-summary').includes('节点 8'), 4000)
|
|
1529
|
-
check('搜索与筛选:显式 fields=[symbol] 能搜默认跳过的符号字段', Boolean(symbolReady) && readout('search-filter-hit-0').includes('symbol = square'), `${readout('search-filter-summary')} / ${readout('search-filter-hit-0')}`)
|
|
1530
|
-
document.querySelector('[data-demo=search-filter-worker]')?.click()
|
|
1531
|
-
click('search-filter-run-async')
|
|
1532
|
-
const asyncReady = await until(() => readout('search-filter-meta').includes('searchAsync'), 6000)
|
|
1533
|
-
check('搜索与筛选:searchAsync 低阈值走 Worker,结果与同步搜索一致', Boolean(asyncReady) && readout('search-filter-meta').includes('worker=是') && readout('search-filter-summary').includes('节点 8'), readout('search-filter-meta'))
|
|
1534
|
-
click('search-filter-filter-voltage')
|
|
1535
|
-
const filterReady = await until(() => readout('search-filter-filter').includes('可见 12 节点'), 4000)
|
|
1536
|
-
check('搜索与筛选:setFilter 隐藏一半节点,getVisibleData 同步可见集合', Boolean(filterReady) && view.getVisibleData().points.length === 12 && view.getData().points.length === 24, `${readout('search-filter-filter')} / ${readout('search-filter-visible')}`)
|
|
1537
|
-
click('search-filter-preset-symbol')
|
|
1538
|
-
const stillSearches = await until(() => readout('search-filter-summary').includes('节点 8'), 4000)
|
|
1539
|
-
check('搜索与筛选:筛选只影响显示,search 仍按全部 24 个节点计算', Boolean(stillSearches) && view.getVisibleData().points.length === 12, `${readout('search-filter-summary')} / 可见 ${view.getVisibleData().points.length}`)
|
|
1540
|
-
click('search-filter-filter-clear')
|
|
1541
|
-
const cleared = await until(() => view.getVisibleData().points.length === 24 && readout('search-filter-visible').includes('24 节点'), 4000)
|
|
1592
|
+
const symbolReady = await until(() => readout('search-filter-summary').includes('节点 8'), 4000)
|
|
1593
|
+
check('搜索与筛选:显式 fields=[symbol] 能搜默认跳过的符号字段', Boolean(symbolReady) && readout('search-filter-hit-0').includes('symbol = square'), `${readout('search-filter-summary')} / ${readout('search-filter-hit-0')}`)
|
|
1594
|
+
document.querySelector('[data-demo=search-filter-worker]')?.click()
|
|
1595
|
+
click('search-filter-run-async')
|
|
1596
|
+
const asyncReady = await until(() => readout('search-filter-meta').includes('searchAsync'), 6000)
|
|
1597
|
+
check('搜索与筛选:searchAsync 低阈值走 Worker,结果与同步搜索一致', Boolean(asyncReady) && readout('search-filter-meta').includes('worker=是') && readout('search-filter-summary').includes('节点 8'), readout('search-filter-meta'))
|
|
1598
|
+
click('search-filter-filter-voltage')
|
|
1599
|
+
const filterReady = await until(() => readout('search-filter-filter').includes('可见 12 节点'), 4000)
|
|
1600
|
+
check('搜索与筛选:setFilter 隐藏一半节点,getVisibleData 同步可见集合', Boolean(filterReady) && view.getVisibleData().points.length === 12 && view.getData().points.length === 24, `${readout('search-filter-filter')} / ${readout('search-filter-visible')}`)
|
|
1601
|
+
click('search-filter-preset-symbol')
|
|
1602
|
+
const stillSearches = await until(() => readout('search-filter-summary').includes('节点 8'), 4000)
|
|
1603
|
+
check('搜索与筛选:筛选只影响显示,search 仍按全部 24 个节点计算', Boolean(stillSearches) && view.getVisibleData().points.length === 12, `${readout('search-filter-summary')} / 可见 ${view.getVisibleData().points.length}`)
|
|
1604
|
+
click('search-filter-filter-clear')
|
|
1605
|
+
const cleared = await until(() => view.getVisibleData().points.length === 24 && readout('search-filter-visible').includes('24 节点'), 4000)
|
|
1542
1606
|
check('搜索与筛选:setFilter(null) 后 getVisibleData 恢复全量', Boolean(cleared), readout('search-filter-visible'))
|
|
1543
|
-
}
|
|
1544
|
-
else {
|
|
1545
|
-
check('搜索与筛选:画布与实例都起来了', false, ready ? '没有 setupState.view' : '没有 canvas')
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
// 2.0a 批量新增:五个入口各点一次,数据条数与方法返回都要变
|
|
1550
|
-
if (await clickTab('批量新增')) {
|
|
1551
|
-
const ready = await canvasReady()
|
|
1552
|
-
await wait(220)
|
|
1553
|
-
const demo = document.querySelector('.dv-demo')
|
|
1554
|
-
const setup = demo?.__vueParentComponent?.setupState
|
|
1555
|
-
const view = await until(() => setup?.view, 8000)
|
|
1556
|
-
const click = key => document.querySelector(`[data-demo="${key}"]`)?.click()
|
|
1557
|
-
const text = key => document.querySelector(`[data-demo="${key}"]`)?.textContent.trim() ?? ''
|
|
1558
|
-
if (ready && view) {
|
|
1607
|
+
}
|
|
1608
|
+
else {
|
|
1609
|
+
check('搜索与筛选:画布与实例都起来了', false, ready ? '没有 setupState.view' : '没有 canvas')
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// 2.0a 批量新增:五个入口各点一次,数据条数与方法返回都要变
|
|
1614
|
+
if (await clickTab('批量新增')) {
|
|
1615
|
+
const ready = await canvasReady()
|
|
1616
|
+
await wait(220)
|
|
1617
|
+
const demo = document.querySelector('.dv-demo')
|
|
1618
|
+
const setup = demo?.__vueParentComponent?.setupState
|
|
1619
|
+
const view = await until(() => setup?.view, 8000)
|
|
1620
|
+
const click = key => document.querySelector(`[data-demo="${key}"]`)?.click()
|
|
1621
|
+
const text = key => document.querySelector(`[data-demo="${key}"]`)?.textContent.trim() ?? ''
|
|
1622
|
+
if (ready && view) {
|
|
1559
1623
|
const groupsOf = () => (view.getOptions().channels ?? []).reduce((sum, channel) => sum + view.getChannelGroups(channel.id).length, 0)
|
|
1560
|
-
const lineIndex = view.getData().lines.findIndex(line => String(line.id) === 'line-0')
|
|
1561
|
-
const waypointCount = () => view.getWaypoints(lineIndex).length
|
|
1562
|
-
const pointsBefore = view.getData().points.length
|
|
1563
|
-
click('batch-add-points')
|
|
1564
|
-
const pointsAfter = await until(() => view.getData().points.length === pointsBefore + 4, 6000)
|
|
1565
|
-
check('批量新增:addPoints 一次补 4 个节点', Boolean(pointsAfter) && text('batch-add-readout').includes('addPoints'), `${pointsBefore} → ${view.getData().points.length} / ${text('batch-add-readout')}`)
|
|
1566
|
-
const linesBefore = view.getData().lines.length
|
|
1567
|
-
click('batch-add-lines')
|
|
1568
|
-
const linesAfter = await until(() => view.getData().lines.length === linesBefore + 4, 6000)
|
|
1569
|
-
check('批量新增:addLines 一次补 4 条线路', Boolean(linesAfter) && text('batch-add-readout').includes('addLines'), `${linesBefore} → ${view.getData().lines.length}`)
|
|
1570
|
-
const channelsBefore = view.getOptions().channels?.length ?? 0
|
|
1571
|
-
click('batch-add-channels')
|
|
1572
|
-
const channelsAfter = await until(() => (view.getOptions().channels?.length ?? 0) === channelsBefore + 2, 6000)
|
|
1573
|
-
check('批量新增:addChannels 一次补 2 条通道', Boolean(channelsAfter) && text('batch-add-readout').includes('addChannels'), `${channelsBefore} → ${view.getOptions().channels?.length ?? 0}`)
|
|
1574
|
-
const groupsBefore = groupsOf()
|
|
1575
|
-
click('batch-add-channel-groups')
|
|
1576
|
-
const groupsAfter = await until(() => groupsOf() === groupsBefore + 2, 6000)
|
|
1577
|
-
check('批量新增:addChannelGroups 一次补 2 组、4 条回线', Boolean(groupsAfter) && text('batch-add-readout').includes('2 组 / 4 条回线'), `${groupsBefore} → ${groupsOf()} / ${text('batch-add-readout')}`)
|
|
1624
|
+
const lineIndex = view.getData().lines.findIndex(line => String(line.id) === 'line-0')
|
|
1625
|
+
const waypointCount = () => view.getWaypoints(lineIndex).length
|
|
1626
|
+
const pointsBefore = view.getData().points.length
|
|
1627
|
+
click('batch-add-points')
|
|
1628
|
+
const pointsAfter = await until(() => view.getData().points.length === pointsBefore + 4, 6000)
|
|
1629
|
+
check('批量新增:addPoints 一次补 4 个节点', Boolean(pointsAfter) && text('batch-add-readout').includes('addPoints'), `${pointsBefore} → ${view.getData().points.length} / ${text('batch-add-readout')}`)
|
|
1630
|
+
const linesBefore = view.getData().lines.length
|
|
1631
|
+
click('batch-add-lines')
|
|
1632
|
+
const linesAfter = await until(() => view.getData().lines.length === linesBefore + 4, 6000)
|
|
1633
|
+
check('批量新增:addLines 一次补 4 条线路', Boolean(linesAfter) && text('batch-add-readout').includes('addLines'), `${linesBefore} → ${view.getData().lines.length}`)
|
|
1634
|
+
const channelsBefore = view.getOptions().channels?.length ?? 0
|
|
1635
|
+
click('batch-add-channels')
|
|
1636
|
+
const channelsAfter = await until(() => (view.getOptions().channels?.length ?? 0) === channelsBefore + 2, 6000)
|
|
1637
|
+
check('批量新增:addChannels 一次补 2 条通道', Boolean(channelsAfter) && text('batch-add-readout').includes('addChannels'), `${channelsBefore} → ${view.getOptions().channels?.length ?? 0}`)
|
|
1638
|
+
const groupsBefore = groupsOf()
|
|
1639
|
+
click('batch-add-channel-groups')
|
|
1640
|
+
const groupsAfter = await until(() => groupsOf() === groupsBefore + 2, 6000)
|
|
1641
|
+
check('批量新增:addChannelGroups 一次补 2 组、4 条回线', Boolean(groupsAfter) && text('batch-add-readout').includes('2 组 / 4 条回线'), `${groupsBefore} → ${groupsOf()} / ${text('batch-add-readout')}`)
|
|
1578
1642
|
const waypointsBefore = waypointCount()
|
|
1579
1643
|
click('batch-add-waypoints')
|
|
1580
1644
|
const waypointsAfter = await until(() => waypointCount() > waypointsBefore, 6000)
|
|
@@ -1618,116 +1682,116 @@
|
|
|
1618
1682
|
const batchGroupRemoved = await until(() => view.getChannels().length < groupsBeforeBatchRemove && text('batch-add-readout').includes('batch(removeChannelGroup)'), 6000)
|
|
1619
1683
|
check('批量新增:batch 内逐条 removeChannelGroup,回线并回相邻组且数据不丢', Boolean(batchGroupRemoved) && text('batch-add-readout').includes('回线数据未丢'), `${groupsBeforeBatchRemove} → ${view.getChannels().length} · ${text('batch-add-readout')}`)
|
|
1620
1684
|
}
|
|
1621
|
-
else {
|
|
1622
|
-
check('批量新增:画布与实例都起来了', false, ready ? '没有 setupState.view' : '没有 canvas')
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
// 2.0b 批量复制与合并:整组复制带回来源、线路与通道,撤销后能恢复;指定目标合并能改写最终节点
|
|
1627
|
-
if (await clickTab('批量复制与合并')) {
|
|
1628
|
-
const ready = await canvasReady()
|
|
1629
|
-
await wait(220)
|
|
1630
|
-
const demo = document.querySelector('.dv-demo')
|
|
1631
|
-
const setup = demo?.__vueParentComponent?.setupState
|
|
1632
|
-
const view = await until(() => setup?.view, 8000)
|
|
1633
|
-
const click = key => document.querySelector(`[data-demo="${key}"]`)?.click()
|
|
1634
|
-
const text = key => document.querySelector(`[data-demo="${key}"]`)?.textContent.trim() ?? ''
|
|
1635
|
-
const unwrap = value => value?.value ?? value
|
|
1636
|
-
if (ready && view) {
|
|
1637
|
-
click('copy-select-sources')
|
|
1638
|
-
const selected = await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
1639
|
-
check('批量复制与合并:框选来源按钮选中 4 个通道端点', Boolean(selected) && text('copy-merge-selection').includes('4 节点'), text('copy-merge-selection'))
|
|
1640
|
-
const features = view.getFeatures()
|
|
1641
|
-
check('批量复制与合并:框选 / 拖动 / 键盘 / 复制 / 合并开关都已打开', features.boxSelect && features.drag && features.keyboard && features.copy && features.merge, JSON.stringify({ boxSelect: features.boxSelect, drag: features.drag, keyboard: features.keyboard, copy: features.copy, merge: features.merge }))
|
|
1642
|
-
const canvasRect = document.querySelector('.gis-view__canvas').getBoundingClientRect()
|
|
1643
|
-
const allBox = { x: 0, y: 0, width: canvasRect.width, height: canvasRect.height }
|
|
1644
|
-
view.updateOptions({ selectionKinds: ['point'] })
|
|
1645
|
-
view.selectInBox(allBox)
|
|
1646
|
-
const onlyPoints = view.getSelectionData()
|
|
1647
|
-
check('批量复制与合并:框选类型只开 point 时不含线路 / 通道', onlyPoints.pointCount > 0 && onlyPoints.lineCount === 0 && onlyPoints.channelCount === 0, JSON.stringify({ points: onlyPoints.pointCount, lines: onlyPoints.lineCount, channels: onlyPoints.channelCount }))
|
|
1648
|
-
view.updateOptions({ selectionKinds: ['line'] })
|
|
1649
|
-
view.selectInBox(allBox)
|
|
1650
|
-
const onlyLines = view.getSelectionData()
|
|
1651
|
-
check('批量复制与合并:框选类型只开 line 时只有通道外线路', onlyLines.pointCount === 0 && onlyLines.lineCount > 0 && onlyLines.channelCount === 0, JSON.stringify({ points: onlyLines.pointCount, lines: onlyLines.lineCount, channels: onlyLines.channelCount }))
|
|
1652
|
-
view.updateOptions({ selectionKinds: ['channel'] })
|
|
1653
|
-
view.selectInBox(allBox)
|
|
1654
|
-
const onlyChannels = view.getSelectionData()
|
|
1655
|
-
check('批量复制与合并:框选类型只开 channel 时只有通道回线', onlyChannels.pointCount === 0 && onlyChannels.lineCount > 0 && onlyChannels.channelCount > 0, JSON.stringify({ points: onlyChannels.pointCount, lines: onlyChannels.lineCount, channels: onlyChannels.channelCount }))
|
|
1656
|
-
view.updateOptions({ selectionKinds: ['point', 'line', 'channel'] })
|
|
1657
|
-
click('copy-select-sources')
|
|
1658
|
-
await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
1659
|
-
const pointsBeforeCopy = view.getData().points.length
|
|
1660
|
-
click('copy-selected')
|
|
1661
|
-
const copied = await until(() => view.getData().points.length >= pointsBeforeCopy + 4, 6000)
|
|
1662
|
-
const copyPayload = unwrap(setup.lastBatch)
|
|
1663
|
-
check('批量复制与合并:只选节点时 copySelectedPoints 只复制节点', Boolean(copied) && copyPayload?.sources?.length === 4 && copyPayload?.lines?.length === 0 && copyPayload?.channels?.length === 0, `${pointsBeforeCopy} → ${view.getData().points.length} / sources=${copyPayload?.sources?.length} lines=${copyPayload?.lines?.length} channels=${copyPayload?.channels?.length}`)
|
|
1664
|
-
check('批量复制与合并:复制读数能从事件载荷看到来源与落地数据', text('copy-readout').includes('copySelectedPoints') && text('copy-readout').includes('来源') && text('copy-readout').includes('线路') && text('copy-readout').includes('通道'), text('copy-readout'))
|
|
1665
|
-
click('copy-revert')
|
|
1666
|
-
const reverted = await until(() => view.getData().points.length === pointsBeforeCopy, 6000)
|
|
1667
|
-
check('批量复制与合并:revert / cancel 一次收回整组复制', Boolean(reverted) && text('copy-action').includes('已撤销整组复制'), `${pointsBeforeCopy} / ${view.getData().points.length} · ${text('copy-action')}`)
|
|
1668
|
-
click('select-sample-channel')
|
|
1669
|
-
const channelSelected = await until(() => view.getSelectionData().channelCount === 1 && view.getSelectionData().lineCount === 3, 5000)
|
|
1670
|
-
const pointsBeforeChannelCopy = view.getData().points.length
|
|
1671
|
-
click('copy-selected')
|
|
1672
|
-
const channelCopied = await until(() => view.getData().points.length === pointsBeforeChannelCopy + 2, 6000)
|
|
1673
|
-
const channelCopyPayload = unwrap(setup.lastBatch)
|
|
1674
|
-
check('批量复制与合并:只选通道时复制补端点并同时带上回线 / 通道', Boolean(channelSelected) && Boolean(channelCopied) && channelCopyPayload?.supportSources?.length === 2 && channelCopyPayload?.lines?.length === 3 && channelCopyPayload?.channels?.length === 1, `${pointsBeforeChannelCopy} → ${view.getData().points.length} / support=${channelCopyPayload?.supportSources?.length} lines=${channelCopyPayload?.lines?.length} channels=${channelCopyPayload?.channels?.length}`)
|
|
1675
|
-
const channelCopyLinks = channelCopyPayload?.lineChannels ?? []
|
|
1676
|
-
check('批量复制与合并:lineChannels 与复制出来的回线一一对齐,并指向新通道 / 新分组',
|
|
1677
|
-
channelCopyLinks.length === channelCopyPayload?.lines?.length &&
|
|
1678
|
-
channelCopyLinks.every((slot, index) => slot &&
|
|
1679
|
-
slot.lineIndex === channelCopyPayload.lineIndexes[index] &&
|
|
1680
|
-
slot.lineId === channelCopyPayload.lines[index].id &&
|
|
1681
|
-
slot.channelIndex === channelCopyPayload.channelIndexes[0] &&
|
|
1682
|
-
slot.channelId === channelCopyPayload.channels[0].id &&
|
|
1683
|
-
Array.isArray(slot.group?.lineIds) && slot.group.lineIds.includes(slot.lineId)) &&
|
|
1684
|
-
text('copy-line-channels-readout').includes('→'),
|
|
1685
|
-
`${text('copy-line-channels-readout')} / ${JSON.stringify(channelCopyLinks.map(slot => ({ line: slot?.lineIndex, channel: slot?.channelIndex, group: slot?.groupId })))}`)
|
|
1686
|
-
click('copy-revert')
|
|
1687
|
-
await until(() => view.getData().points.length === pointsBeforeChannelCopy, 6000)
|
|
1688
|
-
click('copy-select-sources')
|
|
1689
|
-
await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
1690
|
-
click('copy-clipboard')
|
|
1691
|
-
const clipboardReady = await until(() => text('clipboard-readout').includes('count=4'), 5000)
|
|
1692
|
-
const clipboard = unwrap(setup.clipboardItem)
|
|
1693
|
-
check('批量复制与合并:copyPointsToClipboard 留下整组来源快照', Boolean(clipboardReady) && clipboard?.sources?.length === 4 && clipboard?.count === 4, text('clipboard-readout'))
|
|
1694
|
-
const pointsBeforePaste = view.getData().points.length
|
|
1695
|
-
click('paste-points')
|
|
1696
|
-
const pasted = await until(() => view.getData().points.length >= pointsBeforePaste + 4, 6000)
|
|
1697
|
-
check('批量复制与合并:pastePoints 按整组锚点落回画布', Boolean(pasted) && text('copy-readout').includes('pastePoints'), `${pointsBeforePaste} → ${view.getData().points.length} / ${text('copy-readout')}`)
|
|
1698
|
-
click('copy-revert')
|
|
1699
|
-
await until(() => view.getData().points.length === pointsBeforePaste, 6000)
|
|
1700
|
-
click('merge-select-sources')
|
|
1701
|
-
await wait(120)
|
|
1702
|
-
const targetOptionCount = document.querySelectorAll('[data-demo="merge-target"] option').length
|
|
1703
|
-
check('批量复制与合并:保留目标下拉只列当前选中节点', targetOptionCount === 4, `${targetOptionCount} 个选项`)
|
|
1704
|
-
const targetId = String(setup.mergeTargetId ?? '')
|
|
1705
|
-
const pointsBeforeMerge = view.getData().points.length
|
|
1706
|
-
click('merge-selected')
|
|
1707
|
-
const mergedCount = await until(() => view.getData().points.length === pointsBeforeMerge - 3, 6000)
|
|
1708
|
-
const target = view.getData().points.find(point => String(point.id) === targetId)
|
|
1709
|
-
check('批量复制与合并:mergeSelectedPoints 指定保留目标并并掉 3 个来源', Boolean(mergedCount) && target?.demo_merged === true && target?.zone_name === '合并后(item 改写)', `${pointsBeforeMerge} → ${view.getData().points.length} / 目标 ${targetId} / ${target?.name ?? '—'}`)
|
|
1710
|
-
check('批量复制与合并:合并事件读数带来源、目标与最终节点', text('merge-readout').includes('mergeSelectedPoints') && text('merge-readout').includes('保留') && text('merge-readout').includes('并掉') && text('merge-readout').includes('最终'), text('merge-readout'))
|
|
1711
|
-
check('批量复制与合并:merge 载荷的 changes 生成可直接提交的后端事务读数', text('merge-backend-readout').includes('后端事务') && text('merge-backend-readout').includes('删节点') && text('merge-backend-readout').includes('更新线') && text('merge-backend-readout').includes('更新通道'), text('merge-backend-readout'))
|
|
1712
|
-
const mergeTransactionReady = await until(() => {
|
|
1713
|
-
const preview = text('copy-merge-backend-request')
|
|
1714
|
-
return preview.includes('POST /gis/merges') && preview.includes('200 OK')
|
|
1715
|
-
}, 8000)
|
|
1716
|
-
const mergeRequestPreview = text('copy-merge-backend-request')
|
|
1717
|
-
check('批量复制与合并:mergeSelectedPoints 收成一条 /gis/merges 事务请求',
|
|
1718
|
-
Boolean(mergeTransactionReady) &&
|
|
1719
|
-
mergeRequestPreview.includes('POST /gis/merges') &&
|
|
1720
|
-
mergeRequestPreview.includes('"changes"') &&
|
|
1721
|
-
mergeRequestPreview.includes('"impact"') &&
|
|
1722
|
-
mergeRequestPreview.includes('"baseVersion"') &&
|
|
1723
|
-
mergeRequestPreview.includes('"opId"') &&
|
|
1724
|
-
mergeRequestPreview.includes('200 OK'),
|
|
1725
|
-
mergeRequestPreview.slice(0, 300).replace(/\s+/g, ' '))
|
|
1726
|
-
check('批量复制与合并:合并成功响应回标准响应包与事务 id 集合',
|
|
1727
|
-
['"code": "OK"', '"operation": "merge.transaction"', '"transactionId"', '"retainedPointIds"', '"removedPointIds"', '"removedLineIds"', '"removedChannelIds"', 'Idempotency-Key:', 'If-Match: "v'].every(key => mergeRequestPreview.includes(key)),
|
|
1728
|
-
mergeRequestPreview.slice(-320).replace(/\s+/g, ' '))
|
|
1729
|
-
const confirmButton = document.querySelector('[data-demo="merge-confirm"]')
|
|
1730
|
-
const cancelButton = document.querySelector('[data-demo="merge-cancel"]')
|
|
1685
|
+
else {
|
|
1686
|
+
check('批量新增:画布与实例都起来了', false, ready ? '没有 setupState.view' : '没有 canvas')
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// 2.0b 批量复制与合并:整组复制带回来源、线路与通道,撤销后能恢复;指定目标合并能改写最终节点
|
|
1691
|
+
if (await clickTab('批量复制与合并')) {
|
|
1692
|
+
const ready = await canvasReady()
|
|
1693
|
+
await wait(220)
|
|
1694
|
+
const demo = document.querySelector('.dv-demo')
|
|
1695
|
+
const setup = demo?.__vueParentComponent?.setupState
|
|
1696
|
+
const view = await until(() => setup?.view, 8000)
|
|
1697
|
+
const click = key => document.querySelector(`[data-demo="${key}"]`)?.click()
|
|
1698
|
+
const text = key => document.querySelector(`[data-demo="${key}"]`)?.textContent.trim() ?? ''
|
|
1699
|
+
const unwrap = value => value?.value ?? value
|
|
1700
|
+
if (ready && view) {
|
|
1701
|
+
click('copy-select-sources')
|
|
1702
|
+
const selected = await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
1703
|
+
check('批量复制与合并:框选来源按钮选中 4 个通道端点', Boolean(selected) && text('copy-merge-selection').includes('4 节点'), text('copy-merge-selection'))
|
|
1704
|
+
const features = view.getFeatures()
|
|
1705
|
+
check('批量复制与合并:框选 / 拖动 / 键盘 / 复制 / 合并开关都已打开', features.boxSelect && features.drag && features.keyboard && features.copy && features.merge, JSON.stringify({ boxSelect: features.boxSelect, drag: features.drag, keyboard: features.keyboard, copy: features.copy, merge: features.merge }))
|
|
1706
|
+
const canvasRect = document.querySelector('.gis-view__canvas').getBoundingClientRect()
|
|
1707
|
+
const allBox = { x: 0, y: 0, width: canvasRect.width, height: canvasRect.height }
|
|
1708
|
+
view.updateOptions({ selectionKinds: ['point'] })
|
|
1709
|
+
view.selectInBox(allBox)
|
|
1710
|
+
const onlyPoints = view.getSelectionData()
|
|
1711
|
+
check('批量复制与合并:框选类型只开 point 时不含线路 / 通道', onlyPoints.pointCount > 0 && onlyPoints.lineCount === 0 && onlyPoints.channelCount === 0, JSON.stringify({ points: onlyPoints.pointCount, lines: onlyPoints.lineCount, channels: onlyPoints.channelCount }))
|
|
1712
|
+
view.updateOptions({ selectionKinds: ['line'] })
|
|
1713
|
+
view.selectInBox(allBox)
|
|
1714
|
+
const onlyLines = view.getSelectionData()
|
|
1715
|
+
check('批量复制与合并:框选类型只开 line 时只有通道外线路', onlyLines.pointCount === 0 && onlyLines.lineCount > 0 && onlyLines.channelCount === 0, JSON.stringify({ points: onlyLines.pointCount, lines: onlyLines.lineCount, channels: onlyLines.channelCount }))
|
|
1716
|
+
view.updateOptions({ selectionKinds: ['channel'] })
|
|
1717
|
+
view.selectInBox(allBox)
|
|
1718
|
+
const onlyChannels = view.getSelectionData()
|
|
1719
|
+
check('批量复制与合并:框选类型只开 channel 时只有通道回线', onlyChannels.pointCount === 0 && onlyChannels.lineCount > 0 && onlyChannels.channelCount > 0, JSON.stringify({ points: onlyChannels.pointCount, lines: onlyChannels.lineCount, channels: onlyChannels.channelCount }))
|
|
1720
|
+
view.updateOptions({ selectionKinds: ['point', 'line', 'channel'] })
|
|
1721
|
+
click('copy-select-sources')
|
|
1722
|
+
await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
1723
|
+
const pointsBeforeCopy = view.getData().points.length
|
|
1724
|
+
click('copy-selected')
|
|
1725
|
+
const copied = await until(() => view.getData().points.length >= pointsBeforeCopy + 4, 6000)
|
|
1726
|
+
const copyPayload = unwrap(setup.lastBatch)
|
|
1727
|
+
check('批量复制与合并:只选节点时 copySelectedPoints 只复制节点', Boolean(copied) && copyPayload?.sources?.length === 4 && copyPayload?.lines?.length === 0 && copyPayload?.channels?.length === 0, `${pointsBeforeCopy} → ${view.getData().points.length} / sources=${copyPayload?.sources?.length} lines=${copyPayload?.lines?.length} channels=${copyPayload?.channels?.length}`)
|
|
1728
|
+
check('批量复制与合并:复制读数能从事件载荷看到来源与落地数据', text('copy-readout').includes('copySelectedPoints') && text('copy-readout').includes('来源') && text('copy-readout').includes('线路') && text('copy-readout').includes('通道'), text('copy-readout'))
|
|
1729
|
+
click('copy-revert')
|
|
1730
|
+
const reverted = await until(() => view.getData().points.length === pointsBeforeCopy, 6000)
|
|
1731
|
+
check('批量复制与合并:revert / cancel 一次收回整组复制', Boolean(reverted) && text('copy-action').includes('已撤销整组复制'), `${pointsBeforeCopy} / ${view.getData().points.length} · ${text('copy-action')}`)
|
|
1732
|
+
click('select-sample-channel')
|
|
1733
|
+
const channelSelected = await until(() => view.getSelectionData().channelCount === 1 && view.getSelectionData().lineCount === 3, 5000)
|
|
1734
|
+
const pointsBeforeChannelCopy = view.getData().points.length
|
|
1735
|
+
click('copy-selected')
|
|
1736
|
+
const channelCopied = await until(() => view.getData().points.length === pointsBeforeChannelCopy + 2, 6000)
|
|
1737
|
+
const channelCopyPayload = unwrap(setup.lastBatch)
|
|
1738
|
+
check('批量复制与合并:只选通道时复制补端点并同时带上回线 / 通道', Boolean(channelSelected) && Boolean(channelCopied) && channelCopyPayload?.supportSources?.length === 2 && channelCopyPayload?.lines?.length === 3 && channelCopyPayload?.channels?.length === 1, `${pointsBeforeChannelCopy} → ${view.getData().points.length} / support=${channelCopyPayload?.supportSources?.length} lines=${channelCopyPayload?.lines?.length} channels=${channelCopyPayload?.channels?.length}`)
|
|
1739
|
+
const channelCopyLinks = channelCopyPayload?.lineChannels ?? []
|
|
1740
|
+
check('批量复制与合并:lineChannels 与复制出来的回线一一对齐,并指向新通道 / 新分组',
|
|
1741
|
+
channelCopyLinks.length === channelCopyPayload?.lines?.length &&
|
|
1742
|
+
channelCopyLinks.every((slot, index) => slot &&
|
|
1743
|
+
slot.lineIndex === channelCopyPayload.lineIndexes[index] &&
|
|
1744
|
+
slot.lineId === channelCopyPayload.lines[index].id &&
|
|
1745
|
+
slot.channelIndex === channelCopyPayload.channelIndexes[0] &&
|
|
1746
|
+
slot.channelId === channelCopyPayload.channels[0].id &&
|
|
1747
|
+
Array.isArray(slot.group?.lineIds) && slot.group.lineIds.includes(slot.lineId)) &&
|
|
1748
|
+
text('copy-line-channels-readout').includes('→'),
|
|
1749
|
+
`${text('copy-line-channels-readout')} / ${JSON.stringify(channelCopyLinks.map(slot => ({ line: slot?.lineIndex, channel: slot?.channelIndex, group: slot?.groupId })))}`)
|
|
1750
|
+
click('copy-revert')
|
|
1751
|
+
await until(() => view.getData().points.length === pointsBeforeChannelCopy, 6000)
|
|
1752
|
+
click('copy-select-sources')
|
|
1753
|
+
await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
1754
|
+
click('copy-clipboard')
|
|
1755
|
+
const clipboardReady = await until(() => text('clipboard-readout').includes('count=4'), 5000)
|
|
1756
|
+
const clipboard = unwrap(setup.clipboardItem)
|
|
1757
|
+
check('批量复制与合并:copyPointsToClipboard 留下整组来源快照', Boolean(clipboardReady) && clipboard?.sources?.length === 4 && clipboard?.count === 4, text('clipboard-readout'))
|
|
1758
|
+
const pointsBeforePaste = view.getData().points.length
|
|
1759
|
+
click('paste-points')
|
|
1760
|
+
const pasted = await until(() => view.getData().points.length >= pointsBeforePaste + 4, 6000)
|
|
1761
|
+
check('批量复制与合并:pastePoints 按整组锚点落回画布', Boolean(pasted) && text('copy-readout').includes('pastePoints'), `${pointsBeforePaste} → ${view.getData().points.length} / ${text('copy-readout')}`)
|
|
1762
|
+
click('copy-revert')
|
|
1763
|
+
await until(() => view.getData().points.length === pointsBeforePaste, 6000)
|
|
1764
|
+
click('merge-select-sources')
|
|
1765
|
+
await wait(120)
|
|
1766
|
+
const targetOptionCount = document.querySelectorAll('[data-demo="merge-target"] option').length
|
|
1767
|
+
check('批量复制与合并:保留目标下拉只列当前选中节点', targetOptionCount === 4, `${targetOptionCount} 个选项`)
|
|
1768
|
+
const targetId = String(setup.mergeTargetId ?? '')
|
|
1769
|
+
const pointsBeforeMerge = view.getData().points.length
|
|
1770
|
+
click('merge-selected')
|
|
1771
|
+
const mergedCount = await until(() => view.getData().points.length === pointsBeforeMerge - 3, 6000)
|
|
1772
|
+
const target = view.getData().points.find(point => String(point.id) === targetId)
|
|
1773
|
+
check('批量复制与合并:mergeSelectedPoints 指定保留目标并并掉 3 个来源', Boolean(mergedCount) && target?.demo_merged === true && target?.zone_name === '合并后(item 改写)', `${pointsBeforeMerge} → ${view.getData().points.length} / 目标 ${targetId} / ${target?.name ?? '—'}`)
|
|
1774
|
+
check('批量复制与合并:合并事件读数带来源、目标与最终节点', text('merge-readout').includes('mergeSelectedPoints') && text('merge-readout').includes('保留') && text('merge-readout').includes('并掉') && text('merge-readout').includes('最终'), text('merge-readout'))
|
|
1775
|
+
check('批量复制与合并:merge 载荷的 changes 生成可直接提交的后端事务读数', text('merge-backend-readout').includes('后端事务') && text('merge-backend-readout').includes('删节点') && text('merge-backend-readout').includes('更新线') && text('merge-backend-readout').includes('更新通道'), text('merge-backend-readout'))
|
|
1776
|
+
const mergeTransactionReady = await until(() => {
|
|
1777
|
+
const preview = text('copy-merge-backend-request')
|
|
1778
|
+
return preview.includes('POST /gis/merges') && preview.includes('200 OK')
|
|
1779
|
+
}, 8000)
|
|
1780
|
+
const mergeRequestPreview = text('copy-merge-backend-request')
|
|
1781
|
+
check('批量复制与合并:mergeSelectedPoints 收成一条 /gis/merges 事务请求',
|
|
1782
|
+
Boolean(mergeTransactionReady) &&
|
|
1783
|
+
mergeRequestPreview.includes('POST /gis/merges') &&
|
|
1784
|
+
mergeRequestPreview.includes('"changes"') &&
|
|
1785
|
+
mergeRequestPreview.includes('"impact"') &&
|
|
1786
|
+
mergeRequestPreview.includes('"baseVersion"') &&
|
|
1787
|
+
mergeRequestPreview.includes('"opId"') &&
|
|
1788
|
+
mergeRequestPreview.includes('200 OK'),
|
|
1789
|
+
mergeRequestPreview.slice(0, 300).replace(/\s+/g, ' '))
|
|
1790
|
+
check('批量复制与合并:合并成功响应回标准响应包与事务 id 集合',
|
|
1791
|
+
['"code": "OK"', '"operation": "merge.transaction"', '"transactionId"', '"retainedPointIds"', '"removedPointIds"', '"removedLineIds"', '"removedChannelIds"', 'Idempotency-Key:', 'If-Match: "v'].every(key => mergeRequestPreview.includes(key)),
|
|
1792
|
+
mergeRequestPreview.slice(-320).replace(/\s+/g, ' '))
|
|
1793
|
+
const confirmButton = document.querySelector('[data-demo="merge-confirm"]')
|
|
1794
|
+
const cancelButton = document.querySelector('[data-demo="merge-cancel"]')
|
|
1731
1795
|
check('批量复制与合并:待确认面板给出 resolvePointMerge(true / false) 两个入口', Boolean(confirmButton) && Boolean(cancelButton) && confirmButton.disabled && cancelButton.disabled && confirmButton.textContent.includes('resolvePointMerge(true') && cancelButton.textContent.includes('resolvePointMerge(false'), `${confirmButton?.textContent.trim()} / ${cancelButton?.textContent.trim()}`)
|
|
1732
1796
|
setup.resetScene()
|
|
1733
1797
|
await wait(300)
|
|
@@ -1773,145 +1837,145 @@
|
|
|
1773
1837
|
setup.resetScene()
|
|
1774
1838
|
await wait(300)
|
|
1775
1839
|
const dragCanvas = document.querySelector('.gis-view__canvas')
|
|
1776
|
-
const dragBox = dragCanvas.getBoundingClientRect()
|
|
1777
|
-
const dragSend = (type, screen, extra = {}) => {
|
|
1778
|
-
dragCanvas.dispatchEvent(new PointerEvent(type, {
|
|
1779
|
-
bubbles: true,
|
|
1780
|
-
cancelable: true,
|
|
1781
|
-
composed: true,
|
|
1782
|
-
pointerId: 17,
|
|
1783
|
-
pointerType: 'mouse',
|
|
1784
|
-
isPrimary: true,
|
|
1785
|
-
button: 0,
|
|
1786
|
-
buttons: extra.buttons ?? (type === 'pointerup' ? 0 : 1),
|
|
1787
|
-
clientX: dragBox.left + screen[0],
|
|
1788
|
-
clientY: dragBox.top + screen[1],
|
|
1789
|
-
...extra
|
|
1790
|
-
}))
|
|
1791
|
-
}
|
|
1792
|
-
const dragSourcePoint = view.getData().points.find(point => point.id === 'node-0')
|
|
1793
|
-
const dragTargetPoint = view.getData().points.find(point => point.id === 'node-7')
|
|
1794
|
-
const dragSource = view.project(dragSourcePoint.x, dragSourcePoint.y)
|
|
1795
|
-
const dragTarget = view.project(dragTargetPoint.x, dragTargetPoint.y)
|
|
1840
|
+
const dragBox = dragCanvas.getBoundingClientRect()
|
|
1841
|
+
const dragSend = (type, screen, extra = {}) => {
|
|
1842
|
+
dragCanvas.dispatchEvent(new PointerEvent(type, {
|
|
1843
|
+
bubbles: true,
|
|
1844
|
+
cancelable: true,
|
|
1845
|
+
composed: true,
|
|
1846
|
+
pointerId: 17,
|
|
1847
|
+
pointerType: 'mouse',
|
|
1848
|
+
isPrimary: true,
|
|
1849
|
+
button: 0,
|
|
1850
|
+
buttons: extra.buttons ?? (type === 'pointerup' ? 0 : 1),
|
|
1851
|
+
clientX: dragBox.left + screen[0],
|
|
1852
|
+
clientY: dragBox.top + screen[1],
|
|
1853
|
+
...extra
|
|
1854
|
+
}))
|
|
1855
|
+
}
|
|
1856
|
+
const dragSourcePoint = view.getData().points.find(point => point.id === 'node-0')
|
|
1857
|
+
const dragTargetPoint = view.getData().points.find(point => point.id === 'node-7')
|
|
1858
|
+
const dragSource = view.project(dragSourcePoint.x, dragSourcePoint.y)
|
|
1859
|
+
const dragTarget = view.project(dragTargetPoint.x, dragTargetPoint.y)
|
|
1796
1860
|
view.selectFeatures(null)
|
|
1797
|
-
dragSend('pointermove', dragSource, { buttons: 0 })
|
|
1798
|
-
dragSend('pointerdown', dragSource, { buttons: 1 })
|
|
1799
|
-
dragSend('pointermove', [(dragSource[0] + dragTarget[0]) / 2, (dragSource[1] + dragTarget[1]) / 2], { buttons: 1 })
|
|
1800
|
-
dragSend('pointermove', dragTarget, { buttons: 1 })
|
|
1801
|
-
dragSend('pointerup', dragTarget, { buttons: 0 })
|
|
1802
|
-
const dragPending = await until(() => Boolean(view.getPointMerge()) && view.getPointMerge().confirmed === false, 6000)
|
|
1803
|
-
check('批量复制与合并:真实拖拽相撞进入 pending,数据先不动', Boolean(dragPending) && view.getData().points.length === 14, JSON.stringify({ phase: view.getPointMerge()?.phase, confirmed: view.getPointMerge()?.confirmed, points: view.getData().points.length }))
|
|
1804
|
-
const pendingMergePayload = view.getPointMerge()
|
|
1805
|
-
const pendingImpact = pendingMergePayload?.impact
|
|
1806
|
-
const pendingImpactReadout = text('merge-pending-readout')
|
|
1807
|
-
check('批量复制与合并:pending 载荷带 impact 预演(含线路 id、改线 / 删线与通道自环影响)',
|
|
1808
|
-
Boolean(pendingImpact) && pendingImpact.lineCount === pendingImpact.lineIds.length && pendingImpact.lineCount >= pendingImpact.removedLines && pendingImpact.linesChanged > 0 && pendingImpact.removedLines > 0 && pendingImpact.removedChannels >= 1 && pendingImpactReadout.includes('影响') && pendingImpactReadout.includes('改线'),
|
|
1809
|
-
pendingImpact ? `线路 ${pendingImpact.lineCount}(改 ${pendingImpact.linesChanged} / 删 ${pendingImpact.removedLines})· 通道删 ${pendingImpact.removedChannels} · ${pendingImpact.lineIds.join(', ')}` : '没有 impact')
|
|
1810
|
-
click('merge-impact-details')
|
|
1811
|
-
await wait(80)
|
|
1812
|
-
const pendingImpactDetail = text('merge-impact-detail-readout')
|
|
1813
|
-
check('批量复制与合并:按 impact 的 id 调 getLine / getChannel 能列出详情(删掉的通道在确认前仍可读)',
|
|
1814
|
-
Boolean(pendingImpact) &&
|
|
1815
|
-
pendingImpact.lineIds.every(id => pendingImpactDetail.includes(String(id))) &&
|
|
1816
|
-
pendingImpact.channelIds.every(id => pendingImpactDetail.includes(String(id))) &&
|
|
1817
|
-
!pendingImpactDetail.includes('已不存在') && pendingImpactDetail.includes('回'),
|
|
1818
|
-
pendingImpactDetail.slice(0, 180))
|
|
1819
|
-
setup.mergeKeep = 'source'
|
|
1820
|
-
setup.mergedName = '拖拽保留来源'
|
|
1821
|
-
setup.mergedZone = '拖拽来源最终信息'
|
|
1822
|
-
setup.mergedColor = '#00ff88'
|
|
1823
|
-
setup.mergedSize = '21'
|
|
1824
|
-
setup.mergedSymbol = 'square'
|
|
1825
|
-
setup.mergedStrokeColor = '#222222'
|
|
1826
|
-
let confirmImpact = null
|
|
1827
|
-
let landedMerge = null
|
|
1828
|
-
const offMergeProbe = view.on('pointMerge', payload => {
|
|
1829
|
-
if (payload.phase === 'confirm') {
|
|
1830
|
-
confirmImpact = payload.impact
|
|
1831
|
-
}
|
|
1832
|
-
if (payload.phase === 'merge' && payload.ok) {
|
|
1833
|
-
landedMerge = payload
|
|
1834
|
-
}
|
|
1835
|
-
})
|
|
1836
|
-
click('merge-confirm')
|
|
1837
|
-
const sourceMergeDone = await until(() => view.getData().points.length === 13 && view.getData().points.some(point => point.id === 'node-0'), 6000)
|
|
1838
|
-
offMergeProbe()
|
|
1839
|
-
const mergedSource = view.getData().points.find(point => point.id === 'node-0')
|
|
1840
|
-
const removedTarget = view.getData().points.find(point => point.id === 'node-7')
|
|
1841
|
-
check('批量复制与合并:confirm 传 keep: source 时保留拖动来源并删碰撞目标', Boolean(sourceMergeDone) && Boolean(mergedSource) && !removedTarget, JSON.stringify({ source: mergedSource?.id, targetGone: !removedTarget, points: view.getData().points.length }))
|
|
1842
|
-
check('批量复制与合并:confirm 的 impact 按最终 keep 重算,且与落地后的 linesChanged / removedLines 对账一致',
|
|
1843
|
-
Boolean(confirmImpact) && Boolean(landedMerge) && confirmImpact.linesChanged === landedMerge.linesChanged && confirmImpact.removedLines === landedMerge.removedLines && confirmImpact.removedChannels === landedMerge.removedChannels && confirmImpact.mergedChannels === landedMerge.mergedChannels,
|
|
1844
|
-
confirmImpact && landedMerge ? `impact 改 ${confirmImpact.linesChanged} / 删 ${confirmImpact.removedLines} / 通道 ${confirmImpact.removedChannels}+${confirmImpact.mergedChannels} · 落地 改 ${landedMerge.linesChanged} / 删 ${landedMerge.removedLines} / 通道 ${landedMerge.removedChannels}+${landedMerge.mergedChannels}` : '没有 confirm / merge 载荷')
|
|
1845
|
-
check('批量复制与合并:拖拽合并的 item 改写信息与样式都落到最终节点', mergedSource?.name === '拖拽保留来源' && mergedSource?.zone_name === '拖拽来源最终信息' && mergedSource?.color === '#00ff88' && mergedSource?.size === 21 && mergedSource?.symbol === 'square' && mergedSource?.strokeColor === '#222222', JSON.stringify(mergedSource && { name: mergedSource.name, zone: mergedSource.zone_name, color: mergedSource.color, size: mergedSource.size, symbol: mergedSource.symbol, stroke: mergedSource.strokeColor }))
|
|
1846
|
-
click('copy-select-sources')
|
|
1847
|
-
await wait(120)
|
|
1848
|
-
const pointsBeforeManual = view.getData().points.length
|
|
1849
|
-
click('copy-selected')
|
|
1850
|
-
const autoCopied = await until(() => view.getData().points.length === pointsBeforeManual + 4, 6000)
|
|
1851
|
-
click('manual-add-point')
|
|
1852
|
-
const manualAdded = await until(() => view.getData().points.length === pointsBeforeManual + 4 && text('manual-add-point-readout').includes('已删自动副本 4 个'), 6000)
|
|
1853
|
-
const manualPoints = view.getData().points.slice(-4)
|
|
1854
|
-
const autoCopies = view.getData().points.filter(point => point.zone_name === '内置复制副本').length
|
|
1855
|
-
check('批量复制与合并:手动 addPoint 删掉自动副本,最终只多一份', Boolean(autoCopied) && Boolean(manualAdded) && manualPoints.every(point => point.demo_copy_via === 'addPoint' && point.zone_name === 'addPoint 手动复制') && autoCopies === 0 && text('manual-add-point-readout').includes('addPoint ×4') && text('manual-add-point-readout').includes('已删自动副本'), `${pointsBeforeManual} → ${view.getData().points.length} / auto=${autoCopies} / ${text('manual-add-point-readout')}`)
|
|
1856
|
-
await wait(560)
|
|
1857
|
-
const copyRequestPreview = text('copy-merge-backend-request')
|
|
1858
|
-
check('批量复制与合并:复制假接口请求体带批量字段、版本与幂等键',
|
|
1859
|
-
copyRequestPreview.includes('POST /gis/copies/') &&
|
|
1860
|
-
['"points"', '"lines"', '"channels"', '"lineChannels"', '"baseVersion"', '"opId"'].every(key => copyRequestPreview.includes(key)) &&
|
|
1861
|
-
text('copy-merge-backend-status').includes('420ms'),
|
|
1862
|
-
copyRequestPreview.slice(0, 200))
|
|
1863
|
-
check('批量复制与合并:复制成功响应回标准响应包、created 与 idMap',
|
|
1864
|
-
['"code": "OK"', '"operation": "copy.', '"transactionId"', '"created"', '"counts"', '"idMap"', 'Idempotency-Key:', 'If-Match: "v'].every(key => copyRequestPreview.includes(key)),
|
|
1865
|
-
copyRequestPreview.slice(-320).replace(/\s+/g, ' '))
|
|
1866
|
-
const copyReplayButton = document.querySelector('[data-demo="copy-backend-replay"]')
|
|
1867
|
-
const copyVersionBeforeReplay = setup.backendSnapshot?.version
|
|
1868
|
-
const copyAppliedBeforeReplay = setup.backendSnapshot?.applied
|
|
1869
|
-
const copyReplayReady = Boolean(copyReplayButton) && !copyReplayButton.disabled
|
|
1870
|
-
copyReplayButton?.click()
|
|
1871
|
-
const copyReplayHit = await until(() => text('copy-backend-readout').includes('幂等回放') && text('copy-merge-backend-request').includes('"idempotent": true'), 5000)
|
|
1872
|
-
check('批量复制与合并:重放原 opId 命中幂等缓存,版本与应用数都不前进',
|
|
1873
|
-
copyReplayReady && Boolean(copyReplayHit) && setup.backendSnapshot?.version === copyVersionBeforeReplay && setup.backendSnapshot?.applied === copyAppliedBeforeReplay && text('copy-merge-backend-request').includes('Idempotency-Key 命中'),
|
|
1874
|
-
`v${copyVersionBeforeReplay} / applied ${copyAppliedBeforeReplay} → v${setup.backendSnapshot?.version} / applied ${setup.backendSnapshot?.applied} / ${text('copy-backend-readout')}`)
|
|
1875
|
-
const backendModeSelect = document.querySelector('[data-demo="copy-merge-backend-mode"]')
|
|
1876
|
-
const copyRetryButton = document.querySelector('[data-demo="copy-backend-retry"]')
|
|
1877
|
-
const copyRollbackButton = document.querySelector('[data-demo="copy-backend-rollback"]')
|
|
1878
|
-
backendModeSelect.value = 'network'
|
|
1879
|
-
backendModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1880
|
-
await wait(60)
|
|
1881
|
-
click('copy-select-sources')
|
|
1882
|
-
await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
1883
|
-
const pointsBeforeFailedCopy = view.getData().points.length
|
|
1884
|
-
click('copy-selected')
|
|
1885
|
-
const copyFailed = await until(() => !copyRetryButton.disabled && !copyRollbackButton.disabled && text('copy-backend-readout').includes('失败'), 8000)
|
|
1886
|
-
const failedCopyReadout = text('copy-backend-readout')
|
|
1887
|
-
check('批量复制与合并:复制网络失败后保留本地结果,重试与回滚按钮可用', Boolean(copyFailed) && view.getData().points.length === pointsBeforeFailedCopy + 4 && failedCopyReadout.includes('网络请求没有到达服务端'), `${pointsBeforeFailedCopy} → ${view.getData().points.length} / retry=${copyRetryButton.disabled} rollback=${copyRollbackButton.disabled} / ${failedCopyReadout}`)
|
|
1888
|
-
copyRetryButton.click()
|
|
1889
|
-
const copyRetried = await until(() => text('copy-backend-readout').includes('200 OK'), 5000)
|
|
1890
|
-
check('批量复制与合并:复制失败后重试同一请求成功并推进服务端版本', Boolean(copyRetried) && !text('copy-backend-readout').includes('失败'), text('copy-backend-readout'))
|
|
1891
|
-
backendModeSelect.value = 'server'
|
|
1892
|
-
backendModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1893
|
-
click('copy-select-sources')
|
|
1894
|
-
await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
1895
|
-
const pointsBeforeRollback = view.getData().points.length
|
|
1896
|
-
click('copy-selected')
|
|
1897
|
-
const rollbackReady = await until(() => !copyRollbackButton.disabled && text('copy-backend-readout').includes('失败'), 8000)
|
|
1898
|
-
copyRollbackButton.click()
|
|
1899
|
-
const copyRolledBack = await until(() => view.getData().points.length === pointsBeforeRollback && text('copy-backend-readout').includes('回滚'), 5000)
|
|
1900
|
-
check('批量复制与合并:复制失败后可回滚本地整批,服务端版本不前进', Boolean(rollbackReady) && Boolean(copyRolledBack), `${pointsBeforeRollback} → ${view.getData().points.length} / ${text('copy-backend-readout')}`)
|
|
1901
|
-
const conflictVersion = setup.backendSnapshot?.version
|
|
1902
|
-
const conflictApplied = setup.backendSnapshot?.applied
|
|
1903
|
-
backendModeSelect.value = 'conflict'
|
|
1904
|
-
backendModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1905
|
-
click('copy-select-sources')
|
|
1906
|
-
await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
1907
|
-
click('copy-selected')
|
|
1908
|
-
const copyConflict = await until(() => !copyRetryButton.disabled && text('copy-backend-readout').includes('409'), 8000)
|
|
1909
|
-
const conflictPreview = text('copy-merge-backend-request')
|
|
1910
|
-
check('批量复制与合并:版本冲突回 409 响应包与 expectedVersion / currentVersion',
|
|
1911
|
-
Boolean(copyConflict) && conflictPreview.includes('409 Conflict') && conflictPreview.includes('"code": "VERSION_CONFLICT"') && conflictPreview.includes(`"expectedVersion": ${conflictVersion}`) && conflictPreview.includes(`"currentVersion": ${conflictVersion + 1}`) && setup.backendSnapshot?.applied === conflictApplied,
|
|
1912
|
-
`${conflictPreview.slice(-320).replace(/\s+/g, ' ')} / ${text('copy-backend-readout')}`)
|
|
1913
|
-
copyRetryButton.click()
|
|
1914
|
-
const copyRebased = await until(() => text('copy-backend-readout').includes('200 OK') && !text('copy-backend-readout').includes('失败'), 5000)
|
|
1861
|
+
dragSend('pointermove', dragSource, { buttons: 0 })
|
|
1862
|
+
dragSend('pointerdown', dragSource, { buttons: 1 })
|
|
1863
|
+
dragSend('pointermove', [(dragSource[0] + dragTarget[0]) / 2, (dragSource[1] + dragTarget[1]) / 2], { buttons: 1 })
|
|
1864
|
+
dragSend('pointermove', dragTarget, { buttons: 1 })
|
|
1865
|
+
dragSend('pointerup', dragTarget, { buttons: 0 })
|
|
1866
|
+
const dragPending = await until(() => Boolean(view.getPointMerge()) && view.getPointMerge().confirmed === false, 6000)
|
|
1867
|
+
check('批量复制与合并:真实拖拽相撞进入 pending,数据先不动', Boolean(dragPending) && view.getData().points.length === 14, JSON.stringify({ phase: view.getPointMerge()?.phase, confirmed: view.getPointMerge()?.confirmed, points: view.getData().points.length }))
|
|
1868
|
+
const pendingMergePayload = view.getPointMerge()
|
|
1869
|
+
const pendingImpact = pendingMergePayload?.impact
|
|
1870
|
+
const pendingImpactReadout = text('merge-pending-readout')
|
|
1871
|
+
check('批量复制与合并:pending 载荷带 impact 预演(含线路 id、改线 / 删线与通道自环影响)',
|
|
1872
|
+
Boolean(pendingImpact) && pendingImpact.lineCount === pendingImpact.lineIds.length && pendingImpact.lineCount >= pendingImpact.removedLines && pendingImpact.linesChanged > 0 && pendingImpact.removedLines > 0 && pendingImpact.removedChannels >= 1 && pendingImpactReadout.includes('影响') && pendingImpactReadout.includes('改线'),
|
|
1873
|
+
pendingImpact ? `线路 ${pendingImpact.lineCount}(改 ${pendingImpact.linesChanged} / 删 ${pendingImpact.removedLines})· 通道删 ${pendingImpact.removedChannels} · ${pendingImpact.lineIds.join(', ')}` : '没有 impact')
|
|
1874
|
+
click('merge-impact-details')
|
|
1875
|
+
await wait(80)
|
|
1876
|
+
const pendingImpactDetail = text('merge-impact-detail-readout')
|
|
1877
|
+
check('批量复制与合并:按 impact 的 id 调 getLine / getChannel 能列出详情(删掉的通道在确认前仍可读)',
|
|
1878
|
+
Boolean(pendingImpact) &&
|
|
1879
|
+
pendingImpact.lineIds.every(id => pendingImpactDetail.includes(String(id))) &&
|
|
1880
|
+
pendingImpact.channelIds.every(id => pendingImpactDetail.includes(String(id))) &&
|
|
1881
|
+
!pendingImpactDetail.includes('已不存在') && pendingImpactDetail.includes('回'),
|
|
1882
|
+
pendingImpactDetail.slice(0, 180))
|
|
1883
|
+
setup.mergeKeep = 'source'
|
|
1884
|
+
setup.mergedName = '拖拽保留来源'
|
|
1885
|
+
setup.mergedZone = '拖拽来源最终信息'
|
|
1886
|
+
setup.mergedColor = '#00ff88'
|
|
1887
|
+
setup.mergedSize = '21'
|
|
1888
|
+
setup.mergedSymbol = 'square'
|
|
1889
|
+
setup.mergedStrokeColor = '#222222'
|
|
1890
|
+
let confirmImpact = null
|
|
1891
|
+
let landedMerge = null
|
|
1892
|
+
const offMergeProbe = view.on('pointMerge', payload => {
|
|
1893
|
+
if (payload.phase === 'confirm') {
|
|
1894
|
+
confirmImpact = payload.impact
|
|
1895
|
+
}
|
|
1896
|
+
if (payload.phase === 'merge' && payload.ok) {
|
|
1897
|
+
landedMerge = payload
|
|
1898
|
+
}
|
|
1899
|
+
})
|
|
1900
|
+
click('merge-confirm')
|
|
1901
|
+
const sourceMergeDone = await until(() => view.getData().points.length === 13 && view.getData().points.some(point => point.id === 'node-0'), 6000)
|
|
1902
|
+
offMergeProbe()
|
|
1903
|
+
const mergedSource = view.getData().points.find(point => point.id === 'node-0')
|
|
1904
|
+
const removedTarget = view.getData().points.find(point => point.id === 'node-7')
|
|
1905
|
+
check('批量复制与合并:confirm 传 keep: source 时保留拖动来源并删碰撞目标', Boolean(sourceMergeDone) && Boolean(mergedSource) && !removedTarget, JSON.stringify({ source: mergedSource?.id, targetGone: !removedTarget, points: view.getData().points.length }))
|
|
1906
|
+
check('批量复制与合并:confirm 的 impact 按最终 keep 重算,且与落地后的 linesChanged / removedLines 对账一致',
|
|
1907
|
+
Boolean(confirmImpact) && Boolean(landedMerge) && confirmImpact.linesChanged === landedMerge.linesChanged && confirmImpact.removedLines === landedMerge.removedLines && confirmImpact.removedChannels === landedMerge.removedChannels && confirmImpact.mergedChannels === landedMerge.mergedChannels,
|
|
1908
|
+
confirmImpact && landedMerge ? `impact 改 ${confirmImpact.linesChanged} / 删 ${confirmImpact.removedLines} / 通道 ${confirmImpact.removedChannels}+${confirmImpact.mergedChannels} · 落地 改 ${landedMerge.linesChanged} / 删 ${landedMerge.removedLines} / 通道 ${landedMerge.removedChannels}+${landedMerge.mergedChannels}` : '没有 confirm / merge 载荷')
|
|
1909
|
+
check('批量复制与合并:拖拽合并的 item 改写信息与样式都落到最终节点', mergedSource?.name === '拖拽保留来源' && mergedSource?.zone_name === '拖拽来源最终信息' && mergedSource?.color === '#00ff88' && mergedSource?.size === 21 && mergedSource?.symbol === 'square' && mergedSource?.strokeColor === '#222222', JSON.stringify(mergedSource && { name: mergedSource.name, zone: mergedSource.zone_name, color: mergedSource.color, size: mergedSource.size, symbol: mergedSource.symbol, stroke: mergedSource.strokeColor }))
|
|
1910
|
+
click('copy-select-sources')
|
|
1911
|
+
await wait(120)
|
|
1912
|
+
const pointsBeforeManual = view.getData().points.length
|
|
1913
|
+
click('copy-selected')
|
|
1914
|
+
const autoCopied = await until(() => view.getData().points.length === pointsBeforeManual + 4, 6000)
|
|
1915
|
+
click('manual-add-point')
|
|
1916
|
+
const manualAdded = await until(() => view.getData().points.length === pointsBeforeManual + 4 && text('manual-add-point-readout').includes('已删自动副本 4 个'), 6000)
|
|
1917
|
+
const manualPoints = view.getData().points.slice(-4)
|
|
1918
|
+
const autoCopies = view.getData().points.filter(point => point.zone_name === '内置复制副本').length
|
|
1919
|
+
check('批量复制与合并:手动 addPoint 删掉自动副本,最终只多一份', Boolean(autoCopied) && Boolean(manualAdded) && manualPoints.every(point => point.demo_copy_via === 'addPoint' && point.zone_name === 'addPoint 手动复制') && autoCopies === 0 && text('manual-add-point-readout').includes('addPoint ×4') && text('manual-add-point-readout').includes('已删自动副本'), `${pointsBeforeManual} → ${view.getData().points.length} / auto=${autoCopies} / ${text('manual-add-point-readout')}`)
|
|
1920
|
+
await wait(560)
|
|
1921
|
+
const copyRequestPreview = text('copy-merge-backend-request')
|
|
1922
|
+
check('批量复制与合并:复制假接口请求体带批量字段、版本与幂等键',
|
|
1923
|
+
copyRequestPreview.includes('POST /gis/copies/') &&
|
|
1924
|
+
['"points"', '"lines"', '"channels"', '"lineChannels"', '"baseVersion"', '"opId"'].every(key => copyRequestPreview.includes(key)) &&
|
|
1925
|
+
text('copy-merge-backend-status').includes('420ms'),
|
|
1926
|
+
copyRequestPreview.slice(0, 200))
|
|
1927
|
+
check('批量复制与合并:复制成功响应回标准响应包、created 与 idMap',
|
|
1928
|
+
['"code": "OK"', '"operation": "copy.', '"transactionId"', '"created"', '"counts"', '"idMap"', 'Idempotency-Key:', 'If-Match: "v'].every(key => copyRequestPreview.includes(key)),
|
|
1929
|
+
copyRequestPreview.slice(-320).replace(/\s+/g, ' '))
|
|
1930
|
+
const copyReplayButton = document.querySelector('[data-demo="copy-backend-replay"]')
|
|
1931
|
+
const copyVersionBeforeReplay = setup.backendSnapshot?.version
|
|
1932
|
+
const copyAppliedBeforeReplay = setup.backendSnapshot?.applied
|
|
1933
|
+
const copyReplayReady = Boolean(copyReplayButton) && !copyReplayButton.disabled
|
|
1934
|
+
copyReplayButton?.click()
|
|
1935
|
+
const copyReplayHit = await until(() => text('copy-backend-readout').includes('幂等回放') && text('copy-merge-backend-request').includes('"idempotent": true'), 5000)
|
|
1936
|
+
check('批量复制与合并:重放原 opId 命中幂等缓存,版本与应用数都不前进',
|
|
1937
|
+
copyReplayReady && Boolean(copyReplayHit) && setup.backendSnapshot?.version === copyVersionBeforeReplay && setup.backendSnapshot?.applied === copyAppliedBeforeReplay && text('copy-merge-backend-request').includes('Idempotency-Key 命中'),
|
|
1938
|
+
`v${copyVersionBeforeReplay} / applied ${copyAppliedBeforeReplay} → v${setup.backendSnapshot?.version} / applied ${setup.backendSnapshot?.applied} / ${text('copy-backend-readout')}`)
|
|
1939
|
+
const backendModeSelect = document.querySelector('[data-demo="copy-merge-backend-mode"]')
|
|
1940
|
+
const copyRetryButton = document.querySelector('[data-demo="copy-backend-retry"]')
|
|
1941
|
+
const copyRollbackButton = document.querySelector('[data-demo="copy-backend-rollback"]')
|
|
1942
|
+
backendModeSelect.value = 'network'
|
|
1943
|
+
backendModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1944
|
+
await wait(60)
|
|
1945
|
+
click('copy-select-sources')
|
|
1946
|
+
await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
1947
|
+
const pointsBeforeFailedCopy = view.getData().points.length
|
|
1948
|
+
click('copy-selected')
|
|
1949
|
+
const copyFailed = await until(() => !copyRetryButton.disabled && !copyRollbackButton.disabled && text('copy-backend-readout').includes('失败'), 8000)
|
|
1950
|
+
const failedCopyReadout = text('copy-backend-readout')
|
|
1951
|
+
check('批量复制与合并:复制网络失败后保留本地结果,重试与回滚按钮可用', Boolean(copyFailed) && view.getData().points.length === pointsBeforeFailedCopy + 4 && failedCopyReadout.includes('网络请求没有到达服务端'), `${pointsBeforeFailedCopy} → ${view.getData().points.length} / retry=${copyRetryButton.disabled} rollback=${copyRollbackButton.disabled} / ${failedCopyReadout}`)
|
|
1952
|
+
copyRetryButton.click()
|
|
1953
|
+
const copyRetried = await until(() => text('copy-backend-readout').includes('200 OK'), 5000)
|
|
1954
|
+
check('批量复制与合并:复制失败后重试同一请求成功并推进服务端版本', Boolean(copyRetried) && !text('copy-backend-readout').includes('失败'), text('copy-backend-readout'))
|
|
1955
|
+
backendModeSelect.value = 'server'
|
|
1956
|
+
backendModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1957
|
+
click('copy-select-sources')
|
|
1958
|
+
await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
1959
|
+
const pointsBeforeRollback = view.getData().points.length
|
|
1960
|
+
click('copy-selected')
|
|
1961
|
+
const rollbackReady = await until(() => !copyRollbackButton.disabled && text('copy-backend-readout').includes('失败'), 8000)
|
|
1962
|
+
copyRollbackButton.click()
|
|
1963
|
+
const copyRolledBack = await until(() => view.getData().points.length === pointsBeforeRollback && text('copy-backend-readout').includes('回滚'), 5000)
|
|
1964
|
+
check('批量复制与合并:复制失败后可回滚本地整批,服务端版本不前进', Boolean(rollbackReady) && Boolean(copyRolledBack), `${pointsBeforeRollback} → ${view.getData().points.length} / ${text('copy-backend-readout')}`)
|
|
1965
|
+
const conflictVersion = setup.backendSnapshot?.version
|
|
1966
|
+
const conflictApplied = setup.backendSnapshot?.applied
|
|
1967
|
+
backendModeSelect.value = 'conflict'
|
|
1968
|
+
backendModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1969
|
+
click('copy-select-sources')
|
|
1970
|
+
await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
1971
|
+
click('copy-selected')
|
|
1972
|
+
const copyConflict = await until(() => !copyRetryButton.disabled && text('copy-backend-readout').includes('409'), 8000)
|
|
1973
|
+
const conflictPreview = text('copy-merge-backend-request')
|
|
1974
|
+
check('批量复制与合并:版本冲突回 409 响应包与 expectedVersion / currentVersion',
|
|
1975
|
+
Boolean(copyConflict) && conflictPreview.includes('409 Conflict') && conflictPreview.includes('"code": "VERSION_CONFLICT"') && conflictPreview.includes(`"expectedVersion": ${conflictVersion}`) && conflictPreview.includes(`"currentVersion": ${conflictVersion + 1}`) && setup.backendSnapshot?.applied === conflictApplied,
|
|
1976
|
+
`${conflictPreview.slice(-320).replace(/\s+/g, ' ')} / ${text('copy-backend-readout')}`)
|
|
1977
|
+
copyRetryButton.click()
|
|
1978
|
+
const copyRebased = await until(() => text('copy-backend-readout').includes('200 OK') && !text('copy-backend-readout').includes('失败'), 5000)
|
|
1915
1979
|
check('批量复制与合并:409 后 rebase 生成新 opId 并成功落库',
|
|
1916
1980
|
Boolean(copyRebased) && setup.backendSnapshot?.version === conflictVersion + 2 && setup.backendSnapshot?.applied === conflictApplied + 1,
|
|
1917
1981
|
`v${conflictVersion} → v${setup.backendSnapshot?.version} / applied ${conflictApplied} → ${setup.backendSnapshot?.applied} / ${text('copy-backend-readout')}`)
|
|
@@ -1934,11 +1998,11 @@
|
|
|
1934
1998
|
const splitUndone = await until(() => view.getData().points.length === pointsBeforeSplit && view.getData().points.some(point => String(point.id) === String(splitSource?.id)), 5000)
|
|
1935
1999
|
check('批量复制与合并:拆分是一步历史,撤销后节点与连线恢复', Boolean(splitUndone), `${view.getData().points.length} 点 / ${splitSource?.id ?? '—'}`)
|
|
1936
2000
|
}
|
|
1937
|
-
else {
|
|
1938
|
-
check('批量复制与合并:画布与实例都起来了', false, ready ? '没有 setupState.view' : '没有 canvas')
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
|
|
2001
|
+
else {
|
|
2002
|
+
check('批量复制与合并:画布与实例都起来了', false, ready ? '没有 setupState.view' : '没有 canvas')
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
|
|
1942
2006
|
// 2.0c 扩展能力:六个扩展点各开一次,读数 / 徽标 / 导出结果 / WebGL 层与命令表都要跟着变
|
|
1943
2007
|
if (await clickTab('扩展能力')) {
|
|
1944
2008
|
const ready = await canvasReady()
|
|
@@ -2021,179 +2085,179 @@
|
|
|
2021
2085
|
}
|
|
2022
2086
|
|
|
2023
2087
|
// 2.1 拖拽落点:看板图标合成一次拖放 —— 拖拽期间不出现浏览器默认的看板拖影,地图上只跟着一个点
|
|
2024
|
-
// (组件画的落点预览,与落地后的节点同一套画法);松手先显示在地图上,指定属性之后
|
|
2025
|
-
// 「按属性新增」才写进数据、「取消新增」什么都不写;落地后的尺寸与预览里报出的尺寸必须一样
|
|
2026
|
-
await clickTab('拖拽落点')
|
|
2027
|
-
const dragReady = await canvasReady()
|
|
2028
|
-
const paletteItems = [...document.querySelectorAll('.dv-palette__item')]
|
|
2029
|
-
let dragResult = null
|
|
2030
|
-
let cancelResult = null
|
|
2031
|
-
let strayResult = null
|
|
2032
|
-
let ghostHidden = false
|
|
2033
|
-
let ghostNode = null
|
|
2034
|
-
if (dragReady && paletteItems.length === 7) {
|
|
2035
|
-
const dragView = document.querySelector('.dv-demo').__vueParentComponent.setupState.view
|
|
2036
|
-
const pointsBefore = dragView.getData().points.length
|
|
2037
|
-
const canvasNode = document.querySelector('.dv-demo .dv-canvas')
|
|
2038
|
-
const canvasBox = canvasNode.getBoundingClientRect()
|
|
2039
|
-
const transfer = new DataTransfer()
|
|
2040
|
-
// 拖影:看板按钮默认会把整个按钮当拖影(跟着指针的那个「变电站」框)。这里盯着 setDragImage 的入参,
|
|
2041
|
-
// 确认换成了 1×1 的透明元素 —— 屏幕上只剩地图上那个点
|
|
2042
|
-
let ghostCall = null
|
|
2043
|
-
const setDragImage = transfer.setDragImage
|
|
2044
|
-
transfer.setDragImage = (node, x, y) => {
|
|
2045
|
-
ghostCall = { node, x, y }
|
|
2046
|
-
return setDragImage?.call(transfer, node, x, y)
|
|
2047
|
-
}
|
|
2048
|
-
const dragItem = (index, x, y) => {
|
|
2049
|
-
paletteItems[index].dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: transfer }))
|
|
2050
|
-
const init = { bubbles: true, cancelable: true, dataTransfer: transfer, clientX: x, clientY: y }
|
|
2051
|
-
canvasNode.dispatchEvent(new DragEvent('dragover', init))
|
|
2052
|
-
return init
|
|
2053
|
-
}
|
|
2054
|
-
// 第 1 项是 230kV 档:规则给 9px 的圆
|
|
2055
|
-
const dropInit = {
|
|
2056
|
-
bubbles: true,
|
|
2057
|
-
cancelable: true,
|
|
2058
|
-
dataTransfer: transfer,
|
|
2059
|
-
clientX: canvasBox.left + canvasBox.width * 0.3,
|
|
2060
|
-
clientY: canvasBox.top + canvasBox.height * 0.35
|
|
2061
|
-
}
|
|
2062
|
-
dragItem(0, dropInit.clientX, dropInit.clientY)
|
|
2063
|
-
const previewShown = await until(() => dragView.getDropPreview(), 4000)
|
|
2064
|
-
const previewSize = previewShown?.size ?? null
|
|
2065
|
-
const previewSymbol = previewShown?.symbol ?? null
|
|
2066
|
-
ghostNode = document.querySelector('.dv-drag-ghost')
|
|
2067
|
-
ghostHidden = Boolean(ghostCall?.node) && ghostCall.node === ghostNode
|
|
2068
|
-
&& ghostCall.node.getBoundingClientRect().width <= 2
|
|
2069
|
-
&& getComputedStyle(ghostCall.node).opacity === '0'
|
|
2070
|
-
const readoutPreviewed = await until(() => [...document.querySelectorAll('.dv-demo .dv-readout')].some(node => node.textContent.includes('预览图标')), 4000)
|
|
2071
|
-
canvasNode.dispatchEvent(new DragEvent('drop', dropInit))
|
|
2072
|
-
const draftShown = await until(() => dragView.getDropPreview(), 4000)
|
|
2073
|
-
const untouched = dragView.getData().points.length === pointsBefore
|
|
2074
|
-
// 真浏览器在 drop 之后还会给源元素补一条 dragend(拖拽演示里就是看板按钮上绑的那一个)。
|
|
2075
|
-
// 这一条要是顺手把预览收掉,「松手先显示」就只剩一眨眼 —— 这里照着补发一条,核对点还在落点上
|
|
2076
|
-
const draftWorld = draftShown?.world ?? null
|
|
2077
|
-
paletteItems[0].dispatchEvent(new DragEvent('dragend', { bubbles: true, cancelable: true, dataTransfer: transfer }))
|
|
2078
|
-
await wait(200)
|
|
2079
|
-
const afterDragEnd = dragView.getDropPreview()
|
|
2080
|
-
const keptAfterDragEnd = Boolean(afterDragEnd) && Boolean(draftWorld)
|
|
2081
|
-
&& Math.abs(afterDragEnd.world[0] - draftWorld[0]) < 1e-6 && Math.abs(afterDragEnd.world[1] - draftWorld[1]) < 1e-6
|
|
2082
|
-
// 草稿还挂着的时候又拖一项进来(dragover 会先按新的一项画预览),中途松手没落上:
|
|
2083
|
-
// 临时预览收掉、点回到草稿自己的落点上,数据依旧一个字节没动
|
|
2084
|
-
const strayInit = { ...dropInit, clientX: dropInit.clientX + 140, clientY: dropInit.clientY + 80 }
|
|
2085
|
-
dragItem(4, strayInit.clientX, strayInit.clientY)
|
|
2086
|
-
// 预览合并到下一帧才算:等它真的挪到新落点上(还留着草稿的那一份就直接算没挪)
|
|
2087
|
-
const strayShown = await until(() => {
|
|
2088
|
-
const shown = dragView.getDropPreview()
|
|
2089
|
-
return shown && draftWorld && (Math.abs(shown.world[0] - draftWorld[0]) > 1e-6 || Math.abs(shown.world[1] - draftWorld[1]) > 1e-6) ? shown : null
|
|
2090
|
-
}, 4000)
|
|
2091
|
-
const strayMoved = Boolean(strayShown)
|
|
2092
|
-
paletteItems[4].dispatchEvent(new DragEvent('dragend', { bubbles: true, cancelable: true, dataTransfer: transfer }))
|
|
2093
|
-
await wait(200)
|
|
2094
|
-
const restored = dragView.getDropPreview()
|
|
2095
|
-
strayResult = {
|
|
2096
|
-
moved: strayMoved,
|
|
2097
|
-
restored: Boolean(restored) && Boolean(draftWorld)
|
|
2098
|
-
&& Math.abs(restored.world[0] - draftWorld[0]) < 1e-6 && Math.abs(restored.world[1] - draftWorld[1]) < 1e-6,
|
|
2099
|
-
untouched: dragView.getData().points.length === pointsBefore
|
|
2100
|
-
}
|
|
2101
|
-
// 面板上指定属性:改成 525kV(规则给 13px),预览与读数必须跟着重画
|
|
2102
|
-
const kvSelect = [...document.querySelectorAll('.dv-demo select')].find(node => node.textContent.includes('525 kV'))
|
|
2103
|
-
if (kvSelect) {
|
|
2104
|
-
kvSelect.value = '525'
|
|
2105
|
-
kvSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
2106
|
-
}
|
|
2107
|
-
const resized = await until(() => (dragView.getDropPreview()?.size ?? 0) === 13, 4000)
|
|
2108
|
-
const specifiedSize = dragView.getDropPreview()?.size ?? null
|
|
2109
|
-
check('拖拽落点:dragover 期间组件就解析出落点图标(尺寸 / 符号 / 颜色都有读数)', Boolean(previewShown) && Number(previewSize) > 0 && Boolean(previewSymbol), previewShown ? `${previewSize}px / ${previewSymbol} / ${previewShown.color}` : '没有落点预览')
|
|
2110
|
-
check('拖拽落点:预览读数写进面板(不是照着样子仿的)', Boolean(readoutPreviewed), readoutPreviewed ? '读数里出现「预览图标」' : '读数里没有预览尺寸')
|
|
2111
|
-
document.querySelector('.dv-demo .dv-confirm .dv-button--primary')?.click()
|
|
2112
|
-
const landed = await until(() => dragView.getData().points.length === pointsBefore + 1, 4000)
|
|
2113
|
-
const landedItem = landed ? dragView.getData().points.at(-1) : null
|
|
2114
|
-
// 落地后的那一份:同一条规则链解析出来的尺寸与符号必须与预览一致,属性就是面板上指定的那一个
|
|
2115
|
-
const landedStyle = landed ? dragView.getPointStyle(pointsBefore) : null
|
|
2116
|
-
const sameAsPreview = Boolean(landedStyle) && landedStyle.size === specifiedSize && previewSymbol === 'circle' && landedItem?.symbol === 'circle'
|
|
2117
|
-
const specified = Boolean(kvSelect) && Number(landedItem?.vn_kv) === 525 && Boolean(resized)
|
|
2118
|
-
const cleared = landed ? dragView.getDropPreview() === null : false
|
|
2119
|
-
dragResult = { before: pointsBefore, draft: Boolean(draftShown), untouched, keptAfterDragEnd, landed: Boolean(landed), sameAsPreview, specified, size: specifiedSize, cleared, after: dragView.getData().points.length }
|
|
2120
|
-
// 第二轮:拖一个进去之后点「取消新增」—— 地图上的点收掉,数据一个字节没动
|
|
2121
|
-
const cancelBefore = dragView.getData().points.length
|
|
2122
|
-
const cancelX = dropInit.clientX + 24
|
|
2123
|
-
const cancelY = dropInit.clientY + 24
|
|
2124
|
-
dragItem(1, cancelX, cancelY)
|
|
2125
|
-
await until(() => dragView.getDropPreview(), 4000)
|
|
2126
|
-
canvasNode.dispatchEvent(new DragEvent('drop', { ...dropInit, clientX: cancelX, clientY: cancelY }))
|
|
2127
|
-
// 这一段同样要过一遍 dragend:取消新增之前,点必须一直留在地图上
|
|
2128
|
-
paletteItems[1].dispatchEvent(new DragEvent('dragend', { bubbles: true, cancelable: true, dataTransfer: transfer }))
|
|
2129
|
-
const cancelShown = await until(() => dragView.getDropPreview(), 4000)
|
|
2130
|
-
const cancelButton = [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.trim() === '取消新增')
|
|
2131
|
-
cancelButton?.click()
|
|
2132
|
-
await wait(200)
|
|
2133
|
-
cancelResult = { shown: Boolean(cancelShown), button: Boolean(cancelButton), cleared: dragView.getDropPreview() === null, untouched: dragView.getData().points.length === cancelBefore }
|
|
2134
|
-
}
|
|
2135
|
-
check('拖拽落点:拖拽期间不出现浏览器默认的看板拖影(只剩地图上的点)', Boolean(ghostNode) && ghostHidden, ghostHidden ? 'setDragImage 指向 1×1 透明节点' : '拖影还是看板按钮本身')
|
|
2136
|
-
check('拖拽落点:松手之后预览不收起,先显示在地图上、数据一个字节没动', Boolean(dragResult?.draft) && Boolean(dragResult?.untouched), dragResult ? `点 ${dragResult.before} -> ${dragResult.after}` : `看板 ${paletteItems.length} 项 / 画布 ${Boolean(dragReady)}`)
|
|
2137
|
-
check('拖拽落点:drop 之后那条 dragend 不会把原来拖动的点收掉(点一直留在落点上)', Boolean(dragResult?.keptAfterDragEnd), dragResult?.keptAfterDragEnd ? 'dragend 之后点还在,世界坐标没挪' : 'dragend 之后预览被收掉了')
|
|
2138
|
-
check('拖拽落点:草稿挂着时又拖一项进来、中途松手 —— 点回到落点,数据不动', Boolean(strayResult?.moved) && Boolean(strayResult?.restored) && Boolean(strayResult?.untouched), `临时预览 ${strayResult?.moved ? '挪走了' : '没挪'} / 回到落点 ${strayResult?.restored ? '是' : '否'} / 数据 ${strayResult?.untouched ? '没动' : '动了'}`)
|
|
2139
|
-
check('拖拽落点:面板指定属性(525kV)之后预览跟着重画', Boolean(dragResult?.size) && dragResult.size === 13, dragResult ? `指定之后 ${dragResult.size}px` : '没有拖放结果')
|
|
2140
|
-
check('拖拽落点:点「按属性新增」才写进数据,写进去的就是指定的那一份属性', Boolean(dragResult?.landed) && Boolean(dragResult?.specified), dragResult ? `确认后 ${dragResult.after} 个节点 / vn_kv=525` : '没有拖放结果')
|
|
2141
|
-
check('拖拽落点:落地后的图标尺寸 / 符号与预览完全一致(规则链同一条)', Boolean(dragResult?.sameAsPreview), dragResult ? `预览与落地 ${dragResult.sameAsPreview ? '一致' : '不一致'}` : '没有拖放结果')
|
|
2142
|
-
check('拖拽落点:新增之后落点预览收掉,不会留在画面上', Boolean(dragResult?.cleared), dragResult?.cleared ? 'getDropPreview() 回到 null' : '预览还在')
|
|
2143
|
-
check('拖拽落点:「取消新增」把点从地图上收掉、数据不动', Boolean(cancelResult?.shown) && Boolean(cancelResult?.button) && Boolean(cancelResult?.cleared) && Boolean(cancelResult?.untouched), cancelResult ? `取消按钮 ${cancelResult.button ? '有' : '没有'} / 预览 ${cancelResult.cleared ? '已收' : '还在'}` : '没有第二轮结果')
|
|
2144
|
-
|
|
2088
|
+
// (组件画的落点预览,与落地后的节点同一套画法);松手先显示在地图上,指定属性之后
|
|
2089
|
+
// 「按属性新增」才写进数据、「取消新增」什么都不写;落地后的尺寸与预览里报出的尺寸必须一样
|
|
2090
|
+
await clickTab('拖拽落点')
|
|
2091
|
+
const dragReady = await canvasReady()
|
|
2092
|
+
const paletteItems = [...document.querySelectorAll('.dv-palette__item')]
|
|
2093
|
+
let dragResult = null
|
|
2094
|
+
let cancelResult = null
|
|
2095
|
+
let strayResult = null
|
|
2096
|
+
let ghostHidden = false
|
|
2097
|
+
let ghostNode = null
|
|
2098
|
+
if (dragReady && paletteItems.length === 7) {
|
|
2099
|
+
const dragView = document.querySelector('.dv-demo').__vueParentComponent.setupState.view
|
|
2100
|
+
const pointsBefore = dragView.getData().points.length
|
|
2101
|
+
const canvasNode = document.querySelector('.dv-demo .dv-canvas')
|
|
2102
|
+
const canvasBox = canvasNode.getBoundingClientRect()
|
|
2103
|
+
const transfer = new DataTransfer()
|
|
2104
|
+
// 拖影:看板按钮默认会把整个按钮当拖影(跟着指针的那个「变电站」框)。这里盯着 setDragImage 的入参,
|
|
2105
|
+
// 确认换成了 1×1 的透明元素 —— 屏幕上只剩地图上那个点
|
|
2106
|
+
let ghostCall = null
|
|
2107
|
+
const setDragImage = transfer.setDragImage
|
|
2108
|
+
transfer.setDragImage = (node, x, y) => {
|
|
2109
|
+
ghostCall = { node, x, y }
|
|
2110
|
+
return setDragImage?.call(transfer, node, x, y)
|
|
2111
|
+
}
|
|
2112
|
+
const dragItem = (index, x, y) => {
|
|
2113
|
+
paletteItems[index].dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: transfer }))
|
|
2114
|
+
const init = { bubbles: true, cancelable: true, dataTransfer: transfer, clientX: x, clientY: y }
|
|
2115
|
+
canvasNode.dispatchEvent(new DragEvent('dragover', init))
|
|
2116
|
+
return init
|
|
2117
|
+
}
|
|
2118
|
+
// 第 1 项是 230kV 档:规则给 9px 的圆
|
|
2119
|
+
const dropInit = {
|
|
2120
|
+
bubbles: true,
|
|
2121
|
+
cancelable: true,
|
|
2122
|
+
dataTransfer: transfer,
|
|
2123
|
+
clientX: canvasBox.left + canvasBox.width * 0.3,
|
|
2124
|
+
clientY: canvasBox.top + canvasBox.height * 0.35
|
|
2125
|
+
}
|
|
2126
|
+
dragItem(0, dropInit.clientX, dropInit.clientY)
|
|
2127
|
+
const previewShown = await until(() => dragView.getDropPreview(), 4000)
|
|
2128
|
+
const previewSize = previewShown?.size ?? null
|
|
2129
|
+
const previewSymbol = previewShown?.symbol ?? null
|
|
2130
|
+
ghostNode = document.querySelector('.dv-drag-ghost')
|
|
2131
|
+
ghostHidden = Boolean(ghostCall?.node) && ghostCall.node === ghostNode
|
|
2132
|
+
&& ghostCall.node.getBoundingClientRect().width <= 2
|
|
2133
|
+
&& getComputedStyle(ghostCall.node).opacity === '0'
|
|
2134
|
+
const readoutPreviewed = await until(() => [...document.querySelectorAll('.dv-demo .dv-readout')].some(node => node.textContent.includes('预览图标')), 4000)
|
|
2135
|
+
canvasNode.dispatchEvent(new DragEvent('drop', dropInit))
|
|
2136
|
+
const draftShown = await until(() => dragView.getDropPreview(), 4000)
|
|
2137
|
+
const untouched = dragView.getData().points.length === pointsBefore
|
|
2138
|
+
// 真浏览器在 drop 之后还会给源元素补一条 dragend(拖拽演示里就是看板按钮上绑的那一个)。
|
|
2139
|
+
// 这一条要是顺手把预览收掉,「松手先显示」就只剩一眨眼 —— 这里照着补发一条,核对点还在落点上
|
|
2140
|
+
const draftWorld = draftShown?.world ?? null
|
|
2141
|
+
paletteItems[0].dispatchEvent(new DragEvent('dragend', { bubbles: true, cancelable: true, dataTransfer: transfer }))
|
|
2142
|
+
await wait(200)
|
|
2143
|
+
const afterDragEnd = dragView.getDropPreview()
|
|
2144
|
+
const keptAfterDragEnd = Boolean(afterDragEnd) && Boolean(draftWorld)
|
|
2145
|
+
&& Math.abs(afterDragEnd.world[0] - draftWorld[0]) < 1e-6 && Math.abs(afterDragEnd.world[1] - draftWorld[1]) < 1e-6
|
|
2146
|
+
// 草稿还挂着的时候又拖一项进来(dragover 会先按新的一项画预览),中途松手没落上:
|
|
2147
|
+
// 临时预览收掉、点回到草稿自己的落点上,数据依旧一个字节没动
|
|
2148
|
+
const strayInit = { ...dropInit, clientX: dropInit.clientX + 140, clientY: dropInit.clientY + 80 }
|
|
2149
|
+
dragItem(4, strayInit.clientX, strayInit.clientY)
|
|
2150
|
+
// 预览合并到下一帧才算:等它真的挪到新落点上(还留着草稿的那一份就直接算没挪)
|
|
2151
|
+
const strayShown = await until(() => {
|
|
2152
|
+
const shown = dragView.getDropPreview()
|
|
2153
|
+
return shown && draftWorld && (Math.abs(shown.world[0] - draftWorld[0]) > 1e-6 || Math.abs(shown.world[1] - draftWorld[1]) > 1e-6) ? shown : null
|
|
2154
|
+
}, 4000)
|
|
2155
|
+
const strayMoved = Boolean(strayShown)
|
|
2156
|
+
paletteItems[4].dispatchEvent(new DragEvent('dragend', { bubbles: true, cancelable: true, dataTransfer: transfer }))
|
|
2157
|
+
await wait(200)
|
|
2158
|
+
const restored = dragView.getDropPreview()
|
|
2159
|
+
strayResult = {
|
|
2160
|
+
moved: strayMoved,
|
|
2161
|
+
restored: Boolean(restored) && Boolean(draftWorld)
|
|
2162
|
+
&& Math.abs(restored.world[0] - draftWorld[0]) < 1e-6 && Math.abs(restored.world[1] - draftWorld[1]) < 1e-6,
|
|
2163
|
+
untouched: dragView.getData().points.length === pointsBefore
|
|
2164
|
+
}
|
|
2165
|
+
// 面板上指定属性:改成 525kV(规则给 13px),预览与读数必须跟着重画
|
|
2166
|
+
const kvSelect = [...document.querySelectorAll('.dv-demo select')].find(node => node.textContent.includes('525 kV'))
|
|
2167
|
+
if (kvSelect) {
|
|
2168
|
+
kvSelect.value = '525'
|
|
2169
|
+
kvSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
2170
|
+
}
|
|
2171
|
+
const resized = await until(() => (dragView.getDropPreview()?.size ?? 0) === 13, 4000)
|
|
2172
|
+
const specifiedSize = dragView.getDropPreview()?.size ?? null
|
|
2173
|
+
check('拖拽落点:dragover 期间组件就解析出落点图标(尺寸 / 符号 / 颜色都有读数)', Boolean(previewShown) && Number(previewSize) > 0 && Boolean(previewSymbol), previewShown ? `${previewSize}px / ${previewSymbol} / ${previewShown.color}` : '没有落点预览')
|
|
2174
|
+
check('拖拽落点:预览读数写进面板(不是照着样子仿的)', Boolean(readoutPreviewed), readoutPreviewed ? '读数里出现「预览图标」' : '读数里没有预览尺寸')
|
|
2175
|
+
document.querySelector('.dv-demo .dv-confirm .dv-button--primary')?.click()
|
|
2176
|
+
const landed = await until(() => dragView.getData().points.length === pointsBefore + 1, 4000)
|
|
2177
|
+
const landedItem = landed ? dragView.getData().points.at(-1) : null
|
|
2178
|
+
// 落地后的那一份:同一条规则链解析出来的尺寸与符号必须与预览一致,属性就是面板上指定的那一个
|
|
2179
|
+
const landedStyle = landed ? dragView.getPointStyle(pointsBefore) : null
|
|
2180
|
+
const sameAsPreview = Boolean(landedStyle) && landedStyle.size === specifiedSize && previewSymbol === 'circle' && landedItem?.symbol === 'circle'
|
|
2181
|
+
const specified = Boolean(kvSelect) && Number(landedItem?.vn_kv) === 525 && Boolean(resized)
|
|
2182
|
+
const cleared = landed ? dragView.getDropPreview() === null : false
|
|
2183
|
+
dragResult = { before: pointsBefore, draft: Boolean(draftShown), untouched, keptAfterDragEnd, landed: Boolean(landed), sameAsPreview, specified, size: specifiedSize, cleared, after: dragView.getData().points.length }
|
|
2184
|
+
// 第二轮:拖一个进去之后点「取消新增」—— 地图上的点收掉,数据一个字节没动
|
|
2185
|
+
const cancelBefore = dragView.getData().points.length
|
|
2186
|
+
const cancelX = dropInit.clientX + 24
|
|
2187
|
+
const cancelY = dropInit.clientY + 24
|
|
2188
|
+
dragItem(1, cancelX, cancelY)
|
|
2189
|
+
await until(() => dragView.getDropPreview(), 4000)
|
|
2190
|
+
canvasNode.dispatchEvent(new DragEvent('drop', { ...dropInit, clientX: cancelX, clientY: cancelY }))
|
|
2191
|
+
// 这一段同样要过一遍 dragend:取消新增之前,点必须一直留在地图上
|
|
2192
|
+
paletteItems[1].dispatchEvent(new DragEvent('dragend', { bubbles: true, cancelable: true, dataTransfer: transfer }))
|
|
2193
|
+
const cancelShown = await until(() => dragView.getDropPreview(), 4000)
|
|
2194
|
+
const cancelButton = [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.trim() === '取消新增')
|
|
2195
|
+
cancelButton?.click()
|
|
2196
|
+
await wait(200)
|
|
2197
|
+
cancelResult = { shown: Boolean(cancelShown), button: Boolean(cancelButton), cleared: dragView.getDropPreview() === null, untouched: dragView.getData().points.length === cancelBefore }
|
|
2198
|
+
}
|
|
2199
|
+
check('拖拽落点:拖拽期间不出现浏览器默认的看板拖影(只剩地图上的点)', Boolean(ghostNode) && ghostHidden, ghostHidden ? 'setDragImage 指向 1×1 透明节点' : '拖影还是看板按钮本身')
|
|
2200
|
+
check('拖拽落点:松手之后预览不收起,先显示在地图上、数据一个字节没动', Boolean(dragResult?.draft) && Boolean(dragResult?.untouched), dragResult ? `点 ${dragResult.before} -> ${dragResult.after}` : `看板 ${paletteItems.length} 项 / 画布 ${Boolean(dragReady)}`)
|
|
2201
|
+
check('拖拽落点:drop 之后那条 dragend 不会把原来拖动的点收掉(点一直留在落点上)', Boolean(dragResult?.keptAfterDragEnd), dragResult?.keptAfterDragEnd ? 'dragend 之后点还在,世界坐标没挪' : 'dragend 之后预览被收掉了')
|
|
2202
|
+
check('拖拽落点:草稿挂着时又拖一项进来、中途松手 —— 点回到落点,数据不动', Boolean(strayResult?.moved) && Boolean(strayResult?.restored) && Boolean(strayResult?.untouched), `临时预览 ${strayResult?.moved ? '挪走了' : '没挪'} / 回到落点 ${strayResult?.restored ? '是' : '否'} / 数据 ${strayResult?.untouched ? '没动' : '动了'}`)
|
|
2203
|
+
check('拖拽落点:面板指定属性(525kV)之后预览跟着重画', Boolean(dragResult?.size) && dragResult.size === 13, dragResult ? `指定之后 ${dragResult.size}px` : '没有拖放结果')
|
|
2204
|
+
check('拖拽落点:点「按属性新增」才写进数据,写进去的就是指定的那一份属性', Boolean(dragResult?.landed) && Boolean(dragResult?.specified), dragResult ? `确认后 ${dragResult.after} 个节点 / vn_kv=525` : '没有拖放结果')
|
|
2205
|
+
check('拖拽落点:落地后的图标尺寸 / 符号与预览完全一致(规则链同一条)', Boolean(dragResult?.sameAsPreview), dragResult ? `预览与落地 ${dragResult.sameAsPreview ? '一致' : '不一致'}` : '没有拖放结果')
|
|
2206
|
+
check('拖拽落点:新增之后落点预览收掉,不会留在画面上', Boolean(dragResult?.cleared), dragResult?.cleared ? 'getDropPreview() 回到 null' : '预览还在')
|
|
2207
|
+
check('拖拽落点:「取消新增」把点从地图上收掉、数据不动', Boolean(cancelResult?.shown) && Boolean(cancelResult?.button) && Boolean(cancelResult?.cleared) && Boolean(cancelResult?.untouched), cancelResult ? `取消按钮 ${cancelResult.button ? '有' : '没有'} / 预览 ${cancelResult.cleared ? '已收' : '还在'}` : '没有第二轮结果')
|
|
2208
|
+
|
|
2145
2209
|
// 2.2 定向改法与存档回灌:微调按钮走 updatePoint,重建之后数量与标签状态都还在
|
|
2146
|
-
let tweakResult = null
|
|
2147
|
-
if (dragResult?.landed) {
|
|
2148
|
-
const tweakView = document.querySelector('.dv-demo').__vueParentComponent.setupState.view
|
|
2149
|
-
const panelButton = text => [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.trim() === text)
|
|
2150
|
-
// 落点节点数据里没写 size(尺寸由规则给的 9px),所以「改之前」按样式快照读 —— 与预览读数同一个数
|
|
2151
|
-
const landedIndex = tweakView.getData().points.length - 1
|
|
2152
|
-
const sizeBefore = Number(tweakView.getData().points.at(-1).size) || Number(tweakView.getPointStyle(landedIndex)?.size)
|
|
2153
|
-
panelButton('大一点')?.click()
|
|
2154
|
-
await wait(260)
|
|
2155
|
-
const sizeAfter = Number(tweakView.getData().points.at(-1).size)
|
|
2156
|
-
panelButton('换图标')?.click()
|
|
2157
|
-
await wait(260)
|
|
2158
|
-
const symbol = tweakView.getData().points.at(-1).symbol
|
|
2159
|
-
// 先拖一条标签上去,让 labelStates 有内容 —— 不然「重建之后标签状态还在」是 0 == 0
|
|
2160
|
-
const labelTransfer = new DataTransfer()
|
|
2161
|
-
paletteItems[6].dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: labelTransfer }))
|
|
2162
|
-
// 2.1 里的画布节点只在那一段的作用域里,这里重新量一次
|
|
2163
|
-
const labelCanvas = document.querySelector('.dv-demo .dv-canvas')
|
|
2164
|
-
const labelBox = labelCanvas.getBoundingClientRect()
|
|
2165
|
-
const labelInit = {
|
|
2166
|
-
bubbles: true,
|
|
2167
|
-
cancelable: true,
|
|
2168
|
-
dataTransfer: labelTransfer,
|
|
2169
|
-
clientX: labelBox.left + labelBox.width * 0.3,
|
|
2170
|
-
clientY: labelBox.top + labelBox.height * 0.35
|
|
2171
|
-
}
|
|
2172
|
-
labelCanvas.dispatchEvent(new DragEvent('dragover', labelInit))
|
|
2173
|
-
labelCanvas.dispatchEvent(new DragEvent('drop', labelInit))
|
|
2174
|
-
await wait(260)
|
|
2175
|
-
document.querySelector('.dv-demo .dv-confirm .dv-button--primary')?.click()
|
|
2176
|
-
await wait(320)
|
|
2177
|
-
const countsBefore = tweakView.getData().points.length
|
|
2178
|
-
const labelsBefore = tweakView.getLabelStates().length
|
|
2179
|
-
panelButton('存档 → 重建')?.click()
|
|
2180
|
-
await wait(900)
|
|
2181
|
-
const rebuiltView = document.querySelector('.dv-demo').__vueParentComponent.setupState.view
|
|
2182
|
-
tweakResult = {
|
|
2183
|
-
sizeBefore,
|
|
2184
|
-
sizeAfter,
|
|
2185
|
-
symbol: typeof symbol === 'string' ? symbol : symbol?.type,
|
|
2186
|
-
countsBefore,
|
|
2187
|
-
countsAfter: rebuiltView.getData().points.length,
|
|
2188
|
-
labelStates: rebuiltView.getLabelStates().length,
|
|
2189
|
-
labelsBefore
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2210
|
+
let tweakResult = null
|
|
2211
|
+
if (dragResult?.landed) {
|
|
2212
|
+
const tweakView = document.querySelector('.dv-demo').__vueParentComponent.setupState.view
|
|
2213
|
+
const panelButton = text => [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.trim() === text)
|
|
2214
|
+
// 落点节点数据里没写 size(尺寸由规则给的 9px),所以「改之前」按样式快照读 —— 与预览读数同一个数
|
|
2215
|
+
const landedIndex = tweakView.getData().points.length - 1
|
|
2216
|
+
const sizeBefore = Number(tweakView.getData().points.at(-1).size) || Number(tweakView.getPointStyle(landedIndex)?.size)
|
|
2217
|
+
panelButton('大一点')?.click()
|
|
2218
|
+
await wait(260)
|
|
2219
|
+
const sizeAfter = Number(tweakView.getData().points.at(-1).size)
|
|
2220
|
+
panelButton('换图标')?.click()
|
|
2221
|
+
await wait(260)
|
|
2222
|
+
const symbol = tweakView.getData().points.at(-1).symbol
|
|
2223
|
+
// 先拖一条标签上去,让 labelStates 有内容 —— 不然「重建之后标签状态还在」是 0 == 0
|
|
2224
|
+
const labelTransfer = new DataTransfer()
|
|
2225
|
+
paletteItems[6].dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: labelTransfer }))
|
|
2226
|
+
// 2.1 里的画布节点只在那一段的作用域里,这里重新量一次
|
|
2227
|
+
const labelCanvas = document.querySelector('.dv-demo .dv-canvas')
|
|
2228
|
+
const labelBox = labelCanvas.getBoundingClientRect()
|
|
2229
|
+
const labelInit = {
|
|
2230
|
+
bubbles: true,
|
|
2231
|
+
cancelable: true,
|
|
2232
|
+
dataTransfer: labelTransfer,
|
|
2233
|
+
clientX: labelBox.left + labelBox.width * 0.3,
|
|
2234
|
+
clientY: labelBox.top + labelBox.height * 0.35
|
|
2235
|
+
}
|
|
2236
|
+
labelCanvas.dispatchEvent(new DragEvent('dragover', labelInit))
|
|
2237
|
+
labelCanvas.dispatchEvent(new DragEvent('drop', labelInit))
|
|
2238
|
+
await wait(260)
|
|
2239
|
+
document.querySelector('.dv-demo .dv-confirm .dv-button--primary')?.click()
|
|
2240
|
+
await wait(320)
|
|
2241
|
+
const countsBefore = tweakView.getData().points.length
|
|
2242
|
+
const labelsBefore = tweakView.getLabelStates().length
|
|
2243
|
+
panelButton('存档 → 重建')?.click()
|
|
2244
|
+
await wait(900)
|
|
2245
|
+
const rebuiltView = document.querySelector('.dv-demo').__vueParentComponent.setupState.view
|
|
2246
|
+
tweakResult = {
|
|
2247
|
+
sizeBefore,
|
|
2248
|
+
sizeAfter,
|
|
2249
|
+
symbol: typeof symbol === 'string' ? symbol : symbol?.type,
|
|
2250
|
+
countsBefore,
|
|
2251
|
+
countsAfter: rebuiltView.getData().points.length,
|
|
2252
|
+
labelStates: rebuiltView.getLabelStates().length,
|
|
2253
|
+
labelsBefore
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2192
2256
|
check('拖拽落点:微调按钮走 updatePoint 写回数据', Boolean(tweakResult) && tweakResult.sizeAfter === tweakResult.sizeBefore + 4 && tweakResult.symbol === 'square', tweakResult ? `size ${tweakResult.sizeBefore} -> ${tweakResult.sizeAfter} / symbol ${tweakResult.symbol}` : '没有可微调的目标')
|
|
2193
|
-
check('拖拽落点:「存档 → 重建」走 input 口径 + labelStates,数量与标签状态都回显', Boolean(tweakResult) && tweakResult.labelsBefore >= 1 && tweakResult.countsAfter === tweakResult.countsBefore && tweakResult.labelStates === tweakResult.labelsBefore, tweakResult ? `点 ${tweakResult.countsBefore} -> ${tweakResult.countsAfter} / 标签状态 ${tweakResult.labelStates} 条(重建前 ${tweakResult.labelsBefore})` : '没有重建读数')
|
|
2194
|
-
|
|
2195
|
-
// 3. 回到标签演示,逐条验这一轮新增的交互
|
|
2196
|
-
await clickTab('标签交互')
|
|
2257
|
+
check('拖拽落点:「存档 → 重建」走 input 口径 + labelStates,数量与标签状态都回显', Boolean(tweakResult) && tweakResult.labelsBefore >= 1 && tweakResult.countsAfter === tweakResult.countsBefore && tweakResult.labelStates === tweakResult.labelsBefore, tweakResult ? `点 ${tweakResult.countsBefore} -> ${tweakResult.countsAfter} / 标签状态 ${tweakResult.labelStates} 条(重建前 ${tweakResult.labelsBefore})` : '没有重建读数')
|
|
2258
|
+
|
|
2259
|
+
// 3. 回到标签演示,逐条验这一轮新增的交互
|
|
2260
|
+
await clickTab('标签交互')
|
|
2197
2261
|
await canvasReady()
|
|
2198
2262
|
await until(() => document.querySelectorAll('.dv-demo .gis-view__label').length > 0, 15000)
|
|
2199
2263
|
const labelNodes = () => [...document.querySelectorAll('.dv-demo .gis-view__label')]
|
|
@@ -2217,365 +2281,365 @@
|
|
|
2217
2281
|
`A=${editablePerItem?.className ?? '没有'} / B=${lockedPerItem?.className ?? '没有'} / 当前 ${labelNodes().map(node => `${node.dataset.labelKind}:${node.dataset.labelIndex}:${node.dataset.labelLabel}:${node.textContent}`).join(' | ')} / ${document.querySelector('[data-demo="label-per-item-readout"]')?.textContent.trim() ?? ''}`)
|
|
2218
2282
|
// 3.0 一键换标签内容:setLabelContent 只换全局 label / lineLabel 的 content(节点 + 线路一起)
|
|
2219
2283
|
const overlapHideButton = document.querySelector('[data-demo="label-overlap-hide"]')
|
|
2220
|
-
overlapHideButton?.click()
|
|
2221
|
-
await wait(220)
|
|
2222
|
-
const hideOverlapOptions = labelView()?.getOptions()
|
|
2223
|
-
check('标签演示:「碰撞直接隐藏」把节点 / 线路标签一起切到 overlapMode: hide',
|
|
2224
|
-
hideOverlapOptions?.label?.overlapMode === 'hide' && hideOverlapOptions?.lineLabel?.overlapMode === 'hide' &&
|
|
2225
|
-
overlapHideButton?.classList.contains('dv-chip--active') === true,
|
|
2226
|
-
`${hideOverlapOptions?.label?.overlapMode} / ${hideOverlapOptions?.lineLabel?.overlapMode}`)
|
|
2227
|
-
const overlapShiftButton = document.querySelector('[data-demo="label-overlap-shift"]')
|
|
2228
|
-
overlapShiftButton?.click()
|
|
2229
|
-
await wait(220)
|
|
2230
|
-
const shiftOverlapOptions = labelView()?.getOptions()
|
|
2231
|
-
check('标签演示:「主动挪标签」切回 overlapMode: shift(默认避让策略)',
|
|
2232
|
-
shiftOverlapOptions?.label?.overlapMode === 'shift' && shiftOverlapOptions?.lineLabel?.overlapMode === 'shift' &&
|
|
2233
|
-
overlapShiftButton?.classList.contains('dv-chip--active') === true,
|
|
2234
|
-
`${shiftOverlapOptions?.label?.overlapMode} / ${shiftOverlapOptions?.lineLabel?.overlapMode}`)
|
|
2235
|
-
const contentSelect = document.querySelector('[data-demo=label-content-field]')
|
|
2236
|
-
contentSelect.value = '{vn_kv} kV'
|
|
2237
|
-
contentSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
2238
|
-
await wait(500)
|
|
2239
|
-
const optionsAfter = labelView()?.getOptions() ?? null
|
|
2240
|
-
const voltageShown = [...document.querySelectorAll('.dv-demo .gis-view__label')].some(node => node.textContent.includes('kV'))
|
|
2241
|
-
const contentReadout = document.querySelector('[data-demo=label-content-text]')?.textContent.trim() ?? ''
|
|
2242
|
-
check('标签演示:setLabelContent 一键把节点 + 线路标签换成电压等级(位置 / 样式保持)', optionsAfter?.label?.content === '{vn_kv} kV' && optionsAfter?.lineLabel?.content === '{vn_kv} kV' && voltageShown && contentReadout.includes('{vn_kv} kV'), `${optionsAfter?.label?.content} / ${optionsAfter?.lineLabel?.content} / 画面上 ${voltageShown ? '有' : '没有'} kV`)
|
|
2243
|
-
contentSelect.value = '{name}'
|
|
2244
|
-
contentSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
2245
|
-
await wait(300)
|
|
2246
|
-
|
|
2284
|
+
overlapHideButton?.click()
|
|
2285
|
+
await wait(220)
|
|
2286
|
+
const hideOverlapOptions = labelView()?.getOptions()
|
|
2287
|
+
check('标签演示:「碰撞直接隐藏」把节点 / 线路标签一起切到 overlapMode: hide',
|
|
2288
|
+
hideOverlapOptions?.label?.overlapMode === 'hide' && hideOverlapOptions?.lineLabel?.overlapMode === 'hide' &&
|
|
2289
|
+
overlapHideButton?.classList.contains('dv-chip--active') === true,
|
|
2290
|
+
`${hideOverlapOptions?.label?.overlapMode} / ${hideOverlapOptions?.lineLabel?.overlapMode}`)
|
|
2291
|
+
const overlapShiftButton = document.querySelector('[data-demo="label-overlap-shift"]')
|
|
2292
|
+
overlapShiftButton?.click()
|
|
2293
|
+
await wait(220)
|
|
2294
|
+
const shiftOverlapOptions = labelView()?.getOptions()
|
|
2295
|
+
check('标签演示:「主动挪标签」切回 overlapMode: shift(默认避让策略)',
|
|
2296
|
+
shiftOverlapOptions?.label?.overlapMode === 'shift' && shiftOverlapOptions?.lineLabel?.overlapMode === 'shift' &&
|
|
2297
|
+
overlapShiftButton?.classList.contains('dv-chip--active') === true,
|
|
2298
|
+
`${shiftOverlapOptions?.label?.overlapMode} / ${shiftOverlapOptions?.lineLabel?.overlapMode}`)
|
|
2299
|
+
const contentSelect = document.querySelector('[data-demo=label-content-field]')
|
|
2300
|
+
contentSelect.value = '{vn_kv} kV'
|
|
2301
|
+
contentSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
2302
|
+
await wait(500)
|
|
2303
|
+
const optionsAfter = labelView()?.getOptions() ?? null
|
|
2304
|
+
const voltageShown = [...document.querySelectorAll('.dv-demo .gis-view__label')].some(node => node.textContent.includes('kV'))
|
|
2305
|
+
const contentReadout = document.querySelector('[data-demo=label-content-text]')?.textContent.trim() ?? ''
|
|
2306
|
+
check('标签演示:setLabelContent 一键把节点 + 线路标签换成电压等级(位置 / 样式保持)', optionsAfter?.label?.content === '{vn_kv} kV' && optionsAfter?.lineLabel?.content === '{vn_kv} kV' && voltageShown && contentReadout.includes('{vn_kv} kV'), `${optionsAfter?.label?.content} / ${optionsAfter?.lineLabel?.content} / 画面上 ${voltageShown ? '有' : '没有'} kV`)
|
|
2307
|
+
contentSelect.value = '{name}'
|
|
2308
|
+
contentSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
2309
|
+
await wait(300)
|
|
2310
|
+
|
|
2247
2311
|
// 3.1 点一下标签 → labelClick
|
|
2248
2312
|
const target = labelNodes().find(node => node.classList.contains('gis-view__label--editable')) ?? labelNodes()[0]
|
|
2249
|
-
tap(target)
|
|
2250
|
-
const clickLogged = await until(() => logText().includes('labelClick'), 8000)
|
|
2251
|
-
check('点一下标签发 labelClick,日志记下「哪一条、内容是什么」', Boolean(clickLogged), logText().slice(0, 160))
|
|
2252
|
-
const editReadout = document.querySelector('.dv-demo .dv-input')?.closest('.dv-group')?.querySelector('.dv-readout')?.textContent?.trim() ?? ''
|
|
2253
|
-
check('点完之后编辑面板认到了这一条', !editReadout.includes('点一下标签') && editReadout.length > 0, editReadout)
|
|
2254
|
-
|
|
2255
|
-
// 3.2 右键标签 → 自绘菜单(编辑 / 换位置 / 重置 / 隐藏 / 删除)
|
|
2256
|
-
const rect = target.getBoundingClientRect()
|
|
2257
|
-
const menuEvent = contextMenuOn(target, rect.left + 4, rect.top + 4)
|
|
2258
|
-
check('右键标签的系统菜单被拦下(preventContextMenu 缺省开)', menuEvent.defaultPrevented, `defaultPrevented=${menuEvent.defaultPrevented}`)
|
|
2259
|
-
const menu = await until(() => document.querySelector('.dv-menu'), 6000)
|
|
2260
|
-
check('右键弹出演示自己的菜单', Boolean(menu), menu ? [...menu.querySelectorAll('button')].map(node => node.textContent.trim()).join(' / ') : '没弹出')
|
|
2261
|
-
const items = menu ? [...menu.querySelectorAll('button')].map(node => node.textContent.trim()) : []
|
|
2262
|
-
check('菜单里有改内容 / 换位置 / 重置偏移 / 隐藏 / 删除这五项', ['改内容', '换位置', '重置偏移', '删除这一条'].every(text => items.some(item => item.includes(text))), items.join(' / '))
|
|
2263
|
-
const positionButton = menu ? [...menu.querySelectorAll('button')].find(node => node.textContent.includes('换位置')) : null
|
|
2264
|
-
positionButton?.click()
|
|
2265
|
-
await wait(260)
|
|
2266
|
-
check('菜单「换位置」落到 updateLabel 上(日志看得到)', logText().includes('updateLabel'), logText().slice(0, 160))
|
|
2267
|
-
check('点完菜单项之后菜单收起来了', !document.querySelector('.dv-menu'), document.querySelector('.dv-menu') ? '还开着' : '已收')
|
|
2268
|
-
// 3.3 删除一条:自绘菜单的删除落到 removeLabel / updateLabel(hidden)
|
|
2269
|
-
const beforeDelete = labelNodes()
|
|
2270
|
-
const labelForDelete = beforeDelete[beforeDelete.length - 1]
|
|
2271
|
-
// 按内容认「删的是哪一条」:避让是「没位置就不画」,删掉一条腾出来的小格会让别的候选补位,
|
|
2272
|
-
// 所以这里盯的是「被删的那一条不再画」,不是「总数不涨」(13 → 14 是正常的补位)
|
|
2273
|
-
const deletedLabelText = labelForDelete.textContent.trim()
|
|
2274
|
-
contextMenuOn(labelForDelete, labelForDelete.getBoundingClientRect().left + 4, labelForDelete.getBoundingClientRect().top + 4)
|
|
2275
|
-
const deleteMenu = await until(() => document.querySelector('.dv-menu'), 6000)
|
|
2276
|
-
const deleteButton = deleteMenu ? [...deleteMenu.querySelectorAll('button')].find(node => node.textContent.includes('删除这一条')) : null
|
|
2277
|
-
deleteButton?.click()
|
|
2278
|
-
await wait(400)
|
|
2279
|
-
check('菜单「删除这一条」记到了日志里', logText().includes('removeLabel') || logText().includes('updateLabel'), logText().slice(0, 160))
|
|
2280
|
-
const remainingLabels = labelNodes()
|
|
2281
|
-
check('删完之后被删的那一条不再画(腾出的位置允许别的标签补位)', deletedLabelText !== '' && !remainingLabels.some(node => node.textContent.trim() === deletedLabelText), `${deletedLabelText}|${beforeDelete.length} → ${remainingLabels.length}`)
|
|
2282
|
-
|
|
2283
|
-
// 3.4 批量 API:给全部节点加一条
|
|
2284
|
-
const addAllButton = [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.includes('给全部加一条'))
|
|
2285
|
-
addAllButton?.click()
|
|
2286
|
-
await wait(500)
|
|
2287
|
-
check('「给全部加一条」走 addLabels,日志里有新增计数', logText().includes('addLabels'), logText().slice(0, 160))
|
|
2288
|
-
check('labelChange 也跟着发(addAll)', logText().includes('addAll'), logText().slice(0, 200))
|
|
2289
|
-
|
|
2290
|
-
// 3.5 点选即加:先选中第一个节点,再让「给选中加一条」把它加上
|
|
2291
|
-
const selectButton = [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.includes('选中第一个节点'))
|
|
2292
|
-
selectButton?.click()
|
|
2293
|
-
await wait(400)
|
|
2294
|
-
const selectionReadout = [...document.querySelectorAll('.dv-demo .dv-readout')].map(node => node.textContent.trim()).find(text => text.includes('选中'))
|
|
2295
|
-
check('「选中第一个节点」让面板报出选中集', Boolean(selectionReadout) && !selectionReadout.includes('未选中'), selectionReadout ?? '没有选中读数')
|
|
2296
|
-
const preview = [...document.querySelectorAll('.dv-demo .dv-hint')].map(node => node.textContent.trim()).find(text => text.includes('本次会加到'))
|
|
2297
|
-
check('选中集预览用 resolveLabelTargets 算出「会加到几条」', Boolean(preview), preview ?? '没有预览读数')
|
|
2298
|
-
const addSelectionButton = [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.includes('给选中加一条'))
|
|
2299
|
-
check('「给选中加一条」选中集非空时可用', Boolean(addSelectionButton) && !addSelectionButton.disabled, addSelectionButton ? `disabled=${addSelectionButton.disabled}` : '没找到按钮')
|
|
2300
|
-
addSelectionButton?.click()
|
|
2301
|
-
await wait(500)
|
|
2302
|
-
check('给选中集加标签走 addLabels(..., \'selection\', ...)', logText().includes('选中集:新增') || logText().includes('addLabels'), logText().slice(0, 200))
|
|
2303
|
-
// 3.5.1 连点两次:加出来的标签默认要跟别的标签避让,压在一起就整条不画 ——
|
|
2304
|
-
// 面板上看着像「只有第一次加上了」,这里盯住「每一次都多画一条」
|
|
2305
|
-
const noteCount = () => [...document.querySelectorAll('.dv-demo .gis-view__label')].filter(node => node.textContent.includes('备注#')).length
|
|
2306
|
-
const notesBefore = noteCount()
|
|
2307
|
-
addSelectionButton?.click()
|
|
2308
|
-
await wait(400)
|
|
2309
|
-
addSelectionButton?.click()
|
|
2310
|
-
await wait(400)
|
|
2311
|
-
const notesAfter = noteCount()
|
|
2312
|
-
check('连点「给选中加一条」每次都能多画出一条(不只第一次有效)', notesAfter >= notesBefore + 2, `备注标签 ${notesBefore} → ${notesAfter}`)
|
|
2313
|
-
|
|
2314
|
-
// 3.6 线路标签 + 整条通道:切到线路标签再加
|
|
2315
|
-
const lineTab = [...document.querySelectorAll('.dv-demo .dv-chip')].find(node => node.textContent.includes('线路标签'))
|
|
2316
|
-
lineTab?.click()
|
|
2317
|
-
await wait(200)
|
|
2318
|
-
const channelButton = [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.includes('整条通道加'))
|
|
2319
|
-
channelButton?.click()
|
|
2320
|
-
await wait(500)
|
|
2321
|
-
check('「整条通道加」用 { channel } 一次给整条通道的回线加标签', logText().includes('通道'), logText().slice(0, 200))
|
|
2322
|
-
|
|
2323
|
-
// 3.7 双击标签:重置偏移
|
|
2324
|
-
const dblTarget = labelNodes()[0]
|
|
2325
|
-
if (dblTarget) {
|
|
2326
|
-
const dblInit = { bubbles: true, cancelable: true, composed: true, clientX: dblTarget.getBoundingClientRect().left + 2, clientY: dblTarget.getBoundingClientRect().top + 2 }
|
|
2327
|
-
dblTarget.dispatchEvent(new MouseEvent('dblclick', dblInit))
|
|
2328
|
-
await wait(300)
|
|
2329
|
-
check('双击标签发 labelDblClick(这里接成重置偏移)', logText().includes('labelDblClick') || logText().includes('重置偏移'), logText().slice(0, 160))
|
|
2330
|
-
}
|
|
2331
|
-
else {
|
|
2332
|
-
check('双击标签发 labelDblClick(这里接成重置偏移)', false, '没有标签节点可点')
|
|
2333
|
-
}
|
|
2334
|
-
|
|
2335
|
-
// 3.8 标签显示:动态切节点 / 线路标签内容(setLabelContent 的独立演示)
|
|
2336
|
-
if (await clickTab('标签显示')) {
|
|
2337
|
-
const ready = await canvasReady()
|
|
2338
|
-
const demo = await until(() => {
|
|
2339
|
-
const node = document.querySelector('.dv-demo')
|
|
2340
|
-
return node?.textContent.includes('改哪一类标签') ? node : null
|
|
2341
|
-
}, 10000)
|
|
2342
|
-
const setup = demo?.__vueParentComponent?.setupState
|
|
2343
|
-
const view = await until(() => setup?.view, 8000)
|
|
2344
|
-
const countLayer = layer => demo?.querySelectorAll(`.gis-view__label-layer--${layer} .gis-view__label`).length ?? 0
|
|
2345
|
-
const labelsShown = await until(() => countLayer('point') > 0 && countLayer('line') > 0, 12000)
|
|
2346
|
-
const click = key => demo?.querySelector(`[data-demo="${key}"]`)?.click()
|
|
2347
|
-
const readout = key => demo?.querySelector(`[data-demo="${key}"]`)?.textContent.trim() ?? ''
|
|
2348
|
-
if (ready && demo && view) {
|
|
2349
|
-
const initial = view.getOptions()
|
|
2350
|
-
const countReady = await until(() => readout('label-display-count').includes('画面上的标签'), 5000)
|
|
2351
|
-
check('标签显示:节点与线路标签都画出来,初始内容由 label / lineLabel 的同一套模板控制',
|
|
2352
|
-
Boolean(labelsShown) && Boolean(countReady) && initial.label?.content === '{name} · {vn_kv} kV' && initial.lineLabel?.content === '{name} · {vn_kv} kV',
|
|
2353
|
-
`节点 ${countLayer('point')} / 线路 ${countLayer('line')} · ${readout('label-display-count')}`)
|
|
2354
|
-
|
|
2355
|
-
click('label-display-target-point')
|
|
2356
|
-
await wait(80)
|
|
2357
|
-
click('label-display-preset-voltage')
|
|
2358
|
-
const pointOnly = await until(() => {
|
|
2359
|
-
const options = view.getOptions()
|
|
2360
|
-
return options.label?.content === '{vn_kv} kV' && options.lineLabel?.content === '{name} · {vn_kv} kV'
|
|
2361
|
-
}, 5000)
|
|
2362
|
-
check('标签显示:只改节点时 setLabelContent 的 kind=point 不影响线路标签', Boolean(pointOnly), readout('label-display-current'))
|
|
2363
|
-
|
|
2364
|
-
click('label-display-target-both')
|
|
2365
|
-
await wait(80)
|
|
2366
|
-
click('label-display-preset-name-voltage')
|
|
2367
|
-
const bothChanged = await until(() => {
|
|
2368
|
-
const options = view.getOptions()
|
|
2369
|
-
return options.label?.content === '{name} · {vn_kv} kV' && options.lineLabel?.content === '{name} · {vn_kv} kV'
|
|
2370
|
-
}, 5000)
|
|
2371
|
-
check('标签显示:切回节点 + 线路后,一份模板同时落到两类标签上', Boolean(bothChanged) && readout('label-display-action').includes('节点 + 线路'), readout('label-display-action'))
|
|
2372
|
-
|
|
2373
|
-
const customInput = demo.querySelector('[data-demo="label-display-custom-input"]')
|
|
2374
|
-
const customText = '自定义 {name} · {vn_kv}'
|
|
2375
|
-
if (customInput) {
|
|
2376
|
-
customInput.value = customText
|
|
2377
|
-
customInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
2378
|
-
}
|
|
2379
|
-
click('label-display-apply-custom')
|
|
2380
|
-
const customApplied = await until(() => {
|
|
2381
|
-
const options = view.getOptions()
|
|
2382
|
-
return options.label?.content === customText && options.lineLabel?.content === customText
|
|
2383
|
-
}, 5000)
|
|
2384
|
-
check('标签显示:自定义模板原样写进节点与线路的 label.content', Boolean(customApplied), readout('label-display-current'))
|
|
2385
|
-
|
|
2386
|
-
click('label-display-auto')
|
|
2387
|
-
const autoStarted = await until(() => readout('label-display-action').includes('自动轮播开始'), 3000)
|
|
2388
|
-
const autoRotated = await until(() => view.getOptions().label?.content !== customText, 4500)
|
|
2389
|
-
click('label-display-auto')
|
|
2390
|
-
const autoStopped = await until(() => readout('label-display-action').includes('已停止'), 3000)
|
|
2391
|
-
check('标签显示:自动轮播按 1.8 秒换模板,按钮再点一次能停', Boolean(autoStarted) && Boolean(autoRotated) && Boolean(autoStopped), `${readout('label-display-action')} / 当前 ${view.getOptions().label?.content}`)
|
|
2392
|
-
}
|
|
2393
|
-
else {
|
|
2394
|
-
check('标签显示:演示页与实例都起来了', false, ready ? '没有 demo / setupState.view' : '没有 canvas')
|
|
2395
|
-
}
|
|
2396
|
-
}
|
|
2397
|
-
|
|
2398
|
-
// 3.9 悬停与提示:hover 样式、三种 tooltip 内容写法、悬停中刷新内容
|
|
2399
|
-
if (await clickTab('悬停与提示')) {
|
|
2400
|
-
const ready = await canvasReady()
|
|
2401
|
-
const demo = await until(() => {
|
|
2402
|
-
const node = document.querySelector('.dv-demo')
|
|
2403
|
-
return node?.textContent.includes('悬停样式') && node.textContent.includes('提示内容') ? node : null
|
|
2404
|
-
}, 10000)
|
|
2405
|
-
const setup = demo?.__vueParentComponent?.setupState
|
|
2406
|
-
const view = await until(() => setup?.view, 8000)
|
|
2407
|
-
await wait(260)
|
|
2408
|
-
const click = key => demo?.querySelector(`[data-demo="${key}"]`)?.click()
|
|
2409
|
-
const readout = key => demo?.querySelector(`[data-demo="${key}"]`)?.textContent.trim() ?? ''
|
|
2410
|
-
const tooltipNode = () => demo?.querySelector('.gis-view__tooltip') ?? null
|
|
2411
|
-
const tooltipContent = () => {
|
|
2412
|
-
const node = tooltipNode()
|
|
2413
|
-
return node && !node.hidden ? node.textContent.trim() : ''
|
|
2414
|
-
}
|
|
2415
|
-
if (ready && demo && view) {
|
|
2416
|
-
click('hover-first-point')
|
|
2417
|
-
const pointHover = await until(() => (tooltipContent().includes('节点') && readout('hover-readout').includes('point') ? tooltipContent() : null), 6000)
|
|
2418
|
-
check('悬停与提示:hoverAt 命中节点后套用 pointHoverStyle,并弹出节点 tooltip', Boolean(pointHover), `${readout('hover-readout')}|${readout('tooltip-readout')}`)
|
|
2419
|
-
|
|
2420
|
-
const versionBefore = Number(setup.tooltipVersion)
|
|
2421
|
-
click('tooltip-version')
|
|
2422
|
-
const refreshed = await until(() => (tooltipContent().includes(`v${versionBefore + 1}`) ? tooltipContent() : null), 5000)
|
|
2423
|
-
check('悬停与提示:悬停期间 updateOptions({ tooltip }) 立即刷新当前提示(内容版本号变化)', Boolean(refreshed), `${versionBefore} → ${readout('tooltip-readout')}`)
|
|
2424
|
-
|
|
2425
|
-
setup.hoverStyle.pointColor = '#00ff88'
|
|
2426
|
-
setup.hoverStyle.pointSizeScale = 2.4
|
|
2427
|
-
setup.hoverStyle.pointStrokeColor = '#112233'
|
|
2428
|
-
setup.hoverStyle.pointStrokeWidth = 3.5
|
|
2429
|
-
setup.hoverStyle.lineColor = '#00aaff'
|
|
2430
|
-
setup.hoverStyle.lineWidth = 6
|
|
2431
|
-
click('hover-style-apply')
|
|
2432
|
-
await wait(150)
|
|
2433
|
-
const styled = await until(() => {
|
|
2434
|
-
const options = view.getOptions()
|
|
2435
|
-
return options.pointHoverStyle?.color === '#00ff88' && options.pointHoverStyle?.sizeScale === 2.4 && options.pointHoverStyle?.strokeColor === '#112233' && options.pointHoverStyle?.strokeWidth === 3.5 && options.lineHoverStyle?.color === '#00aaff' && options.lineHoverStyle?.width === 6
|
|
2436
|
-
}, 5000)
|
|
2437
|
-
check('悬停与提示:节点 / 线路悬停样式都能现场改并写回 options', Boolean(styled) && readout('hover-style-readout').includes('#00ff88') && readout('hover-style-readout').includes('6px'), readout('hover-style-readout'))
|
|
2438
|
-
|
|
2439
|
-
click('hover-first-line')
|
|
2440
|
-
const lineHover = await until(() => (tooltipContent().includes('线路') && readout('hover-readout').includes('line #') ? tooltipContent() : null), 6000)
|
|
2441
|
-
check('悬停与提示:独立线路命中后走 lineHoverStyle / tooltip.line', Boolean(lineHover), `${readout('hover-readout')}|${readout('tooltip-readout')}`)
|
|
2442
|
-
|
|
2443
|
-
click('hover-first-channel')
|
|
2444
|
-
const channelHover = await until(() => (tooltipContent().includes('通道') && readout('hover-readout').includes('通道') ? tooltipContent() : null), 6000)
|
|
2445
|
-
check('悬停与提示:通道回线命中后显示 channel 内容并带 channelScope 读数', Boolean(channelHover), `${readout('hover-readout')}|${readout('tooltip-readout')}`)
|
|
2446
|
-
|
|
2447
|
-
const setSelect = (key, value) => {
|
|
2448
|
-
const node = demo.querySelector(`[data-demo="${key}"]`)
|
|
2449
|
-
if (!node) {
|
|
2450
|
-
return
|
|
2451
|
-
}
|
|
2452
|
-
node.value = value
|
|
2453
|
-
node.dispatchEvent(new Event('change', { bubbles: true }))
|
|
2454
|
-
}
|
|
2455
|
-
setSelect('tooltip-content-mode', 'function')
|
|
2456
|
-
const functionMode = await until(() => (typeof view.getOptions().tooltip?.point === 'function' && tooltipContent().includes('函数') ? tooltipContent() : null), 5000)
|
|
2457
|
-
check('悬停与提示:内容模式切到函数后,tooltip 用命中载荷当场算出文本', Boolean(functionMode), readout('tooltip-config-readout'))
|
|
2458
|
-
|
|
2459
|
-
setSelect('tooltip-content-mode', 'element')
|
|
2460
|
-
const elementMode = await until(() => {
|
|
2461
|
-
const node = tooltipNode()
|
|
2462
|
-
return typeof view.getOptions().tooltip?.point === 'function' && node && !node.hidden && node.querySelector('strong') && node.textContent.includes('DOM 元素')
|
|
2463
|
-
}, 5000)
|
|
2464
|
-
check('悬停与提示:内容模式切到 DOM 元素后,函数返回的节点被直接插进提示框', Boolean(elementMode), readout('tooltip-readout'))
|
|
2465
|
-
|
|
2466
|
-
click('tooltip-html-preset')
|
|
2467
|
-
const htmlMode = await until(() => {
|
|
2468
|
-
const node = tooltipNode()
|
|
2469
|
-
return view.getOptions().tooltip?.html === true && node && !node.hidden && node.innerHTML.includes('<b>')
|
|
2470
|
-
}, 5000)
|
|
2471
|
-
check('悬停与提示:html=true 时模板里的 <b> / <br> 按 HTML 渲染', Boolean(htmlMode), readout('tooltip-config-readout'))
|
|
2472
|
-
|
|
2473
|
-
setSelect('tooltip-channel-scope', 'line')
|
|
2474
|
-
const scopeChanged = await until(() => view.getOptions().tooltip?.channelScope === 'line' && readout('tooltip-config-readout').includes('channelScope=line'), 5000)
|
|
2475
|
-
check('悬停与提示:channelScope 可现场切换(channel / group / line)', Boolean(scopeChanged), readout('tooltip-config-readout'))
|
|
2476
|
-
|
|
2477
|
-
click('hover-clear')
|
|
2478
|
-
const cleared = await until(() => (!tooltipNode() || tooltipNode().hidden) && readout('hover-readout').includes('已离开'), 5000)
|
|
2479
|
-
check('悬停与提示:clearHover 收起提示并撤销悬停高亮', Boolean(cleared), readout('hover-readout'))
|
|
2480
|
-
}
|
|
2481
|
-
else {
|
|
2482
|
-
check('悬停与提示:演示页与实例都起来了', false, ready ? '没有 demo / setupState.view' : '没有 canvas')
|
|
2483
|
-
}
|
|
2484
|
-
}
|
|
2485
|
-
|
|
2486
|
-
// 4. 事件与方法:拖到一起进「待确认合并」、Alt 点击停在预览态的复制 —— 两个弹框都在演示里裁决
|
|
2487
|
-
await clickTab('事件与方法')
|
|
2488
|
-
await canvasReady()
|
|
2489
|
-
await until(() => [...document.querySelectorAll('.dv-demo .dv-group h3')].some(node => node.textContent.includes('光标读数')), 8000)
|
|
2490
|
-
const eventDemo = document.querySelector('.dv-demo')
|
|
2491
|
-
const eventSetup = eventDemo.__vueParentComponent.setupState
|
|
2492
|
-
const eventView = await until(() => eventSetup.view, 8000)
|
|
2493
|
-
const eventCanvas = eventDemo.querySelector('canvas.gis-view__canvas')
|
|
2494
|
-
if (eventView && eventCanvas) {
|
|
2495
|
-
const eventBox = eventCanvas.getBoundingClientRect()
|
|
2496
|
-
// 合成指针:事件挂在 canvas 上,坐标是画布内的 CSS 像素(与 selftest-features 的 cmSend 同一套)
|
|
2497
|
-
const eventSend = (type, screen, extra = {}) => {
|
|
2498
|
-
eventCanvas.dispatchEvent(new PointerEvent(type, {
|
|
2499
|
-
bubbles: true,
|
|
2500
|
-
cancelable: true,
|
|
2501
|
-
composed: true,
|
|
2502
|
-
pointerId: 9,
|
|
2503
|
-
pointerType: 'mouse',
|
|
2504
|
-
isPrimary: true,
|
|
2505
|
-
button: extra.button ?? 0,
|
|
2506
|
-
buttons: extra.buttons ?? (type === 'pointerup' ? 0 : 1),
|
|
2507
|
-
clientX: eventBox.left + screen[0],
|
|
2508
|
-
clientY: eventBox.top + screen[1],
|
|
2509
|
-
...extra
|
|
2510
|
-
}))
|
|
2511
|
-
}
|
|
2512
|
-
const eventDrag = async (from, to) => {
|
|
2513
|
-
eventSend('pointermove', from, { buttons: 0 })
|
|
2514
|
-
eventSend('pointerdown', from, { buttons: 1 })
|
|
2515
|
-
eventSend('pointermove', [(from[0] + to[0]) / 2, (from[1] + to[1]) / 2], { buttons: 1 })
|
|
2516
|
-
eventSend('pointermove', to, { buttons: 1 })
|
|
2517
|
-
eventSend('pointerup', to, { buttons: 0 })
|
|
2518
|
-
await wait(140)
|
|
2519
|
-
}
|
|
2520
|
-
const eventPoints = eventView.getData().points
|
|
2521
|
-
const eventSource = eventView.project(eventPoints[0].x, eventPoints[0].y)
|
|
2522
|
-
const eventTarget = eventView.project(eventPoints[1].x, eventPoints[1].y)
|
|
2523
|
-
const pointsBeforeMerge = eventPoints.length
|
|
2524
|
-
await eventDrag(eventSource, eventTarget)
|
|
2525
|
-
const mergeBar = await until(() => eventDemo.querySelector('.dv-confirm'), 6000)
|
|
2526
|
-
const mergeText = mergeBar ? mergeBar.textContent.trim() : ''
|
|
2527
|
-
const mergeCursor = eventCanvas.style.cursor
|
|
2528
|
-
check('事件与方法:拖到一起进入「待确认合并」,数据先不动', Boolean(mergeBar) && eventSetup.mergeState?.phase === 'pending' && eventView.getData().points.length === pointsBeforeMerge, mergeBar ? 'phase=' + eventSetup.mergeState?.phase + ' 光标=' + mergeCursor : '没有确认条')
|
|
2529
|
-
check('事件与方法:待确认合并的光标切到 copy 档(cursorStyle.copy)', mergeCursor.includes('copy'), '光标=' + mergeCursor)
|
|
2530
|
-
check('事件与方法:确认条报出「来源 → 目标」', mergeText.includes('合并待确认') && mergeText.includes('→'), mergeText.slice(0, 120))
|
|
2531
|
-
mergeBar?.querySelector('.dv-button--primary')?.click()
|
|
2532
|
-
const mergedCount = await until(() => (eventView.getData().points.length === pointsBeforeMerge - 1 ? eventView.getData().points.length : null), 6000)
|
|
2533
|
-
check('事件与方法:确认条「确认合并」= payload.confirm(),两个节点并成一个', mergedCount === pointsBeforeMerge - 1 && !eventDemo.querySelector('.dv-confirm'), pointsBeforeMerge + ' → ' + eventView.getData().points.length)
|
|
2534
|
-
// 复制:Alt 点击出预览 → 跟着鼠标走 → **松开左键**才结束(中途不弹框)→ 收尾条给一次撤销
|
|
2535
|
-
const copyFrom = eventView.project(eventView.getData().points[0].x, eventView.getData().points[0].y)
|
|
2536
|
-
const copyTo = [copyFrom[0] + 70, copyFrom[1] + 46]
|
|
2537
|
-
const pointsBeforeCopy = eventView.getData().points.length
|
|
2538
|
-
eventSend('pointermove', copyFrom, { buttons: 0 })
|
|
2539
|
-
eventSend('pointerdown', copyFrom, { altKey: true })
|
|
2540
|
-
eventSend('pointermove', copyTo, { buttons: 0, altKey: true })
|
|
2541
|
-
await wait(100)
|
|
2542
|
-
const copyCursor = eventCanvas.style.cursor
|
|
2543
|
-
check('事件与方法:复制中途不弹框(松开左键才算结束)', !eventDemo.querySelector('.dv-confirm') && eventView.getPointCopy()?.phase === 'preview' && eventView.getData().points.length === pointsBeforeCopy, `phase=${eventView.getPointCopy()?.phase} points=${eventView.getData().points.length}`)
|
|
2544
|
-
// 松开 Alt 只是不再算复制修饰键:预览照旧跟着鼠标,抬起左键那一下才落地
|
|
2545
|
-
window.dispatchEvent(new KeyboardEvent('keyup', { key: 'Alt', altKey: false, bubbles: true }))
|
|
2546
|
-
await wait(60)
|
|
2547
|
-
check('事件与方法:松开 Alt 不落地(旧口径就是在这里落地的)', eventView.getPointCopy()?.phase === 'preview' && eventView.getData().points.length === pointsBeforeCopy, `phase=${eventView.getPointCopy()?.phase}`)
|
|
2548
|
-
eventSend('pointerup', copyTo, { buttons: 0, altKey: true })
|
|
2549
|
-
const copiedCount = await until(() => (eventView.getData().points.length === pointsBeforeCopy + 1 ? eventView.getData().points.length : null), 6000)
|
|
2550
|
-
const copyBar = await until(() => eventDemo.querySelector('.dv-confirm'), 6000)
|
|
2551
|
-
const copyText = copyBar ? copyBar.textContent.trim() : ''
|
|
2552
|
-
check('事件与方法:复制预览跟着鼠标走,光标同样是 copy 档', copyCursor.includes('copy'), '光标=' + copyCursor)
|
|
2553
|
-
check('事件与方法:松开左键落地(复制结束),收尾条这时才弹出来', copiedCount === pointsBeforeCopy + 1 && Boolean(copyBar) && copyText.includes('复制结束') && eventView.getPointCopy() === null, `points=${pointsBeforeCopy} → ${eventView.getData().points.length} · ${copyText.slice(0, 90)}`)
|
|
2554
|
-
// 收尾条上的「撤销」= end 阶段载荷的 payload.revert():已经写进去的复制品整份收回去
|
|
2555
|
-
copyBar?.querySelector('.dv-button--primary')?.click()
|
|
2556
|
-
const revertedCount = await until(() => (eventView.getData().points.length === pointsBeforeCopy ? eventView.getData().points.length : null), 6000)
|
|
2557
|
-
check('事件与方法:收尾条「撤销」= payload.revert(),复制品整份收回去', revertedCount === pointsBeforeCopy && !eventDemo.querySelector('.dv-confirm'), `points=${pointsBeforeCopy} → ${eventView.getData().points.length}`)
|
|
2558
|
-
// 「知道了」只关掉条:复制已经落地,复制品留着
|
|
2559
|
-
eventSend('pointerdown', copyFrom, { altKey: true })
|
|
2560
|
-
eventSend('pointermove', copyTo, { buttons: 0, altKey: true })
|
|
2561
|
-
eventSend('pointerup', copyTo, { buttons: 0, altKey: true })
|
|
2562
|
-
await wait(140)
|
|
2563
|
-
const keptBar = await until(() => eventDemo.querySelector('.dv-confirm'), 6000)
|
|
2564
|
-
keptBar?.querySelector('.dv-button:not(.dv-button--primary)')?.click()
|
|
2565
|
-
await wait(160)
|
|
2566
|
-
check('事件与方法:收尾条「知道了」只关框,复制品留着', !eventDemo.querySelector('.dv-confirm') && eventView.getData().points.length === pointsBeforeCopy + 1, 'points=' + eventView.getData().points.length)
|
|
2567
|
-
}
|
|
2568
|
-
else {
|
|
2569
|
-
check('事件与方法:画布与视图都起来了', false, '没等到 canvas 或 view')
|
|
2570
|
-
}
|
|
2571
|
-
// 5. 通道与数据:右键一条通道回线弹出交互框(分组列表 / 线路分配 / runChannelAction 五个动作)
|
|
2572
|
-
await clickTab('通道与数据')
|
|
2573
|
-
await canvasReady()
|
|
2574
|
-
await until(() => [...document.querySelectorAll('.dv-demo .dv-group h3')].some(node => node.textContent.includes('右键交互框')), 8000)
|
|
2575
|
-
const channelDemo = document.querySelector('.dv-demo')
|
|
2576
|
-
const channelSetup = channelDemo.__vueParentComponent.setupState
|
|
2577
|
-
const channelView = await until(() => channelSetup.view, 8000)
|
|
2578
|
-
await until(() => (channelView?.getChannels() ?? []).length > 0, 8000)
|
|
2313
|
+
tap(target)
|
|
2314
|
+
const clickLogged = await until(() => logText().includes('labelClick'), 8000)
|
|
2315
|
+
check('点一下标签发 labelClick,日志记下「哪一条、内容是什么」', Boolean(clickLogged), logText().slice(0, 160))
|
|
2316
|
+
const editReadout = document.querySelector('.dv-demo .dv-input')?.closest('.dv-group')?.querySelector('.dv-readout')?.textContent?.trim() ?? ''
|
|
2317
|
+
check('点完之后编辑面板认到了这一条', !editReadout.includes('点一下标签') && editReadout.length > 0, editReadout)
|
|
2318
|
+
|
|
2319
|
+
// 3.2 右键标签 → 自绘菜单(编辑 / 换位置 / 重置 / 隐藏 / 删除)
|
|
2320
|
+
const rect = target.getBoundingClientRect()
|
|
2321
|
+
const menuEvent = contextMenuOn(target, rect.left + 4, rect.top + 4)
|
|
2322
|
+
check('右键标签的系统菜单被拦下(preventContextMenu 缺省开)', menuEvent.defaultPrevented, `defaultPrevented=${menuEvent.defaultPrevented}`)
|
|
2323
|
+
const menu = await until(() => document.querySelector('.dv-menu'), 6000)
|
|
2324
|
+
check('右键弹出演示自己的菜单', Boolean(menu), menu ? [...menu.querySelectorAll('button')].map(node => node.textContent.trim()).join(' / ') : '没弹出')
|
|
2325
|
+
const items = menu ? [...menu.querySelectorAll('button')].map(node => node.textContent.trim()) : []
|
|
2326
|
+
check('菜单里有改内容 / 换位置 / 重置偏移 / 隐藏 / 删除这五项', ['改内容', '换位置', '重置偏移', '删除这一条'].every(text => items.some(item => item.includes(text))), items.join(' / '))
|
|
2327
|
+
const positionButton = menu ? [...menu.querySelectorAll('button')].find(node => node.textContent.includes('换位置')) : null
|
|
2328
|
+
positionButton?.click()
|
|
2329
|
+
await wait(260)
|
|
2330
|
+
check('菜单「换位置」落到 updateLabel 上(日志看得到)', logText().includes('updateLabel'), logText().slice(0, 160))
|
|
2331
|
+
check('点完菜单项之后菜单收起来了', !document.querySelector('.dv-menu'), document.querySelector('.dv-menu') ? '还开着' : '已收')
|
|
2332
|
+
// 3.3 删除一条:自绘菜单的删除落到 removeLabel / updateLabel(hidden)
|
|
2333
|
+
const beforeDelete = labelNodes()
|
|
2334
|
+
const labelForDelete = beforeDelete[beforeDelete.length - 1]
|
|
2335
|
+
// 按内容认「删的是哪一条」:避让是「没位置就不画」,删掉一条腾出来的小格会让别的候选补位,
|
|
2336
|
+
// 所以这里盯的是「被删的那一条不再画」,不是「总数不涨」(13 → 14 是正常的补位)
|
|
2337
|
+
const deletedLabelText = labelForDelete.textContent.trim()
|
|
2338
|
+
contextMenuOn(labelForDelete, labelForDelete.getBoundingClientRect().left + 4, labelForDelete.getBoundingClientRect().top + 4)
|
|
2339
|
+
const deleteMenu = await until(() => document.querySelector('.dv-menu'), 6000)
|
|
2340
|
+
const deleteButton = deleteMenu ? [...deleteMenu.querySelectorAll('button')].find(node => node.textContent.includes('删除这一条')) : null
|
|
2341
|
+
deleteButton?.click()
|
|
2342
|
+
await wait(400)
|
|
2343
|
+
check('菜单「删除这一条」记到了日志里', logText().includes('removeLabel') || logText().includes('updateLabel'), logText().slice(0, 160))
|
|
2344
|
+
const remainingLabels = labelNodes()
|
|
2345
|
+
check('删完之后被删的那一条不再画(腾出的位置允许别的标签补位)', deletedLabelText !== '' && !remainingLabels.some(node => node.textContent.trim() === deletedLabelText), `${deletedLabelText}|${beforeDelete.length} → ${remainingLabels.length}`)
|
|
2346
|
+
|
|
2347
|
+
// 3.4 批量 API:给全部节点加一条
|
|
2348
|
+
const addAllButton = [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.includes('给全部加一条'))
|
|
2349
|
+
addAllButton?.click()
|
|
2350
|
+
await wait(500)
|
|
2351
|
+
check('「给全部加一条」走 addLabels,日志里有新增计数', logText().includes('addLabels'), logText().slice(0, 160))
|
|
2352
|
+
check('labelChange 也跟着发(addAll)', logText().includes('addAll'), logText().slice(0, 200))
|
|
2353
|
+
|
|
2354
|
+
// 3.5 点选即加:先选中第一个节点,再让「给选中加一条」把它加上
|
|
2355
|
+
const selectButton = [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.includes('选中第一个节点'))
|
|
2356
|
+
selectButton?.click()
|
|
2357
|
+
await wait(400)
|
|
2358
|
+
const selectionReadout = [...document.querySelectorAll('.dv-demo .dv-readout')].map(node => node.textContent.trim()).find(text => text.includes('选中'))
|
|
2359
|
+
check('「选中第一个节点」让面板报出选中集', Boolean(selectionReadout) && !selectionReadout.includes('未选中'), selectionReadout ?? '没有选中读数')
|
|
2360
|
+
const preview = [...document.querySelectorAll('.dv-demo .dv-hint')].map(node => node.textContent.trim()).find(text => text.includes('本次会加到'))
|
|
2361
|
+
check('选中集预览用 resolveLabelTargets 算出「会加到几条」', Boolean(preview), preview ?? '没有预览读数')
|
|
2362
|
+
const addSelectionButton = [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.includes('给选中加一条'))
|
|
2363
|
+
check('「给选中加一条」选中集非空时可用', Boolean(addSelectionButton) && !addSelectionButton.disabled, addSelectionButton ? `disabled=${addSelectionButton.disabled}` : '没找到按钮')
|
|
2364
|
+
addSelectionButton?.click()
|
|
2365
|
+
await wait(500)
|
|
2366
|
+
check('给选中集加标签走 addLabels(..., \'selection\', ...)', logText().includes('选中集:新增') || logText().includes('addLabels'), logText().slice(0, 200))
|
|
2367
|
+
// 3.5.1 连点两次:加出来的标签默认要跟别的标签避让,压在一起就整条不画 ——
|
|
2368
|
+
// 面板上看着像「只有第一次加上了」,这里盯住「每一次都多画一条」
|
|
2369
|
+
const noteCount = () => [...document.querySelectorAll('.dv-demo .gis-view__label')].filter(node => node.textContent.includes('备注#')).length
|
|
2370
|
+
const notesBefore = noteCount()
|
|
2371
|
+
addSelectionButton?.click()
|
|
2372
|
+
await wait(400)
|
|
2373
|
+
addSelectionButton?.click()
|
|
2374
|
+
await wait(400)
|
|
2375
|
+
const notesAfter = noteCount()
|
|
2376
|
+
check('连点「给选中加一条」每次都能多画出一条(不只第一次有效)', notesAfter >= notesBefore + 2, `备注标签 ${notesBefore} → ${notesAfter}`)
|
|
2377
|
+
|
|
2378
|
+
// 3.6 线路标签 + 整条通道:切到线路标签再加
|
|
2379
|
+
const lineTab = [...document.querySelectorAll('.dv-demo .dv-chip')].find(node => node.textContent.includes('线路标签'))
|
|
2380
|
+
lineTab?.click()
|
|
2381
|
+
await wait(200)
|
|
2382
|
+
const channelButton = [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.includes('整条通道加'))
|
|
2383
|
+
channelButton?.click()
|
|
2384
|
+
await wait(500)
|
|
2385
|
+
check('「整条通道加」用 { channel } 一次给整条通道的回线加标签', logText().includes('通道'), logText().slice(0, 200))
|
|
2386
|
+
|
|
2387
|
+
// 3.7 双击标签:重置偏移
|
|
2388
|
+
const dblTarget = labelNodes()[0]
|
|
2389
|
+
if (dblTarget) {
|
|
2390
|
+
const dblInit = { bubbles: true, cancelable: true, composed: true, clientX: dblTarget.getBoundingClientRect().left + 2, clientY: dblTarget.getBoundingClientRect().top + 2 }
|
|
2391
|
+
dblTarget.dispatchEvent(new MouseEvent('dblclick', dblInit))
|
|
2392
|
+
await wait(300)
|
|
2393
|
+
check('双击标签发 labelDblClick(这里接成重置偏移)', logText().includes('labelDblClick') || logText().includes('重置偏移'), logText().slice(0, 160))
|
|
2394
|
+
}
|
|
2395
|
+
else {
|
|
2396
|
+
check('双击标签发 labelDblClick(这里接成重置偏移)', false, '没有标签节点可点')
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
// 3.8 标签显示:动态切节点 / 线路标签内容(setLabelContent 的独立演示)
|
|
2400
|
+
if (await clickTab('标签显示')) {
|
|
2401
|
+
const ready = await canvasReady()
|
|
2402
|
+
const demo = await until(() => {
|
|
2403
|
+
const node = document.querySelector('.dv-demo')
|
|
2404
|
+
return node?.textContent.includes('改哪一类标签') ? node : null
|
|
2405
|
+
}, 10000)
|
|
2406
|
+
const setup = demo?.__vueParentComponent?.setupState
|
|
2407
|
+
const view = await until(() => setup?.view, 8000)
|
|
2408
|
+
const countLayer = layer => demo?.querySelectorAll(`.gis-view__label-layer--${layer} .gis-view__label`).length ?? 0
|
|
2409
|
+
const labelsShown = await until(() => countLayer('point') > 0 && countLayer('line') > 0, 12000)
|
|
2410
|
+
const click = key => demo?.querySelector(`[data-demo="${key}"]`)?.click()
|
|
2411
|
+
const readout = key => demo?.querySelector(`[data-demo="${key}"]`)?.textContent.trim() ?? ''
|
|
2412
|
+
if (ready && demo && view) {
|
|
2413
|
+
const initial = view.getOptions()
|
|
2414
|
+
const countReady = await until(() => readout('label-display-count').includes('画面上的标签'), 5000)
|
|
2415
|
+
check('标签显示:节点与线路标签都画出来,初始内容由 label / lineLabel 的同一套模板控制',
|
|
2416
|
+
Boolean(labelsShown) && Boolean(countReady) && initial.label?.content === '{name} · {vn_kv} kV' && initial.lineLabel?.content === '{name} · {vn_kv} kV',
|
|
2417
|
+
`节点 ${countLayer('point')} / 线路 ${countLayer('line')} · ${readout('label-display-count')}`)
|
|
2418
|
+
|
|
2419
|
+
click('label-display-target-point')
|
|
2420
|
+
await wait(80)
|
|
2421
|
+
click('label-display-preset-voltage')
|
|
2422
|
+
const pointOnly = await until(() => {
|
|
2423
|
+
const options = view.getOptions()
|
|
2424
|
+
return options.label?.content === '{vn_kv} kV' && options.lineLabel?.content === '{name} · {vn_kv} kV'
|
|
2425
|
+
}, 5000)
|
|
2426
|
+
check('标签显示:只改节点时 setLabelContent 的 kind=point 不影响线路标签', Boolean(pointOnly), readout('label-display-current'))
|
|
2427
|
+
|
|
2428
|
+
click('label-display-target-both')
|
|
2429
|
+
await wait(80)
|
|
2430
|
+
click('label-display-preset-name-voltage')
|
|
2431
|
+
const bothChanged = await until(() => {
|
|
2432
|
+
const options = view.getOptions()
|
|
2433
|
+
return options.label?.content === '{name} · {vn_kv} kV' && options.lineLabel?.content === '{name} · {vn_kv} kV'
|
|
2434
|
+
}, 5000)
|
|
2435
|
+
check('标签显示:切回节点 + 线路后,一份模板同时落到两类标签上', Boolean(bothChanged) && readout('label-display-action').includes('节点 + 线路'), readout('label-display-action'))
|
|
2436
|
+
|
|
2437
|
+
const customInput = demo.querySelector('[data-demo="label-display-custom-input"]')
|
|
2438
|
+
const customText = '自定义 {name} · {vn_kv}'
|
|
2439
|
+
if (customInput) {
|
|
2440
|
+
customInput.value = customText
|
|
2441
|
+
customInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
2442
|
+
}
|
|
2443
|
+
click('label-display-apply-custom')
|
|
2444
|
+
const customApplied = await until(() => {
|
|
2445
|
+
const options = view.getOptions()
|
|
2446
|
+
return options.label?.content === customText && options.lineLabel?.content === customText
|
|
2447
|
+
}, 5000)
|
|
2448
|
+
check('标签显示:自定义模板原样写进节点与线路的 label.content', Boolean(customApplied), readout('label-display-current'))
|
|
2449
|
+
|
|
2450
|
+
click('label-display-auto')
|
|
2451
|
+
const autoStarted = await until(() => readout('label-display-action').includes('自动轮播开始'), 3000)
|
|
2452
|
+
const autoRotated = await until(() => view.getOptions().label?.content !== customText, 4500)
|
|
2453
|
+
click('label-display-auto')
|
|
2454
|
+
const autoStopped = await until(() => readout('label-display-action').includes('已停止'), 3000)
|
|
2455
|
+
check('标签显示:自动轮播按 1.8 秒换模板,按钮再点一次能停', Boolean(autoStarted) && Boolean(autoRotated) && Boolean(autoStopped), `${readout('label-display-action')} / 当前 ${view.getOptions().label?.content}`)
|
|
2456
|
+
}
|
|
2457
|
+
else {
|
|
2458
|
+
check('标签显示:演示页与实例都起来了', false, ready ? '没有 demo / setupState.view' : '没有 canvas')
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
// 3.9 悬停与提示:hover 样式、三种 tooltip 内容写法、悬停中刷新内容
|
|
2463
|
+
if (await clickTab('悬停与提示')) {
|
|
2464
|
+
const ready = await canvasReady()
|
|
2465
|
+
const demo = await until(() => {
|
|
2466
|
+
const node = document.querySelector('.dv-demo')
|
|
2467
|
+
return node?.textContent.includes('悬停样式') && node.textContent.includes('提示内容') ? node : null
|
|
2468
|
+
}, 10000)
|
|
2469
|
+
const setup = demo?.__vueParentComponent?.setupState
|
|
2470
|
+
const view = await until(() => setup?.view, 8000)
|
|
2471
|
+
await wait(260)
|
|
2472
|
+
const click = key => demo?.querySelector(`[data-demo="${key}"]`)?.click()
|
|
2473
|
+
const readout = key => demo?.querySelector(`[data-demo="${key}"]`)?.textContent.trim() ?? ''
|
|
2474
|
+
const tooltipNode = () => demo?.querySelector('.gis-view__tooltip') ?? null
|
|
2475
|
+
const tooltipContent = () => {
|
|
2476
|
+
const node = tooltipNode()
|
|
2477
|
+
return node && !node.hidden ? node.textContent.trim() : ''
|
|
2478
|
+
}
|
|
2479
|
+
if (ready && demo && view) {
|
|
2480
|
+
click('hover-first-point')
|
|
2481
|
+
const pointHover = await until(() => (tooltipContent().includes('节点') && readout('hover-readout').includes('point') ? tooltipContent() : null), 6000)
|
|
2482
|
+
check('悬停与提示:hoverAt 命中节点后套用 pointHoverStyle,并弹出节点 tooltip', Boolean(pointHover), `${readout('hover-readout')}|${readout('tooltip-readout')}`)
|
|
2483
|
+
|
|
2484
|
+
const versionBefore = Number(setup.tooltipVersion)
|
|
2485
|
+
click('tooltip-version')
|
|
2486
|
+
const refreshed = await until(() => (tooltipContent().includes(`v${versionBefore + 1}`) ? tooltipContent() : null), 5000)
|
|
2487
|
+
check('悬停与提示:悬停期间 updateOptions({ tooltip }) 立即刷新当前提示(内容版本号变化)', Boolean(refreshed), `${versionBefore} → ${readout('tooltip-readout')}`)
|
|
2488
|
+
|
|
2489
|
+
setup.hoverStyle.pointColor = '#00ff88'
|
|
2490
|
+
setup.hoverStyle.pointSizeScale = 2.4
|
|
2491
|
+
setup.hoverStyle.pointStrokeColor = '#112233'
|
|
2492
|
+
setup.hoverStyle.pointStrokeWidth = 3.5
|
|
2493
|
+
setup.hoverStyle.lineColor = '#00aaff'
|
|
2494
|
+
setup.hoverStyle.lineWidth = 6
|
|
2495
|
+
click('hover-style-apply')
|
|
2496
|
+
await wait(150)
|
|
2497
|
+
const styled = await until(() => {
|
|
2498
|
+
const options = view.getOptions()
|
|
2499
|
+
return options.pointHoverStyle?.color === '#00ff88' && options.pointHoverStyle?.sizeScale === 2.4 && options.pointHoverStyle?.strokeColor === '#112233' && options.pointHoverStyle?.strokeWidth === 3.5 && options.lineHoverStyle?.color === '#00aaff' && options.lineHoverStyle?.width === 6
|
|
2500
|
+
}, 5000)
|
|
2501
|
+
check('悬停与提示:节点 / 线路悬停样式都能现场改并写回 options', Boolean(styled) && readout('hover-style-readout').includes('#00ff88') && readout('hover-style-readout').includes('6px'), readout('hover-style-readout'))
|
|
2502
|
+
|
|
2503
|
+
click('hover-first-line')
|
|
2504
|
+
const lineHover = await until(() => (tooltipContent().includes('线路') && readout('hover-readout').includes('line #') ? tooltipContent() : null), 6000)
|
|
2505
|
+
check('悬停与提示:独立线路命中后走 lineHoverStyle / tooltip.line', Boolean(lineHover), `${readout('hover-readout')}|${readout('tooltip-readout')}`)
|
|
2506
|
+
|
|
2507
|
+
click('hover-first-channel')
|
|
2508
|
+
const channelHover = await until(() => (tooltipContent().includes('通道') && readout('hover-readout').includes('通道') ? tooltipContent() : null), 6000)
|
|
2509
|
+
check('悬停与提示:通道回线命中后显示 channel 内容并带 channelScope 读数', Boolean(channelHover), `${readout('hover-readout')}|${readout('tooltip-readout')}`)
|
|
2510
|
+
|
|
2511
|
+
const setSelect = (key, value) => {
|
|
2512
|
+
const node = demo.querySelector(`[data-demo="${key}"]`)
|
|
2513
|
+
if (!node) {
|
|
2514
|
+
return
|
|
2515
|
+
}
|
|
2516
|
+
node.value = value
|
|
2517
|
+
node.dispatchEvent(new Event('change', { bubbles: true }))
|
|
2518
|
+
}
|
|
2519
|
+
setSelect('tooltip-content-mode', 'function')
|
|
2520
|
+
const functionMode = await until(() => (typeof view.getOptions().tooltip?.point === 'function' && tooltipContent().includes('函数') ? tooltipContent() : null), 5000)
|
|
2521
|
+
check('悬停与提示:内容模式切到函数后,tooltip 用命中载荷当场算出文本', Boolean(functionMode), readout('tooltip-config-readout'))
|
|
2522
|
+
|
|
2523
|
+
setSelect('tooltip-content-mode', 'element')
|
|
2524
|
+
const elementMode = await until(() => {
|
|
2525
|
+
const node = tooltipNode()
|
|
2526
|
+
return typeof view.getOptions().tooltip?.point === 'function' && node && !node.hidden && node.querySelector('strong') && node.textContent.includes('DOM 元素')
|
|
2527
|
+
}, 5000)
|
|
2528
|
+
check('悬停与提示:内容模式切到 DOM 元素后,函数返回的节点被直接插进提示框', Boolean(elementMode), readout('tooltip-readout'))
|
|
2529
|
+
|
|
2530
|
+
click('tooltip-html-preset')
|
|
2531
|
+
const htmlMode = await until(() => {
|
|
2532
|
+
const node = tooltipNode()
|
|
2533
|
+
return view.getOptions().tooltip?.html === true && node && !node.hidden && node.innerHTML.includes('<b>')
|
|
2534
|
+
}, 5000)
|
|
2535
|
+
check('悬停与提示:html=true 时模板里的 <b> / <br> 按 HTML 渲染', Boolean(htmlMode), readout('tooltip-config-readout'))
|
|
2536
|
+
|
|
2537
|
+
setSelect('tooltip-channel-scope', 'line')
|
|
2538
|
+
const scopeChanged = await until(() => view.getOptions().tooltip?.channelScope === 'line' && readout('tooltip-config-readout').includes('channelScope=line'), 5000)
|
|
2539
|
+
check('悬停与提示:channelScope 可现场切换(channel / group / line)', Boolean(scopeChanged), readout('tooltip-config-readout'))
|
|
2540
|
+
|
|
2541
|
+
click('hover-clear')
|
|
2542
|
+
const cleared = await until(() => (!tooltipNode() || tooltipNode().hidden) && readout('hover-readout').includes('已离开'), 5000)
|
|
2543
|
+
check('悬停与提示:clearHover 收起提示并撤销悬停高亮', Boolean(cleared), readout('hover-readout'))
|
|
2544
|
+
}
|
|
2545
|
+
else {
|
|
2546
|
+
check('悬停与提示:演示页与实例都起来了', false, ready ? '没有 demo / setupState.view' : '没有 canvas')
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
// 4. 事件与方法:拖到一起进「待确认合并」、Alt 点击停在预览态的复制 —— 两个弹框都在演示里裁决
|
|
2551
|
+
await clickTab('事件与方法')
|
|
2552
|
+
await canvasReady()
|
|
2553
|
+
await until(() => [...document.querySelectorAll('.dv-demo .dv-group h3')].some(node => node.textContent.includes('光标读数')), 8000)
|
|
2554
|
+
const eventDemo = document.querySelector('.dv-demo')
|
|
2555
|
+
const eventSetup = eventDemo.__vueParentComponent.setupState
|
|
2556
|
+
const eventView = await until(() => eventSetup.view, 8000)
|
|
2557
|
+
const eventCanvas = eventDemo.querySelector('canvas.gis-view__canvas')
|
|
2558
|
+
if (eventView && eventCanvas) {
|
|
2559
|
+
const eventBox = eventCanvas.getBoundingClientRect()
|
|
2560
|
+
// 合成指针:事件挂在 canvas 上,坐标是画布内的 CSS 像素(与 selftest-features 的 cmSend 同一套)
|
|
2561
|
+
const eventSend = (type, screen, extra = {}) => {
|
|
2562
|
+
eventCanvas.dispatchEvent(new PointerEvent(type, {
|
|
2563
|
+
bubbles: true,
|
|
2564
|
+
cancelable: true,
|
|
2565
|
+
composed: true,
|
|
2566
|
+
pointerId: 9,
|
|
2567
|
+
pointerType: 'mouse',
|
|
2568
|
+
isPrimary: true,
|
|
2569
|
+
button: extra.button ?? 0,
|
|
2570
|
+
buttons: extra.buttons ?? (type === 'pointerup' ? 0 : 1),
|
|
2571
|
+
clientX: eventBox.left + screen[0],
|
|
2572
|
+
clientY: eventBox.top + screen[1],
|
|
2573
|
+
...extra
|
|
2574
|
+
}))
|
|
2575
|
+
}
|
|
2576
|
+
const eventDrag = async (from, to) => {
|
|
2577
|
+
eventSend('pointermove', from, { buttons: 0 })
|
|
2578
|
+
eventSend('pointerdown', from, { buttons: 1 })
|
|
2579
|
+
eventSend('pointermove', [(from[0] + to[0]) / 2, (from[1] + to[1]) / 2], { buttons: 1 })
|
|
2580
|
+
eventSend('pointermove', to, { buttons: 1 })
|
|
2581
|
+
eventSend('pointerup', to, { buttons: 0 })
|
|
2582
|
+
await wait(140)
|
|
2583
|
+
}
|
|
2584
|
+
const eventPoints = eventView.getData().points
|
|
2585
|
+
const eventSource = eventView.project(eventPoints[0].x, eventPoints[0].y)
|
|
2586
|
+
const eventTarget = eventView.project(eventPoints[1].x, eventPoints[1].y)
|
|
2587
|
+
const pointsBeforeMerge = eventPoints.length
|
|
2588
|
+
await eventDrag(eventSource, eventTarget)
|
|
2589
|
+
const mergeBar = await until(() => eventDemo.querySelector('.dv-confirm'), 6000)
|
|
2590
|
+
const mergeText = mergeBar ? mergeBar.textContent.trim() : ''
|
|
2591
|
+
const mergeCursor = eventCanvas.style.cursor
|
|
2592
|
+
check('事件与方法:拖到一起进入「待确认合并」,数据先不动', Boolean(mergeBar) && eventSetup.mergeState?.phase === 'pending' && eventView.getData().points.length === pointsBeforeMerge, mergeBar ? 'phase=' + eventSetup.mergeState?.phase + ' 光标=' + mergeCursor : '没有确认条')
|
|
2593
|
+
check('事件与方法:待确认合并的光标切到 copy 档(cursorStyle.copy)', mergeCursor.includes('copy'), '光标=' + mergeCursor)
|
|
2594
|
+
check('事件与方法:确认条报出「来源 → 目标」', mergeText.includes('合并待确认') && mergeText.includes('→'), mergeText.slice(0, 120))
|
|
2595
|
+
mergeBar?.querySelector('.dv-button--primary')?.click()
|
|
2596
|
+
const mergedCount = await until(() => (eventView.getData().points.length === pointsBeforeMerge - 1 ? eventView.getData().points.length : null), 6000)
|
|
2597
|
+
check('事件与方法:确认条「确认合并」= payload.confirm(),两个节点并成一个', mergedCount === pointsBeforeMerge - 1 && !eventDemo.querySelector('.dv-confirm'), pointsBeforeMerge + ' → ' + eventView.getData().points.length)
|
|
2598
|
+
// 复制:Alt 点击出预览 → 跟着鼠标走 → **松开左键**才结束(中途不弹框)→ 收尾条给一次撤销
|
|
2599
|
+
const copyFrom = eventView.project(eventView.getData().points[0].x, eventView.getData().points[0].y)
|
|
2600
|
+
const copyTo = [copyFrom[0] + 70, copyFrom[1] + 46]
|
|
2601
|
+
const pointsBeforeCopy = eventView.getData().points.length
|
|
2602
|
+
eventSend('pointermove', copyFrom, { buttons: 0 })
|
|
2603
|
+
eventSend('pointerdown', copyFrom, { altKey: true })
|
|
2604
|
+
eventSend('pointermove', copyTo, { buttons: 0, altKey: true })
|
|
2605
|
+
await wait(100)
|
|
2606
|
+
const copyCursor = eventCanvas.style.cursor
|
|
2607
|
+
check('事件与方法:复制中途不弹框(松开左键才算结束)', !eventDemo.querySelector('.dv-confirm') && eventView.getPointCopy()?.phase === 'preview' && eventView.getData().points.length === pointsBeforeCopy, `phase=${eventView.getPointCopy()?.phase} points=${eventView.getData().points.length}`)
|
|
2608
|
+
// 松开 Alt 只是不再算复制修饰键:预览照旧跟着鼠标,抬起左键那一下才落地
|
|
2609
|
+
window.dispatchEvent(new KeyboardEvent('keyup', { key: 'Alt', altKey: false, bubbles: true }))
|
|
2610
|
+
await wait(60)
|
|
2611
|
+
check('事件与方法:松开 Alt 不落地(旧口径就是在这里落地的)', eventView.getPointCopy()?.phase === 'preview' && eventView.getData().points.length === pointsBeforeCopy, `phase=${eventView.getPointCopy()?.phase}`)
|
|
2612
|
+
eventSend('pointerup', copyTo, { buttons: 0, altKey: true })
|
|
2613
|
+
const copiedCount = await until(() => (eventView.getData().points.length === pointsBeforeCopy + 1 ? eventView.getData().points.length : null), 6000)
|
|
2614
|
+
const copyBar = await until(() => eventDemo.querySelector('.dv-confirm'), 6000)
|
|
2615
|
+
const copyText = copyBar ? copyBar.textContent.trim() : ''
|
|
2616
|
+
check('事件与方法:复制预览跟着鼠标走,光标同样是 copy 档', copyCursor.includes('copy'), '光标=' + copyCursor)
|
|
2617
|
+
check('事件与方法:松开左键落地(复制结束),收尾条这时才弹出来', copiedCount === pointsBeforeCopy + 1 && Boolean(copyBar) && copyText.includes('复制结束') && eventView.getPointCopy() === null, `points=${pointsBeforeCopy} → ${eventView.getData().points.length} · ${copyText.slice(0, 90)}`)
|
|
2618
|
+
// 收尾条上的「撤销」= end 阶段载荷的 payload.revert():已经写进去的复制品整份收回去
|
|
2619
|
+
copyBar?.querySelector('.dv-button--primary')?.click()
|
|
2620
|
+
const revertedCount = await until(() => (eventView.getData().points.length === pointsBeforeCopy ? eventView.getData().points.length : null), 6000)
|
|
2621
|
+
check('事件与方法:收尾条「撤销」= payload.revert(),复制品整份收回去', revertedCount === pointsBeforeCopy && !eventDemo.querySelector('.dv-confirm'), `points=${pointsBeforeCopy} → ${eventView.getData().points.length}`)
|
|
2622
|
+
// 「知道了」只关掉条:复制已经落地,复制品留着
|
|
2623
|
+
eventSend('pointerdown', copyFrom, { altKey: true })
|
|
2624
|
+
eventSend('pointermove', copyTo, { buttons: 0, altKey: true })
|
|
2625
|
+
eventSend('pointerup', copyTo, { buttons: 0, altKey: true })
|
|
2626
|
+
await wait(140)
|
|
2627
|
+
const keptBar = await until(() => eventDemo.querySelector('.dv-confirm'), 6000)
|
|
2628
|
+
keptBar?.querySelector('.dv-button:not(.dv-button--primary)')?.click()
|
|
2629
|
+
await wait(160)
|
|
2630
|
+
check('事件与方法:收尾条「知道了」只关框,复制品留着', !eventDemo.querySelector('.dv-confirm') && eventView.getData().points.length === pointsBeforeCopy + 1, 'points=' + eventView.getData().points.length)
|
|
2631
|
+
}
|
|
2632
|
+
else {
|
|
2633
|
+
check('事件与方法:画布与视图都起来了', false, '没等到 canvas 或 view')
|
|
2634
|
+
}
|
|
2635
|
+
// 5. 通道与数据:右键一条通道回线弹出交互框(分组列表 / 线路分配 / runChannelAction 五个动作)
|
|
2636
|
+
await clickTab('通道与数据')
|
|
2637
|
+
await canvasReady()
|
|
2638
|
+
await until(() => [...document.querySelectorAll('.dv-demo .dv-group h3')].some(node => node.textContent.includes('右键交互框')), 8000)
|
|
2639
|
+
const channelDemo = document.querySelector('.dv-demo')
|
|
2640
|
+
const channelSetup = channelDemo.__vueParentComponent.setupState
|
|
2641
|
+
const channelView = await until(() => channelSetup.view, 8000)
|
|
2642
|
+
await until(() => (channelView?.getChannels() ?? []).length > 0, 8000)
|
|
2579
2643
|
const channelField = key => channelDemo.querySelector(`[data-demo="${key}"]`)?.textContent.trim() ?? ''
|
|
2580
2644
|
const channelCanvas = channelDemo.querySelector('canvas.gis-view__canvas')
|
|
2581
2645
|
if (channelView && channelCanvas) {
|
|
@@ -2743,159 +2807,159 @@
|
|
|
2743
2807
|
}
|
|
2744
2808
|
await wait(120)
|
|
2745
2809
|
channelSetup.syncReadouts?.()
|
|
2746
|
-
const channelRightClick = (screen) => {
|
|
2747
|
-
channelCanvas.dispatchEvent(new MouseEvent('contextmenu', {
|
|
2748
|
-
bubbles: true,
|
|
2749
|
-
cancelable: true,
|
|
2750
|
-
composed: true,
|
|
2751
|
-
clientX: channelBox.left + screen[0],
|
|
2752
|
-
clientY: channelBox.top + screen[1]
|
|
2753
|
-
}))
|
|
2754
|
-
}
|
|
2755
|
-
// 找一条通道回线:取两端节点投影的中点,再沿法线扫几像素(回线是骨架的平行偏移)
|
|
2756
|
-
const channelLines = channelView.getData().lines
|
|
2757
|
-
const memberIndex = channelLines.findIndex((line, index) => channelView.getLineChannel(index))
|
|
2758
|
-
const memberLine = channelLines[memberIndex]
|
|
2759
|
-
const channelPoints = channelView.getData().points
|
|
2760
|
-
const memberFrom = channelPoints.find(point => point.id === memberLine.from)
|
|
2761
|
-
const memberTo = channelPoints.find(point => point.id === memberLine.to)
|
|
2762
|
-
const fromScreen = channelView.project(memberFrom.x, memberFrom.y)
|
|
2763
|
-
const toScreen = channelView.project(memberTo.x, memberTo.y)
|
|
2764
|
-
const dx = toScreen[0] - fromScreen[0]
|
|
2765
|
-
const dy = toScreen[1] - fromScreen[1]
|
|
2766
|
-
const span = Math.hypot(dx, dy) || 1
|
|
2767
|
-
const normal = [-dy / span, dx / span]
|
|
2768
|
-
const middle = [(fromScreen[0] + toScreen[0]) / 2, (fromScreen[1] + toScreen[1]) / 2]
|
|
2769
|
-
let menuPayload = null
|
|
2770
|
-
let menuScreen = middle
|
|
2771
|
-
for (const offset of [0, 1, 2, 3, 4, 5, 6, -1, -2, -3, -4, -5, -6]) {
|
|
2772
|
-
const candidate = [middle[0] + normal[0] * offset, middle[1] + normal[1] * offset]
|
|
2773
|
-
channelRightClick(candidate)
|
|
2774
|
-
await wait(60)
|
|
2775
|
-
if (channelSetup.channelMenu) {
|
|
2776
|
-
menuPayload = channelSetup.channelMenu
|
|
2777
|
-
menuScreen = candidate
|
|
2778
|
-
break
|
|
2779
|
-
}
|
|
2780
|
-
}
|
|
2781
|
-
const channelMenuNode = channelDemo.querySelector('.dv-channel-menu')
|
|
2810
|
+
const channelRightClick = (screen) => {
|
|
2811
|
+
channelCanvas.dispatchEvent(new MouseEvent('contextmenu', {
|
|
2812
|
+
bubbles: true,
|
|
2813
|
+
cancelable: true,
|
|
2814
|
+
composed: true,
|
|
2815
|
+
clientX: channelBox.left + screen[0],
|
|
2816
|
+
clientY: channelBox.top + screen[1]
|
|
2817
|
+
}))
|
|
2818
|
+
}
|
|
2819
|
+
// 找一条通道回线:取两端节点投影的中点,再沿法线扫几像素(回线是骨架的平行偏移)
|
|
2820
|
+
const channelLines = channelView.getData().lines
|
|
2821
|
+
const memberIndex = channelLines.findIndex((line, index) => channelView.getLineChannel(index))
|
|
2822
|
+
const memberLine = channelLines[memberIndex]
|
|
2823
|
+
const channelPoints = channelView.getData().points
|
|
2824
|
+
const memberFrom = channelPoints.find(point => point.id === memberLine.from)
|
|
2825
|
+
const memberTo = channelPoints.find(point => point.id === memberLine.to)
|
|
2826
|
+
const fromScreen = channelView.project(memberFrom.x, memberFrom.y)
|
|
2827
|
+
const toScreen = channelView.project(memberTo.x, memberTo.y)
|
|
2828
|
+
const dx = toScreen[0] - fromScreen[0]
|
|
2829
|
+
const dy = toScreen[1] - fromScreen[1]
|
|
2830
|
+
const span = Math.hypot(dx, dy) || 1
|
|
2831
|
+
const normal = [-dy / span, dx / span]
|
|
2832
|
+
const middle = [(fromScreen[0] + toScreen[0]) / 2, (fromScreen[1] + toScreen[1]) / 2]
|
|
2833
|
+
let menuPayload = null
|
|
2834
|
+
let menuScreen = middle
|
|
2835
|
+
for (const offset of [0, 1, 2, 3, 4, 5, 6, -1, -2, -3, -4, -5, -6]) {
|
|
2836
|
+
const candidate = [middle[0] + normal[0] * offset, middle[1] + normal[1] * offset]
|
|
2837
|
+
channelRightClick(candidate)
|
|
2838
|
+
await wait(60)
|
|
2839
|
+
if (channelSetup.channelMenu) {
|
|
2840
|
+
menuPayload = channelSetup.channelMenu
|
|
2841
|
+
menuScreen = candidate
|
|
2842
|
+
break
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
const channelMenuNode = channelDemo.querySelector('.dv-channel-menu')
|
|
2782
2846
|
check('通道与数据:右键通道回线弹出交互框(onContextMenu.channelContext + features.contextMenu)', Boolean(menuPayload) && Boolean(channelMenuNode) && channelView.getFeatures().contextMenu === true, menuPayload ? '通道 ' + menuPayload.channelId + ' · 命中回线 ' + menuPayload.lineId + ' · 共 ' + menuPayload.groupCount + ' 组' : '没弹出')
|
|
2783
|
-
const groupsBeforeSplit = channelView.getChannelGroups(menuPayload?.channelId).length
|
|
2784
|
-
const channelAction = text => [...channelDemo.querySelectorAll('.dv-channel-menu__actions button')].find(node => node.textContent.trim() === text)
|
|
2785
|
-
check('通道与数据:交互框列出 6 个 runChannelAction 动作(含取消该分组选中),拐点编辑已开(waypointEditable)', channelDemo.querySelectorAll('.dv-channel-menu__actions button').length === 6 && Boolean(channelAction('加拐点')) && !channelAction('加拐点').disabled && Boolean(channelAction('取消该分组选中')), [...channelDemo.querySelectorAll('.dv-channel-menu__actions button')].map(node => node.textContent.trim() + (node.disabled ? '(灰)' : '')).join(' / '))
|
|
2786
|
-
// 「取消该分组选中」= deselect({ channelId, groupId }):先选中这一组,点一下按钮之后选中集清空
|
|
2787
|
-
channelView.selectFeatures({ type: 'channel', channelId: menuPayload.channelId, groupId: menuPayload.groupId })
|
|
2788
|
-
const selectedBeforeDeselect = channelView.getSelectionData().count
|
|
2789
|
-
channelAction('取消该分组选中')?.click()
|
|
2790
|
-
await wait(200)
|
|
2791
|
-
check('通道与数据:交互框的「取消该分组选中」走 deselect,选中集清空(deselect-group)', selectedBeforeDeselect > 0 && channelView.getSelectionData().count === 0, `${selectedBeforeDeselect} → ${channelView.getSelectionData().count}`)
|
|
2792
|
-
channelAction('拆成多组')?.click()
|
|
2793
|
-
await wait(300)
|
|
2794
|
-
const groupsAfterSplit = channelView.getChannelGroups(menuPayload?.channelId).length
|
|
2795
|
-
check('通道与数据:交互框的「拆成多组」走 runChannelAction,分组变多', groupsAfterSplit > groupsBeforeSplit, groupsBeforeSplit + ' → ' + groupsAfterSplit + ' 组')
|
|
2796
|
-
// 线路分配:当前组还有多条回线,下拉默认「拆成新分组」,点「分配」走 moveChannelLine
|
|
2797
|
-
channelDemo.querySelector('.dv-channel-menu__lines li button')?.click()
|
|
2798
|
-
await wait(300)
|
|
2799
|
-
const groupsAfterAssign = channelView.getChannelGroups(menuPayload?.channelId).length
|
|
2800
|
-
check('通道与数据:线路分配的「拆成新分组」走 moveChannelLine', groupsAfterAssign === groupsAfterSplit + 1, groupsAfterSplit + ' → ' + groupsAfterAssign + ' 组')
|
|
2801
|
-
await wait(560)
|
|
2802
|
-
const groupRequestPreview = channelField('channel-backend-request')
|
|
2803
|
-
check('通道与数据:分组动作生成整份 groups 覆盖请求(带动作、版本与幂等键)',
|
|
2804
|
-
groupRequestPreview.includes('PATCH /gis/channels/') &&
|
|
2805
|
-
groupRequestPreview.includes('/groups') &&
|
|
2806
|
-
groupRequestPreview.includes('"groups"') &&
|
|
2807
|
-
groupRequestPreview.includes('"lineIds"') &&
|
|
2808
|
-
groupRequestPreview.includes('"baseVersion"') &&
|
|
2809
|
-
groupRequestPreview.includes('"opId"') &&
|
|
2810
|
-
groupRequestPreview.includes('200 OK'),
|
|
2811
|
-
groupRequestPreview.slice(0, 200))
|
|
2812
|
-
check('通道与数据:分组保存成功回标准响应包与最新 groups 快照',
|
|
2813
|
-
['"code": "OK"', '"operation": "channel.groups"', '"transactionId"', '"result"', '"groupCount"', '"lineCount"', 'Idempotency-Key:', 'If-Match: "v'].every(key => groupRequestPreview.includes(key)),
|
|
2814
|
-
groupRequestPreview.slice(-300).replace(/\s+/g, ' '))
|
|
2815
|
-
// 点分组 = setCurrentChannelGroup(顺带选中这一组的回线)
|
|
2816
|
-
const groupsNow = channelView.getChannelGroups(menuPayload?.channelId)
|
|
2817
|
-
const currentId = channelView.getChannelContext(menuPayload?.channelId)?.groupId
|
|
2818
|
-
const targetGroup = groupsNow.find(group => String(group.id) !== String(currentId))
|
|
2819
|
-
const groupItems = [...channelDemo.querySelectorAll('.dv-channel-menu__groups li')]
|
|
2820
|
-
groupItems[groupsNow.findIndex(group => String(group.id) === String(targetGroup?.id))]?.click()
|
|
2821
|
-
await wait(300)
|
|
2822
|
-
const switchedContext = channelView.getChannelContext(menuPayload?.channelId)
|
|
2823
|
-
// getChannelGroups 每一项只给 lineIds(lineCount 是 getChannelContext().group 的字段),回线数按 lineIds.length 算
|
|
2824
|
-
const targetLineCount = targetGroup?.lineIds?.length ?? 0
|
|
2825
|
-
check('通道与数据:点分组 = setCurrentChannelGroup + 选中它的回线', Boolean(targetGroup) && switchedContext?.groupId === targetGroup.id && channelView.getSelections().length === targetLineCount, switchedContext ? '当前「' + switchedContext.groupId + '」/ 选中 ' + channelView.getSelections().length + ' 回线(目标「' + (targetGroup?.id ?? '—') + '」' + targetLineCount + ' 回)' : '没有上下文')
|
|
2847
|
+
const groupsBeforeSplit = channelView.getChannelGroups(menuPayload?.channelId).length
|
|
2848
|
+
const channelAction = text => [...channelDemo.querySelectorAll('.dv-channel-menu__actions button')].find(node => node.textContent.trim() === text)
|
|
2849
|
+
check('通道与数据:交互框列出 6 个 runChannelAction 动作(含取消该分组选中),拐点编辑已开(waypointEditable)', channelDemo.querySelectorAll('.dv-channel-menu__actions button').length === 6 && Boolean(channelAction('加拐点')) && !channelAction('加拐点').disabled && Boolean(channelAction('取消该分组选中')), [...channelDemo.querySelectorAll('.dv-channel-menu__actions button')].map(node => node.textContent.trim() + (node.disabled ? '(灰)' : '')).join(' / '))
|
|
2850
|
+
// 「取消该分组选中」= deselect({ channelId, groupId }):先选中这一组,点一下按钮之后选中集清空
|
|
2851
|
+
channelView.selectFeatures({ type: 'channel', channelId: menuPayload.channelId, groupId: menuPayload.groupId })
|
|
2852
|
+
const selectedBeforeDeselect = channelView.getSelectionData().count
|
|
2853
|
+
channelAction('取消该分组选中')?.click()
|
|
2854
|
+
await wait(200)
|
|
2855
|
+
check('通道与数据:交互框的「取消该分组选中」走 deselect,选中集清空(deselect-group)', selectedBeforeDeselect > 0 && channelView.getSelectionData().count === 0, `${selectedBeforeDeselect} → ${channelView.getSelectionData().count}`)
|
|
2856
|
+
channelAction('拆成多组')?.click()
|
|
2857
|
+
await wait(300)
|
|
2858
|
+
const groupsAfterSplit = channelView.getChannelGroups(menuPayload?.channelId).length
|
|
2859
|
+
check('通道与数据:交互框的「拆成多组」走 runChannelAction,分组变多', groupsAfterSplit > groupsBeforeSplit, groupsBeforeSplit + ' → ' + groupsAfterSplit + ' 组')
|
|
2860
|
+
// 线路分配:当前组还有多条回线,下拉默认「拆成新分组」,点「分配」走 moveChannelLine
|
|
2861
|
+
channelDemo.querySelector('.dv-channel-menu__lines li button')?.click()
|
|
2862
|
+
await wait(300)
|
|
2863
|
+
const groupsAfterAssign = channelView.getChannelGroups(menuPayload?.channelId).length
|
|
2864
|
+
check('通道与数据:线路分配的「拆成新分组」走 moveChannelLine', groupsAfterAssign === groupsAfterSplit + 1, groupsAfterSplit + ' → ' + groupsAfterAssign + ' 组')
|
|
2865
|
+
await wait(560)
|
|
2866
|
+
const groupRequestPreview = channelField('channel-backend-request')
|
|
2867
|
+
check('通道与数据:分组动作生成整份 groups 覆盖请求(带动作、版本与幂等键)',
|
|
2868
|
+
groupRequestPreview.includes('PATCH /gis/channels/') &&
|
|
2869
|
+
groupRequestPreview.includes('/groups') &&
|
|
2870
|
+
groupRequestPreview.includes('"groups"') &&
|
|
2871
|
+
groupRequestPreview.includes('"lineIds"') &&
|
|
2872
|
+
groupRequestPreview.includes('"baseVersion"') &&
|
|
2873
|
+
groupRequestPreview.includes('"opId"') &&
|
|
2874
|
+
groupRequestPreview.includes('200 OK'),
|
|
2875
|
+
groupRequestPreview.slice(0, 200))
|
|
2876
|
+
check('通道与数据:分组保存成功回标准响应包与最新 groups 快照',
|
|
2877
|
+
['"code": "OK"', '"operation": "channel.groups"', '"transactionId"', '"result"', '"groupCount"', '"lineCount"', 'Idempotency-Key:', 'If-Match: "v'].every(key => groupRequestPreview.includes(key)),
|
|
2878
|
+
groupRequestPreview.slice(-300).replace(/\s+/g, ' '))
|
|
2879
|
+
// 点分组 = setCurrentChannelGroup(顺带选中这一组的回线)
|
|
2880
|
+
const groupsNow = channelView.getChannelGroups(menuPayload?.channelId)
|
|
2881
|
+
const currentId = channelView.getChannelContext(menuPayload?.channelId)?.groupId
|
|
2882
|
+
const targetGroup = groupsNow.find(group => String(group.id) !== String(currentId))
|
|
2883
|
+
const groupItems = [...channelDemo.querySelectorAll('.dv-channel-menu__groups li')]
|
|
2884
|
+
groupItems[groupsNow.findIndex(group => String(group.id) === String(targetGroup?.id))]?.click()
|
|
2885
|
+
await wait(300)
|
|
2886
|
+
const switchedContext = channelView.getChannelContext(menuPayload?.channelId)
|
|
2887
|
+
// getChannelGroups 每一项只给 lineIds(lineCount 是 getChannelContext().group 的字段),回线数按 lineIds.length 算
|
|
2888
|
+
const targetLineCount = targetGroup?.lineIds?.length ?? 0
|
|
2889
|
+
check('通道与数据:点分组 = setCurrentChannelGroup + 选中它的回线', Boolean(targetGroup) && switchedContext?.groupId === targetGroup.id && channelView.getSelections().length === targetLineCount, switchedContext ? '当前「' + switchedContext.groupId + '」/ 选中 ' + channelView.getSelections().length + ' 回线(目标「' + (targetGroup?.id ?? '—') + '」' + targetLineCount + ' 回)' : '没有上下文')
|
|
2826
2890
|
// 右键落到节点上:onContextMenu 不带 channelContext,框自己收掉
|
|
2827
2891
|
channelRightClick(fromScreen)
|
|
2828
2892
|
await wait(200)
|
|
2829
2893
|
check('通道与数据:右键落到节点上时 channelContext 为空,框收掉', !channelDemo.querySelector('.dv-channel-menu') && channelSetup.channelMenu === null, '')
|
|
2830
|
-
// 再右键回回线上、按 Esc 也能收
|
|
2831
|
-
channelRightClick(menuScreen)
|
|
2832
|
-
const reopenedMenu = await until(() => channelDemo.querySelector('.dv-channel-menu'), 5000)
|
|
2833
|
-
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
2834
|
-
await wait(200)
|
|
2835
|
-
check('通道与数据:Esc 收框', Boolean(reopenedMenu) && !channelDemo.querySelector('.dv-channel-menu'), '')
|
|
2836
|
-
|
|
2837
|
-
// 一次拆几条:面板上的勾选块走 addChannelGroup(moveChannelLine 的批量版)
|
|
2838
|
-
const splitPicks = channelDemo.querySelector('[data-demo="add-channel-group-picks"]')
|
|
2839
|
-
const splitBoxes = () => [...(splitPicks?.querySelectorAll('input[type="checkbox"]') ?? [])]
|
|
2840
|
-
const splitButton = channelDemo.querySelector('[data-demo="add-channel-group"]')
|
|
2841
|
-
const splitReadout = channelDemo.querySelector('[data-demo="add-channel-group-text"]')
|
|
2842
|
-
const splitModeSelect = channelDemo.querySelector('[data-demo="add-channel-group-mode"]')
|
|
2843
|
-
const splitIdInput = channelDemo.querySelector('[data-demo="add-channel-group-id"]')
|
|
2844
|
-
const firstChannelId = channelView.getChannels()[0]?.channelId
|
|
2845
|
-
const groupsOfFirst = () => channelView.getChannelGroups(firstChannelId)
|
|
2846
|
-
const membersOfFirst = () => groupsOfFirst().reduce((sum, group) => sum + group.lineIds.length, 0)
|
|
2847
|
-
const idKeys = ids => ids.map(id => String(id)).sort().join(',')
|
|
2848
|
-
// 先并回一组:一条通道一组、三条回线,后面每一步都可预期
|
|
2849
|
-
;[...channelDemo.querySelectorAll('.dv-panel button')].find(node => node.textContent.trim() === '并回一组')?.click()
|
|
2850
|
-
await wait(320)
|
|
2851
|
-
const mergedGroups = groupsOfFirst()
|
|
2852
|
-
check('一次拆几条:勾选块列出第一条通道的全部回线,没勾时按钮禁用着', mergedGroups.length === 1 && splitBoxes().length === membersOfFirst() && splitButton?.disabled === true, `${mergedGroups.length} 组 / ${splitBoxes().length} 个勾选`)
|
|
2853
|
-
// 给这一组加一个拐点,下面才看得出「新分组照抄来源组当前的骨架」
|
|
2854
|
-
channelView.addChannelGroupWaypoint(firstChannelId, mergedGroups[0]?.id)
|
|
2855
|
-
await wait(320)
|
|
2856
|
-
const sourcePath = JSON.stringify(groupsOfFirst()[0]?.path ?? null)
|
|
2857
|
-
// 逐条勾(勾一次等一次重渲染):勾完从面板的状态里读回真正勾上了哪几条
|
|
2858
|
-
const clickBox = async (id) => {
|
|
2859
|
-
const node = splitBoxes().find(entry => String(entry.value) === String(id))
|
|
2860
|
-
if (node && !node.checked) {
|
|
2861
|
-
node.click()
|
|
2862
|
-
}
|
|
2863
|
-
await wait(60)
|
|
2864
|
-
}
|
|
2865
|
-
for (const id of channelSetup.splitCandidates.slice(0, 2).map(row => row.id)) {
|
|
2866
|
-
await clickBox(id)
|
|
2867
|
-
}
|
|
2868
|
-
const pickedIds = channelSetup.splitPicked.map(id => String(id))
|
|
2869
|
-
splitIdInput.value = '拆组-甲'
|
|
2870
|
-
splitIdInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
2871
|
-
await wait(80)
|
|
2872
|
-
splitButton.click()
|
|
2873
|
-
await wait(320)
|
|
2874
|
-
const afterSplit = groupsOfFirst()
|
|
2875
|
-
const landedGroup = afterSplit.find(group => String(group.id) === '拆组-甲')
|
|
2876
|
-
check('勾两条点一下:addChannelGroup 建出 options.id 指定的那一组,成员就是勾的那两条', pickedIds.length === 2 && Boolean(landedGroup) && idKeys(landedGroup.lineIds) === idKeys(pickedIds), `picked=${JSON.stringify(pickedIds)} landed=${JSON.stringify(landedGroup?.lineIds ?? null)} groups=${JSON.stringify(afterSplit.map(group => [group.id, group.lineIds]))}`)
|
|
2877
|
-
check('其余回线一条不丢(三条回线还在,只是分成两组)', membersOfFirst() === mergedGroups[0].lineIds.length && afterSplit.length === 2, `${membersOfFirst()} 回 / ${afterSplit.length} 组`)
|
|
2878
|
-
check('新这一组照抄来源组当前的骨架(拐点没丢、画面不会跳)', JSON.stringify(landedGroup?.path ?? null) === sourcePath && sourcePath !== '[]' && sourcePath !== 'null', sourcePath)
|
|
2879
|
-
check('读数报出新组与组数,日志里留下 addChannelGroup', splitReadout.textContent.includes('拆组-甲') && logText().includes('addChannelGroup'), splitReadout.textContent.trim().slice(0, 90))
|
|
2880
|
-
// 这两条已经单独成一组:不写 path 就什么都没改动,读数报「被拒绝」
|
|
2881
|
-
for (const id of pickedIds) {
|
|
2882
|
-
await clickBox(id)
|
|
2883
|
-
}
|
|
2884
|
-
await wait(80)
|
|
2885
|
-
splitButton.click()
|
|
2886
|
-
await wait(320)
|
|
2887
|
-
check('同两条再拆一次被拒(reason 说「已经是单独一组」)', splitReadout.textContent.includes('被拒绝') && splitReadout.textContent.includes('单独一组'), splitReadout.textContent.trim().slice(0, 90))
|
|
2888
|
-
// 写了 path 就不算「什么都没改」:切「直连」给这一组一份没有拐点的骨架
|
|
2889
|
-
splitModeSelect.value = 'straight'
|
|
2890
|
-
splitModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
2891
|
-
splitIdInput.value = ''
|
|
2892
|
-
splitIdInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
2893
|
-
for (const id of pickedIds) {
|
|
2894
|
-
await clickBox(id)
|
|
2895
|
-
}
|
|
2896
|
-
await wait(80)
|
|
2897
|
-
splitButton.click()
|
|
2898
|
-
await wait(320)
|
|
2894
|
+
// 再右键回回线上、按 Esc 也能收
|
|
2895
|
+
channelRightClick(menuScreen)
|
|
2896
|
+
const reopenedMenu = await until(() => channelDemo.querySelector('.dv-channel-menu'), 5000)
|
|
2897
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
2898
|
+
await wait(200)
|
|
2899
|
+
check('通道与数据:Esc 收框', Boolean(reopenedMenu) && !channelDemo.querySelector('.dv-channel-menu'), '')
|
|
2900
|
+
|
|
2901
|
+
// 一次拆几条:面板上的勾选块走 addChannelGroup(moveChannelLine 的批量版)
|
|
2902
|
+
const splitPicks = channelDemo.querySelector('[data-demo="add-channel-group-picks"]')
|
|
2903
|
+
const splitBoxes = () => [...(splitPicks?.querySelectorAll('input[type="checkbox"]') ?? [])]
|
|
2904
|
+
const splitButton = channelDemo.querySelector('[data-demo="add-channel-group"]')
|
|
2905
|
+
const splitReadout = channelDemo.querySelector('[data-demo="add-channel-group-text"]')
|
|
2906
|
+
const splitModeSelect = channelDemo.querySelector('[data-demo="add-channel-group-mode"]')
|
|
2907
|
+
const splitIdInput = channelDemo.querySelector('[data-demo="add-channel-group-id"]')
|
|
2908
|
+
const firstChannelId = channelView.getChannels()[0]?.channelId
|
|
2909
|
+
const groupsOfFirst = () => channelView.getChannelGroups(firstChannelId)
|
|
2910
|
+
const membersOfFirst = () => groupsOfFirst().reduce((sum, group) => sum + group.lineIds.length, 0)
|
|
2911
|
+
const idKeys = ids => ids.map(id => String(id)).sort().join(',')
|
|
2912
|
+
// 先并回一组:一条通道一组、三条回线,后面每一步都可预期
|
|
2913
|
+
;[...channelDemo.querySelectorAll('.dv-panel button')].find(node => node.textContent.trim() === '并回一组')?.click()
|
|
2914
|
+
await wait(320)
|
|
2915
|
+
const mergedGroups = groupsOfFirst()
|
|
2916
|
+
check('一次拆几条:勾选块列出第一条通道的全部回线,没勾时按钮禁用着', mergedGroups.length === 1 && splitBoxes().length === membersOfFirst() && splitButton?.disabled === true, `${mergedGroups.length} 组 / ${splitBoxes().length} 个勾选`)
|
|
2917
|
+
// 给这一组加一个拐点,下面才看得出「新分组照抄来源组当前的骨架」
|
|
2918
|
+
channelView.addChannelGroupWaypoint(firstChannelId, mergedGroups[0]?.id)
|
|
2919
|
+
await wait(320)
|
|
2920
|
+
const sourcePath = JSON.stringify(groupsOfFirst()[0]?.path ?? null)
|
|
2921
|
+
// 逐条勾(勾一次等一次重渲染):勾完从面板的状态里读回真正勾上了哪几条
|
|
2922
|
+
const clickBox = async (id) => {
|
|
2923
|
+
const node = splitBoxes().find(entry => String(entry.value) === String(id))
|
|
2924
|
+
if (node && !node.checked) {
|
|
2925
|
+
node.click()
|
|
2926
|
+
}
|
|
2927
|
+
await wait(60)
|
|
2928
|
+
}
|
|
2929
|
+
for (const id of channelSetup.splitCandidates.slice(0, 2).map(row => row.id)) {
|
|
2930
|
+
await clickBox(id)
|
|
2931
|
+
}
|
|
2932
|
+
const pickedIds = channelSetup.splitPicked.map(id => String(id))
|
|
2933
|
+
splitIdInput.value = '拆组-甲'
|
|
2934
|
+
splitIdInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
2935
|
+
await wait(80)
|
|
2936
|
+
splitButton.click()
|
|
2937
|
+
await wait(320)
|
|
2938
|
+
const afterSplit = groupsOfFirst()
|
|
2939
|
+
const landedGroup = afterSplit.find(group => String(group.id) === '拆组-甲')
|
|
2940
|
+
check('勾两条点一下:addChannelGroup 建出 options.id 指定的那一组,成员就是勾的那两条', pickedIds.length === 2 && Boolean(landedGroup) && idKeys(landedGroup.lineIds) === idKeys(pickedIds), `picked=${JSON.stringify(pickedIds)} landed=${JSON.stringify(landedGroup?.lineIds ?? null)} groups=${JSON.stringify(afterSplit.map(group => [group.id, group.lineIds]))}`)
|
|
2941
|
+
check('其余回线一条不丢(三条回线还在,只是分成两组)', membersOfFirst() === mergedGroups[0].lineIds.length && afterSplit.length === 2, `${membersOfFirst()} 回 / ${afterSplit.length} 组`)
|
|
2942
|
+
check('新这一组照抄来源组当前的骨架(拐点没丢、画面不会跳)', JSON.stringify(landedGroup?.path ?? null) === sourcePath && sourcePath !== '[]' && sourcePath !== 'null', sourcePath)
|
|
2943
|
+
check('读数报出新组与组数,日志里留下 addChannelGroup', splitReadout.textContent.includes('拆组-甲') && logText().includes('addChannelGroup'), splitReadout.textContent.trim().slice(0, 90))
|
|
2944
|
+
// 这两条已经单独成一组:不写 path 就什么都没改动,读数报「被拒绝」
|
|
2945
|
+
for (const id of pickedIds) {
|
|
2946
|
+
await clickBox(id)
|
|
2947
|
+
}
|
|
2948
|
+
await wait(80)
|
|
2949
|
+
splitButton.click()
|
|
2950
|
+
await wait(320)
|
|
2951
|
+
check('同两条再拆一次被拒(reason 说「已经是单独一组」)', splitReadout.textContent.includes('被拒绝') && splitReadout.textContent.includes('单独一组'), splitReadout.textContent.trim().slice(0, 90))
|
|
2952
|
+
// 写了 path 就不算「什么都没改」:切「直连」给这一组一份没有拐点的骨架
|
|
2953
|
+
splitModeSelect.value = 'straight'
|
|
2954
|
+
splitModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
2955
|
+
splitIdInput.value = ''
|
|
2956
|
+
splitIdInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
2957
|
+
for (const id of pickedIds) {
|
|
2958
|
+
await clickBox(id)
|
|
2959
|
+
}
|
|
2960
|
+
await wait(80)
|
|
2961
|
+
splitButton.click()
|
|
2962
|
+
await wait(320)
|
|
2899
2963
|
const straightGroup = groupsOfFirst().find(group => group.lineIds.length === 2)
|
|
2900
2964
|
check('切「直连」再拆:options.path 写 [] 就真的一个拐点都不带', Boolean(straightGroup) && straightGroup.path.length === 0, JSON.stringify(groupsOfFirst().map(group => [group.id, group.lineIds, group.path])))
|
|
2901
2965
|
// 线路分组弹框:行内确认移入只改草稿,底部确定时一次 splitChannelGroups 提交整份 groups
|
|
@@ -2967,60 +3031,60 @@
|
|
|
2967
3031
|
Boolean(confirmButton) && splitGroupCalls === 1 && membersOfFirst() === mergedGroups[0].lineIds.length && movedGroup?.lineIds.some(id => String(id) === String(movedLineId)) && logText().includes('splitChannelGroups'),
|
|
2968
3032
|
`调用 ${splitGroupCalls} 次 / ${confirmedGroups.length} 组 / ${membersOfFirst()} 回线 / ${movedLineId} → ${movedTargetId}`)
|
|
2969
3033
|
const waypointGroupId = straightGroup?.id ?? groupsOfFirst()[0]?.id
|
|
2970
|
-
const backendModeSelect = channelDemo.querySelector('[data-demo="channel-backend-mode"]')
|
|
2971
|
-
const backendRetryButton = channelDemo.querySelector('[data-demo="channel-backend-retry"]')
|
|
2972
|
-
backendModeSelect.value = 'network'
|
|
2973
|
-
backendModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
2974
|
-
channelView.addChannelGroupWaypoint(firstChannelId, waypointGroupId)
|
|
2975
|
-
await wait(560)
|
|
2976
|
-
const waypointFailedPreview = channelField('channel-backend-request')
|
|
2977
|
-
check('通道与数据:拐点 add 生成整份 path 请求,失败保留本地编辑并开放重试',
|
|
2978
|
-
waypointFailedPreview.includes(`PATCH /gis/channels/${firstChannelId}/groups/waypoints`) &&
|
|
2979
|
-
waypointFailedPreview.includes('"path"') &&
|
|
2980
|
-
waypointFailedPreview.includes('"channelId"') &&
|
|
2981
|
-
waypointFailedPreview.includes('"groupId"') &&
|
|
2982
|
-
waypointFailedPreview.includes('"baseVersion"') &&
|
|
2983
|
-
waypointFailedPreview.includes('"opId"') &&
|
|
2984
|
-
channelField('channel-waypoint-status').includes('失败') &&
|
|
2985
|
-
backendRetryButton.disabled === false,
|
|
2986
|
-
waypointFailedPreview.slice(0, 220))
|
|
2987
|
-
backendRetryButton.click()
|
|
2988
|
-
const waypointRetried = await until(() => channelField('channel-waypoint-status').includes('已同步'), 5000)
|
|
2989
|
-
check('通道与数据:拐点请求失败后可原样重试并同步到服务端', Boolean(waypointRetried), channelField('channel-waypoint-status'))
|
|
2990
|
-
const channelReplayButton = channelDemo.querySelector('[data-demo="channel-backend-replay"]')
|
|
2991
|
-
const channelVersionBeforeReplay = channelSetup.backendSnapshot?.version
|
|
2992
|
-
const channelAppliedBeforeReplay = channelSetup.backendSnapshot?.applied
|
|
2993
|
-
const channelReplayReady = Boolean(channelReplayButton) && !channelReplayButton.disabled
|
|
2994
|
-
channelReplayButton?.click()
|
|
2995
|
-
const channelReplayHit = await until(() => channelField('channel-backend-readout').includes('幂等回放') && channelField('channel-backend-request').includes('"idempotent": true'), 5000)
|
|
2996
|
-
check('通道与数据:重放原 opId 命中幂等缓存,版本与已应用数都不前进',
|
|
2997
|
-
channelReplayReady && Boolean(channelReplayHit) && channelSetup.backendSnapshot?.version === channelVersionBeforeReplay && channelSetup.backendSnapshot?.applied === channelAppliedBeforeReplay && channelField('channel-backend-request').includes('Idempotency-Key 命中'),
|
|
2998
|
-
`v${channelVersionBeforeReplay} / applied ${channelAppliedBeforeReplay} → v${channelSetup.backendSnapshot?.version} / applied ${channelSetup.backendSnapshot?.applied} / ${channelField('channel-backend-readout')}`)
|
|
2999
|
-
backendModeSelect.value = 'success'
|
|
3000
|
-
backendModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
3001
|
-
channelView.removeChannelGroupWaypoint(firstChannelId, waypointGroupId)
|
|
3002
|
-
await wait(560)
|
|
3003
|
-
const emptyPathPreview = channelField('channel-backend-request')
|
|
3004
|
-
const waypointPath = channelView.getChannelGroupWaypoints(firstChannelId, waypointGroupId)?.path ?? null
|
|
3005
|
-
check('通道与数据:删掉最后一个拐点会提交合法空 path(不是拒绝请求)',
|
|
3006
|
-
Array.isArray(waypointPath) && waypointPath.length === 0 &&
|
|
3007
|
-
emptyPathPreview.includes('"path": []') &&
|
|
3008
|
-
emptyPathPreview.includes('200 OK'),
|
|
3009
|
-
`path=${JSON.stringify(waypointPath)} / ${emptyPathPreview.slice(-120)}`)
|
|
3010
|
-
// 通道选中模式:切到 channel 之后,单击一条回线选中整条通道
|
|
3011
|
-
const channelLineTotal = channelView.getChannelGroups(menuPayload?.channelId).reduce((sum, group) => sum + group.lineIds.length, 0)
|
|
3012
|
-
const modeSelect = [...channelDemo.querySelectorAll('.dv-panel select')].find(node => [...node.options].some(option => option.value === 'channel'))
|
|
3013
|
-
if (modeSelect) {
|
|
3014
|
-
modeSelect.value = 'channel'
|
|
3015
|
-
modeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
3016
|
-
}
|
|
3034
|
+
const backendModeSelect = channelDemo.querySelector('[data-demo="channel-backend-mode"]')
|
|
3035
|
+
const backendRetryButton = channelDemo.querySelector('[data-demo="channel-backend-retry"]')
|
|
3036
|
+
backendModeSelect.value = 'network'
|
|
3037
|
+
backendModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
3038
|
+
channelView.addChannelGroupWaypoint(firstChannelId, waypointGroupId)
|
|
3039
|
+
await wait(560)
|
|
3040
|
+
const waypointFailedPreview = channelField('channel-backend-request')
|
|
3041
|
+
check('通道与数据:拐点 add 生成整份 path 请求,失败保留本地编辑并开放重试',
|
|
3042
|
+
waypointFailedPreview.includes(`PATCH /gis/channels/${firstChannelId}/groups/waypoints`) &&
|
|
3043
|
+
waypointFailedPreview.includes('"path"') &&
|
|
3044
|
+
waypointFailedPreview.includes('"channelId"') &&
|
|
3045
|
+
waypointFailedPreview.includes('"groupId"') &&
|
|
3046
|
+
waypointFailedPreview.includes('"baseVersion"') &&
|
|
3047
|
+
waypointFailedPreview.includes('"opId"') &&
|
|
3048
|
+
channelField('channel-waypoint-status').includes('失败') &&
|
|
3049
|
+
backendRetryButton.disabled === false,
|
|
3050
|
+
waypointFailedPreview.slice(0, 220))
|
|
3051
|
+
backendRetryButton.click()
|
|
3052
|
+
const waypointRetried = await until(() => channelField('channel-waypoint-status').includes('已同步'), 5000)
|
|
3053
|
+
check('通道与数据:拐点请求失败后可原样重试并同步到服务端', Boolean(waypointRetried), channelField('channel-waypoint-status'))
|
|
3054
|
+
const channelReplayButton = channelDemo.querySelector('[data-demo="channel-backend-replay"]')
|
|
3055
|
+
const channelVersionBeforeReplay = channelSetup.backendSnapshot?.version
|
|
3056
|
+
const channelAppliedBeforeReplay = channelSetup.backendSnapshot?.applied
|
|
3057
|
+
const channelReplayReady = Boolean(channelReplayButton) && !channelReplayButton.disabled
|
|
3058
|
+
channelReplayButton?.click()
|
|
3059
|
+
const channelReplayHit = await until(() => channelField('channel-backend-readout').includes('幂等回放') && channelField('channel-backend-request').includes('"idempotent": true'), 5000)
|
|
3060
|
+
check('通道与数据:重放原 opId 命中幂等缓存,版本与已应用数都不前进',
|
|
3061
|
+
channelReplayReady && Boolean(channelReplayHit) && channelSetup.backendSnapshot?.version === channelVersionBeforeReplay && channelSetup.backendSnapshot?.applied === channelAppliedBeforeReplay && channelField('channel-backend-request').includes('Idempotency-Key 命中'),
|
|
3062
|
+
`v${channelVersionBeforeReplay} / applied ${channelAppliedBeforeReplay} → v${channelSetup.backendSnapshot?.version} / applied ${channelSetup.backendSnapshot?.applied} / ${channelField('channel-backend-readout')}`)
|
|
3063
|
+
backendModeSelect.value = 'success'
|
|
3064
|
+
backendModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
3065
|
+
channelView.removeChannelGroupWaypoint(firstChannelId, waypointGroupId)
|
|
3066
|
+
await wait(560)
|
|
3067
|
+
const emptyPathPreview = channelField('channel-backend-request')
|
|
3068
|
+
const waypointPath = channelView.getChannelGroupWaypoints(firstChannelId, waypointGroupId)?.path ?? null
|
|
3069
|
+
check('通道与数据:删掉最后一个拐点会提交合法空 path(不是拒绝请求)',
|
|
3070
|
+
Array.isArray(waypointPath) && waypointPath.length === 0 &&
|
|
3071
|
+
emptyPathPreview.includes('"path": []') &&
|
|
3072
|
+
emptyPathPreview.includes('200 OK'),
|
|
3073
|
+
`path=${JSON.stringify(waypointPath)} / ${emptyPathPreview.slice(-120)}`)
|
|
3074
|
+
// 通道选中模式:切到 channel 之后,单击一条回线选中整条通道
|
|
3075
|
+
const channelLineTotal = channelView.getChannelGroups(menuPayload?.channelId).reduce((sum, group) => sum + group.lineIds.length, 0)
|
|
3076
|
+
const modeSelect = [...channelDemo.querySelectorAll('.dv-panel select')].find(node => [...node.options].some(option => option.value === 'channel'))
|
|
3077
|
+
if (modeSelect) {
|
|
3078
|
+
modeSelect.value = 'channel'
|
|
3079
|
+
modeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
3080
|
+
}
|
|
3017
3081
|
channelView.selectFeatures(null)
|
|
3018
|
-
await wait(200)
|
|
3019
|
-
const channelClick = (screen) => {
|
|
3020
|
-
const base = { bubbles: true, cancelable: true, composed: true, pointerId: 12, pointerType: 'mouse', isPrimary: true, clientX: channelBox.left + screen[0], clientY: channelBox.top + screen[1] }
|
|
3021
|
-
channelCanvas.dispatchEvent(new PointerEvent('pointerdown', { ...base, button: 0, buttons: 1 }))
|
|
3022
|
-
channelCanvas.dispatchEvent(new PointerEvent('pointerup', { ...base, button: 0, buttons: 0 }))
|
|
3023
|
-
}
|
|
3082
|
+
await wait(200)
|
|
3083
|
+
const channelClick = (screen) => {
|
|
3084
|
+
const base = { bubbles: true, cancelable: true, composed: true, pointerId: 12, pointerType: 'mouse', isPrimary: true, clientX: channelBox.left + screen[0], clientY: channelBox.top + screen[1] }
|
|
3085
|
+
channelCanvas.dispatchEvent(new PointerEvent('pointerdown', { ...base, button: 0, buttons: 1 }))
|
|
3086
|
+
channelCanvas.dispatchEvent(new PointerEvent('pointerup', { ...base, button: 0, buttons: 0 }))
|
|
3087
|
+
}
|
|
3024
3088
|
channelClick(menuScreen)
|
|
3025
3089
|
await wait(240)
|
|
3026
3090
|
const modeInfo = channelView.getSelectionData()
|
|
@@ -3043,205 +3107,205 @@
|
|
|
3043
3107
|
Boolean(waypointDragModeSelect) && Boolean(preparedWaypointGroups) && waypointDragModeApplied && channelField('channel-waypoint-drag-text').includes('已准备 2 组'),
|
|
3044
3108
|
`${channelView.getOptions().channelWaypointDragMode} / ${(preparedWaypointGroups ?? []).map(group => group.path.length).join(' / ')} / ${channelField('channel-waypoint-drag-text')}`)
|
|
3045
3109
|
// 删除:requestRemove 先出载荷(待确认期间数据不动),确认框由 demo 自己按载荷画
|
|
3046
|
-
const linesBeforeRemove = channelView.getData().lines.length
|
|
3047
|
-
const channelsBeforeRemove = channelView.getOptions().channels.length
|
|
3048
|
-
;[...channelDemo.querySelectorAll('.dv-panel button')].find(node => node.textContent.includes('删除选中'))?.click()
|
|
3049
|
-
const removeBox = await until(() => channelDemo.querySelector('.dv-remove-box'), 5000)
|
|
3050
|
-
check('通道与数据:删除选中先出删除载荷,待确认期间数据一个字节不动', Boolean(removeBox) && channelSetup.removeRequest?.phase === 'pending' && channelView.getData().lines.length === linesBeforeRemove, removeBox ? 'lineCount=' + channelView.getData().lines.length + ' / 选中 ' + (channelSetup.removeRequest?.counts?.lines ?? 0) + ' 回' : '确认框没弹出来')
|
|
3051
|
-
const removeActions = [...(removeBox?.querySelectorAll('.dv-channel-menu__actions button') ?? [])].map(node => node.textContent.trim())
|
|
3052
|
-
check('通道与数据:确认框给出「只删线路 / 整个分组 / 整条通道」三个范围', removeActions.some(text => text.includes('删除选中的线路')) && removeActions.some(text => text.includes('删除整个分组')) && removeActions.some(text => text.includes('删除整条通道')), removeActions.join(' / '))
|
|
3053
|
-
;[...(removeBox?.querySelectorAll('.dv-channel-menu__actions button') ?? [])].find(node => node.textContent.includes('删除整条通道'))?.click()
|
|
3054
|
-
const removedLines = await until(() => (channelView.getData().lines.length === linesBeforeRemove - channelLineTotal ? channelView.getData().lines.length : null), 6000)
|
|
3055
|
-
check('通道与数据:按「整条通道」确认之后整条通道的回线一起消失', removedLines === linesBeforeRemove - channelLineTotal && channelView.getOptions().channels.length === channelsBeforeRemove - 1 && !channelDemo.querySelector('.dv-remove-box'), linesBeforeRemove + ' → ' + channelView.getData().lines.length + ' 线 / 通道 ' + channelsBeforeRemove + ' → ' + channelView.getOptions().channels.length)
|
|
3056
|
-
const dtoButton = channelDemo.querySelector('[data-demo="channel-export-dto"]')
|
|
3057
|
-
check('通道与数据:导出区给出自定义格式按钮(defineExportFormat)', Boolean(dtoButton) && dtoButton.textContent.includes('后端 DTO'), dtoButton?.textContent.trim() ?? '没有按钮')
|
|
3058
|
-
dtoButton?.click()
|
|
3059
|
-
await wait(120)
|
|
3060
|
-
const dtoReadout = channelField('channel-export-readout')
|
|
3061
|
-
check('通道与数据:后端 DTO 按钮按注册的标准文档映射出业务列,并列出当前格式表',
|
|
3062
|
-
dtoReadout.includes('channel-backend-dto') && dtoReadout.includes('当前可注册表') && dtoReadout.includes('view / new / test / input / full / channel-backend-dto'),
|
|
3063
|
-
dtoReadout.slice(0, 180))
|
|
3064
|
-
}
|
|
3065
|
-
else {
|
|
3066
|
-
check('通道与数据:画布与视图都起来了', false, '没等到 canvas 或 view')
|
|
3067
|
-
}
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
// 2.8 编辑与保存(后端接口):分组增删改 / 拐点增删改 / 场站改坐标 —— 每一步生成一条待提交请求
|
|
3071
|
-
if (await clickTab('编辑与保存')) {
|
|
3072
|
-
const ready = await canvasReady()
|
|
3073
|
-
const demo = document.querySelector('.dv-demo')
|
|
3074
|
-
const setup = demo?.__vueParentComponent?.setupState
|
|
3075
|
-
const syncView = () => setup?.view ?? null
|
|
3076
|
-
const text = selector => demo.querySelector(selector)?.textContent.trim() ?? ''
|
|
3077
|
-
const preview = () => text('[data-demo="sync-preview"]')
|
|
3078
|
-
const queue = () => setup?.pending?.length ?? -1
|
|
3079
|
-
const firstChannelId = () => (syncView()?.getOptions().channels ?? [])[0]?.id
|
|
3080
|
-
const firstGroupPath = () => (syncView()?.getChannelGroups(firstChannelId()) ?? [])[0]?.path ?? []
|
|
3081
|
-
const firstChannelGroups = () => syncView()?.getChannelGroups(firstChannelId()) ?? []
|
|
3082
|
-
const spec = text('[data-demo="sync-spec"]')
|
|
3083
|
-
const fragments = ['/gis/points/{id}', '/gis/lines/{id}/path', '/gis/channels/{id}/groups/waypoints', '/gis/channels/{id}/groups', '/gis/documents']
|
|
3084
|
-
check('编辑与保存:接口约定把五条请求摊在面板上(场站 / 线路 / 通道组 / 分组 / 整份)', Boolean(ready) && Boolean(setup) && fragments.every(fragment => spec.includes(fragment)) && queue() === 0, text('[data-demo="sync-queue"]'))
|
|
3085
|
-
const groupReadout = () => setup?.waypointGroup ?? null
|
|
3086
|
-
const perLineWaypoints = () => (groupReadout()?.lines ?? []).map(line => line.waypoints.length)
|
|
3087
|
-
check('编辑与保存:getChannelGroupWaypoints 给出整组读数(整组共用 path + 组内每条回线偏移后的拐点)', (groupReadout()?.lines?.length ?? 0) >= 2 && groupReadout().path.length === firstGroupPath().length && text('[data-demo=sync-wp-group]').includes('waypoints'), `组内 ${groupReadout()?.lines?.length ?? 0} 条回线`)
|
|
3088
|
-
check('编辑与保存:整组读数的 scope=group、每条回线 derived=true(公共存储 / 各自派生分得开)', groupReadout()?.scope === 'group' && (groupReadout()?.lines ?? []).every(line => line.derived === true), `${groupReadout()?.scope} / ${(groupReadout()?.lines ?? []).length} 条回线`)
|
|
3089
|
-
// 场站位置:改坐标 → 数据真的动了,并生成一条 PATCH /gis/points/{id}
|
|
3090
|
-
const xBefore = syncView().getData().points[0].x
|
|
3091
|
-
const xInput = demo.querySelector('[data-demo="sync-station-x"]')
|
|
3092
|
-
xInput.value = String(Number(xBefore) + 0.5)
|
|
3093
|
-
xInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
3094
|
-
await wait(80)
|
|
3095
|
-
demo.querySelector('[data-demo="sync-station-move"]').click()
|
|
3096
|
-
await wait(220)
|
|
3097
|
-
check('编辑与保存:改场站坐标 → PATCH /gis/points/{id},body 带 op / pointId / x / y / baseVersion / opId', syncView().getData().points[0].x !== xBefore && preview().includes('point.move') && preview().includes('"pointId"') && preview().includes('"x"') && preview().includes('"baseVersion"') && preview().includes('"opId"') && queue() === 1, `队列 ${queue()} / ${preview().slice(0, 110)}`)
|
|
3098
|
-
// 位置对账:getMovedPoints 记下改过位置的场站,clearMovedPoints 对账之后读数归零
|
|
3099
|
-
check('编辑与保存:位置对账读数(getMovedPoints)列出改过位置的场站', text('[data-demo="sync-moved"]').includes('个场站改过位置') && text('[data-demo="sync-moved"]').includes('→'), text('[data-demo="sync-moved"]'))
|
|
3100
|
-
demo.querySelector('[data-demo="sync-moved-clear"]').click()
|
|
3101
|
-
await wait(140)
|
|
3102
|
-
check('编辑与保存:clearMovedPoints 对账之后读数归零(发完接口记成新基准)', text('[data-demo="sync-moved"]').includes('没有场站改过位置') && queue() === 1, text('[data-demo="sync-moved"]'))
|
|
3103
|
-
// 拐点:加一个 → 分组骨架多一个顶点;请求体给的是整份 path
|
|
3104
|
-
const waypointsBefore = firstGroupPath().length
|
|
3105
|
-
demo.querySelector('[data-demo="sync-wp-add"]').click()
|
|
3106
|
-
await wait(240)
|
|
3107
|
-
check('编辑与保存:加拐点走 addWaypoint,请求体是整份 path(channel.group.path / line.path)', firstGroupPath().length === waypointsBefore + 1 && preview().includes('"path"') && (preview().includes('channel.group.path') || preview().includes('line.path')) && queue() === 2, `path ${waypointsBefore} → ${firstGroupPath().length}`)
|
|
3108
|
-
check('编辑与保存:同组加拐点后每条回线都同步(读 getChannelGroupWaypoints().lines[].waypoints)', perLineWaypoints().length >= 2 && perLineWaypoints().every(count => count === firstGroupPath().length), `每条 ${perLineWaypoints().join(' / ')} 个拐点`)
|
|
3109
|
-
check('编辑与保存:怎么传给 API —— fetch + method / headers / JSON body 摊在面板上', text('[data-demo=sync-fetch]').includes('await fetch(') && text('[data-demo=sync-fetch]').includes('method:') && text('[data-demo=sync-fetch]').includes('application/json') && text('[data-demo=sync-fetch]').includes('JSON.stringify'), text('[data-demo=sync-fetch]').slice(0, 90))
|
|
3110
|
-
demo.querySelector('[data-demo=sync-pending-0]')?.click()
|
|
3111
|
-
await wait(120)
|
|
3112
|
-
check('编辑与保存:点待提交里的一条可以回看它的 body(预览切回那一条)', preview().includes('point.move') && text('[data-demo=sync-fetch]').includes('/gis/points/'), text('[data-demo=sync-preview]').slice(0, 80))
|
|
3113
|
-
// 改坐标:第 1 个拐点挪到别处(整份覆盖)
|
|
3114
|
-
const pathBefore = JSON.stringify(firstGroupPath())
|
|
3115
|
-
const pick = demo.querySelector('[data-demo="sync-wp-pick"]')
|
|
3116
|
-
pick.value = String(waypointsBefore + 1)
|
|
3117
|
-
pick.dispatchEvent(new Event('change', { bubbles: true }))
|
|
3118
|
-
await wait(80)
|
|
3119
|
-
const wpX = demo.querySelector('[data-demo="sync-wp-x"]')
|
|
3120
|
-
wpX.value = String(Number(wpX.value) + 0.4)
|
|
3121
|
-
wpX.dispatchEvent(new Event('input', { bubbles: true }))
|
|
3122
|
-
await wait(80)
|
|
3123
|
-
demo.querySelector('[data-demo="sync-wp-update"]').click()
|
|
3124
|
-
await wait(240)
|
|
3125
|
-
check('编辑与保存:改拐点坐标走 setChannelPath / setLineWaypoints,整份 path 覆盖', JSON.stringify(firstGroupPath()) !== pathBefore && queue() === 3, `队列 ${queue()}`)
|
|
3126
|
-
// 删拐点:回到加点之前的顶点数
|
|
3127
|
-
demo.querySelector('[data-demo="sync-wp-remove"]').click()
|
|
3128
|
-
await wait(240)
|
|
3129
|
-
check('编辑与保存:删拐点走 removeWaypoint(pathIndex 就是读数里那个)', firstGroupPath().length === waypointsBefore && queue() === 4, `path=${firstGroupPath().length}`)
|
|
3130
|
-
// 分组:勾一条拆成新组 → 整份 groups 声明进请求体;再并回去
|
|
3131
|
-
const picks = [...demo.querySelectorAll('[data-demo="sync-group-picks"] input[type="checkbox"]')]
|
|
3132
|
-
picks[picks.length - 1]?.click()
|
|
3133
|
-
await wait(120)
|
|
3134
|
-
demo.querySelector('[data-demo="sync-group-add"]').click()
|
|
3135
|
-
await wait(260)
|
|
3136
|
-
check('编辑与保存:addChannelGroup 拆出新组 → 请求体是整份 channel.groups(id / lineIds / path)', firstChannelGroups().length === 2 && preview().includes('channel.groups') && preview().includes('"lineIds"') && preview().includes('"path"') && queue() === 5, `${firstChannelGroups().length} 组`)
|
|
3137
|
-
const secondGroup = firstChannelGroups()[1]
|
|
3138
|
-
demo.querySelector(`[data-demo="sync-group-remove-${secondGroup.id}"]`)?.click()
|
|
3139
|
-
await wait(260)
|
|
3140
|
-
check('编辑与保存:删除分组走 mergeChannelGroups(整组并回去,组数回到 1)', firstChannelGroups().length === 1 && preview().includes('channel.groups') && queue() === 6, `${firstChannelGroups().length} 组`)
|
|
3141
|
-
// 整份保存:document = exportData({ format: "full" })
|
|
3142
|
-
demo.querySelector('[data-demo="sync-save-document"]').click()
|
|
3143
|
-
await wait(400)
|
|
3144
|
-
check('编辑与保存:整份保存 PUT /gis/documents(document 是八列标准文档)', preview().includes('document.save') && preview().includes('gis-view') && queue() === 7, text('[data-demo="sync-queue"]'))
|
|
3145
|
-
// 提交:一次带上 baseVersion / opId,服务端版本 +1、队列清零
|
|
3146
|
-
const versionBefore = setup.server.version
|
|
3147
|
-
demo.querySelector('[data-demo="sync-submit"]').click()
|
|
3148
|
-
await wait(240)
|
|
3149
|
-
check('编辑与保存:提交整批 → 服务端版本 +1、队列清零', queue() === 0 && setup.server.version === versionBefore + 1, `v${versionBefore} → v${setup.server.version}`)
|
|
3150
|
-
// 409:勾「撞版本」再提交,队列留着,可以回灌服务端那一份
|
|
3151
|
-
demo.querySelector('[data-demo="sync-conflict"]').click()
|
|
3152
|
-
await wait(60)
|
|
3153
|
-
demo.querySelector('[data-demo="sync-station-move"]').click()
|
|
3154
|
-
await wait(200)
|
|
3155
|
-
demo.querySelector('[data-demo="sync-submit"]').click()
|
|
3156
|
-
await wait(240)
|
|
3157
|
-
check('编辑与保存:撞版本 → 409、队列原样留着', text('[data-demo="sync-conflict-text"]').includes('409') && queue() === 1, text('[data-demo="sync-conflict-text"]').slice(0, 80))
|
|
3158
|
-
demo.querySelector('[data-demo="sync-pull"]').click()
|
|
3159
|
-
await wait(320)
|
|
3160
|
-
check('编辑与保存:回灌服务端那一份走 importData,队列清空', queue() === 0 && text('[data-demo="sync-conflict-text"]').includes('回灌'), text('[data-demo="sync-conflict-text"]').slice(0, 60))
|
|
3161
|
-
// 视口导出:只出屏幕上看得见的那一撮
|
|
3162
|
-
demo.querySelector('[data-demo="sync-export-view"]').click()
|
|
3163
|
-
await wait(200)
|
|
3164
|
-
// 视口导出:只出屏幕上看得见的那一撮
|
|
3165
|
-
demo.querySelector('[data-demo="sync-export-view"]').click()
|
|
3166
|
-
await wait(200)
|
|
3110
|
+
const linesBeforeRemove = channelView.getData().lines.length
|
|
3111
|
+
const channelsBeforeRemove = channelView.getOptions().channels.length
|
|
3112
|
+
;[...channelDemo.querySelectorAll('.dv-panel button')].find(node => node.textContent.includes('删除选中'))?.click()
|
|
3113
|
+
const removeBox = await until(() => channelDemo.querySelector('.dv-remove-box'), 5000)
|
|
3114
|
+
check('通道与数据:删除选中先出删除载荷,待确认期间数据一个字节不动', Boolean(removeBox) && channelSetup.removeRequest?.phase === 'pending' && channelView.getData().lines.length === linesBeforeRemove, removeBox ? 'lineCount=' + channelView.getData().lines.length + ' / 选中 ' + (channelSetup.removeRequest?.counts?.lines ?? 0) + ' 回' : '确认框没弹出来')
|
|
3115
|
+
const removeActions = [...(removeBox?.querySelectorAll('.dv-channel-menu__actions button') ?? [])].map(node => node.textContent.trim())
|
|
3116
|
+
check('通道与数据:确认框给出「只删线路 / 整个分组 / 整条通道」三个范围', removeActions.some(text => text.includes('删除选中的线路')) && removeActions.some(text => text.includes('删除整个分组')) && removeActions.some(text => text.includes('删除整条通道')), removeActions.join(' / '))
|
|
3117
|
+
;[...(removeBox?.querySelectorAll('.dv-channel-menu__actions button') ?? [])].find(node => node.textContent.includes('删除整条通道'))?.click()
|
|
3118
|
+
const removedLines = await until(() => (channelView.getData().lines.length === linesBeforeRemove - channelLineTotal ? channelView.getData().lines.length : null), 6000)
|
|
3119
|
+
check('通道与数据:按「整条通道」确认之后整条通道的回线一起消失', removedLines === linesBeforeRemove - channelLineTotal && channelView.getOptions().channels.length === channelsBeforeRemove - 1 && !channelDemo.querySelector('.dv-remove-box'), linesBeforeRemove + ' → ' + channelView.getData().lines.length + ' 线 / 通道 ' + channelsBeforeRemove + ' → ' + channelView.getOptions().channels.length)
|
|
3120
|
+
const dtoButton = channelDemo.querySelector('[data-demo="channel-export-dto"]')
|
|
3121
|
+
check('通道与数据:导出区给出自定义格式按钮(defineExportFormat)', Boolean(dtoButton) && dtoButton.textContent.includes('后端 DTO'), dtoButton?.textContent.trim() ?? '没有按钮')
|
|
3122
|
+
dtoButton?.click()
|
|
3123
|
+
await wait(120)
|
|
3124
|
+
const dtoReadout = channelField('channel-export-readout')
|
|
3125
|
+
check('通道与数据:后端 DTO 按钮按注册的标准文档映射出业务列,并列出当前格式表',
|
|
3126
|
+
dtoReadout.includes('channel-backend-dto') && dtoReadout.includes('当前可注册表') && dtoReadout.includes('view / new / test / input / full / channel-backend-dto'),
|
|
3127
|
+
dtoReadout.slice(0, 180))
|
|
3128
|
+
}
|
|
3129
|
+
else {
|
|
3130
|
+
check('通道与数据:画布与视图都起来了', false, '没等到 canvas 或 view')
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
|
|
3134
|
+
// 2.8 编辑与保存(后端接口):分组增删改 / 拐点增删改 / 场站改坐标 —— 每一步生成一条待提交请求
|
|
3135
|
+
if (await clickTab('编辑与保存')) {
|
|
3136
|
+
const ready = await canvasReady()
|
|
3137
|
+
const demo = document.querySelector('.dv-demo')
|
|
3138
|
+
const setup = demo?.__vueParentComponent?.setupState
|
|
3139
|
+
const syncView = () => setup?.view ?? null
|
|
3140
|
+
const text = selector => demo.querySelector(selector)?.textContent.trim() ?? ''
|
|
3141
|
+
const preview = () => text('[data-demo="sync-preview"]')
|
|
3142
|
+
const queue = () => setup?.pending?.length ?? -1
|
|
3143
|
+
const firstChannelId = () => (syncView()?.getOptions().channels ?? [])[0]?.id
|
|
3144
|
+
const firstGroupPath = () => (syncView()?.getChannelGroups(firstChannelId()) ?? [])[0]?.path ?? []
|
|
3145
|
+
const firstChannelGroups = () => syncView()?.getChannelGroups(firstChannelId()) ?? []
|
|
3146
|
+
const spec = text('[data-demo="sync-spec"]')
|
|
3147
|
+
const fragments = ['/gis/points/{id}', '/gis/lines/{id}/path', '/gis/channels/{id}/groups/waypoints', '/gis/channels/{id}/groups', '/gis/documents']
|
|
3148
|
+
check('编辑与保存:接口约定把五条请求摊在面板上(场站 / 线路 / 通道组 / 分组 / 整份)', Boolean(ready) && Boolean(setup) && fragments.every(fragment => spec.includes(fragment)) && queue() === 0, text('[data-demo="sync-queue"]'))
|
|
3149
|
+
const groupReadout = () => setup?.waypointGroup ?? null
|
|
3150
|
+
const perLineWaypoints = () => (groupReadout()?.lines ?? []).map(line => line.waypoints.length)
|
|
3151
|
+
check('编辑与保存:getChannelGroupWaypoints 给出整组读数(整组共用 path + 组内每条回线偏移后的拐点)', (groupReadout()?.lines?.length ?? 0) >= 2 && groupReadout().path.length === firstGroupPath().length && text('[data-demo=sync-wp-group]').includes('waypoints'), `组内 ${groupReadout()?.lines?.length ?? 0} 条回线`)
|
|
3152
|
+
check('编辑与保存:整组读数的 scope=group、每条回线 derived=true(公共存储 / 各自派生分得开)', groupReadout()?.scope === 'group' && (groupReadout()?.lines ?? []).every(line => line.derived === true), `${groupReadout()?.scope} / ${(groupReadout()?.lines ?? []).length} 条回线`)
|
|
3153
|
+
// 场站位置:改坐标 → 数据真的动了,并生成一条 PATCH /gis/points/{id}
|
|
3154
|
+
const xBefore = syncView().getData().points[0].x
|
|
3155
|
+
const xInput = demo.querySelector('[data-demo="sync-station-x"]')
|
|
3156
|
+
xInput.value = String(Number(xBefore) + 0.5)
|
|
3157
|
+
xInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
3158
|
+
await wait(80)
|
|
3159
|
+
demo.querySelector('[data-demo="sync-station-move"]').click()
|
|
3160
|
+
await wait(220)
|
|
3161
|
+
check('编辑与保存:改场站坐标 → PATCH /gis/points/{id},body 带 op / pointId / x / y / baseVersion / opId', syncView().getData().points[0].x !== xBefore && preview().includes('point.move') && preview().includes('"pointId"') && preview().includes('"x"') && preview().includes('"baseVersion"') && preview().includes('"opId"') && queue() === 1, `队列 ${queue()} / ${preview().slice(0, 110)}`)
|
|
3162
|
+
// 位置对账:getMovedPoints 记下改过位置的场站,clearMovedPoints 对账之后读数归零
|
|
3163
|
+
check('编辑与保存:位置对账读数(getMovedPoints)列出改过位置的场站', text('[data-demo="sync-moved"]').includes('个场站改过位置') && text('[data-demo="sync-moved"]').includes('→'), text('[data-demo="sync-moved"]'))
|
|
3164
|
+
demo.querySelector('[data-demo="sync-moved-clear"]').click()
|
|
3165
|
+
await wait(140)
|
|
3166
|
+
check('编辑与保存:clearMovedPoints 对账之后读数归零(发完接口记成新基准)', text('[data-demo="sync-moved"]').includes('没有场站改过位置') && queue() === 1, text('[data-demo="sync-moved"]'))
|
|
3167
|
+
// 拐点:加一个 → 分组骨架多一个顶点;请求体给的是整份 path
|
|
3168
|
+
const waypointsBefore = firstGroupPath().length
|
|
3169
|
+
demo.querySelector('[data-demo="sync-wp-add"]').click()
|
|
3170
|
+
await wait(240)
|
|
3171
|
+
check('编辑与保存:加拐点走 addWaypoint,请求体是整份 path(channel.group.path / line.path)', firstGroupPath().length === waypointsBefore + 1 && preview().includes('"path"') && (preview().includes('channel.group.path') || preview().includes('line.path')) && queue() === 2, `path ${waypointsBefore} → ${firstGroupPath().length}`)
|
|
3172
|
+
check('编辑与保存:同组加拐点后每条回线都同步(读 getChannelGroupWaypoints().lines[].waypoints)', perLineWaypoints().length >= 2 && perLineWaypoints().every(count => count === firstGroupPath().length), `每条 ${perLineWaypoints().join(' / ')} 个拐点`)
|
|
3173
|
+
check('编辑与保存:怎么传给 API —— fetch + method / headers / JSON body 摊在面板上', text('[data-demo=sync-fetch]').includes('await fetch(') && text('[data-demo=sync-fetch]').includes('method:') && text('[data-demo=sync-fetch]').includes('application/json') && text('[data-demo=sync-fetch]').includes('JSON.stringify'), text('[data-demo=sync-fetch]').slice(0, 90))
|
|
3174
|
+
demo.querySelector('[data-demo=sync-pending-0]')?.click()
|
|
3175
|
+
await wait(120)
|
|
3176
|
+
check('编辑与保存:点待提交里的一条可以回看它的 body(预览切回那一条)', preview().includes('point.move') && text('[data-demo=sync-fetch]').includes('/gis/points/'), text('[data-demo=sync-preview]').slice(0, 80))
|
|
3177
|
+
// 改坐标:第 1 个拐点挪到别处(整份覆盖)
|
|
3178
|
+
const pathBefore = JSON.stringify(firstGroupPath())
|
|
3179
|
+
const pick = demo.querySelector('[data-demo="sync-wp-pick"]')
|
|
3180
|
+
pick.value = String(waypointsBefore + 1)
|
|
3181
|
+
pick.dispatchEvent(new Event('change', { bubbles: true }))
|
|
3182
|
+
await wait(80)
|
|
3183
|
+
const wpX = demo.querySelector('[data-demo="sync-wp-x"]')
|
|
3184
|
+
wpX.value = String(Number(wpX.value) + 0.4)
|
|
3185
|
+
wpX.dispatchEvent(new Event('input', { bubbles: true }))
|
|
3186
|
+
await wait(80)
|
|
3187
|
+
demo.querySelector('[data-demo="sync-wp-update"]').click()
|
|
3188
|
+
await wait(240)
|
|
3189
|
+
check('编辑与保存:改拐点坐标走 setChannelPath / setLineWaypoints,整份 path 覆盖', JSON.stringify(firstGroupPath()) !== pathBefore && queue() === 3, `队列 ${queue()}`)
|
|
3190
|
+
// 删拐点:回到加点之前的顶点数
|
|
3191
|
+
demo.querySelector('[data-demo="sync-wp-remove"]').click()
|
|
3192
|
+
await wait(240)
|
|
3193
|
+
check('编辑与保存:删拐点走 removeWaypoint(pathIndex 就是读数里那个)', firstGroupPath().length === waypointsBefore && queue() === 4, `path=${firstGroupPath().length}`)
|
|
3194
|
+
// 分组:勾一条拆成新组 → 整份 groups 声明进请求体;再并回去
|
|
3195
|
+
const picks = [...demo.querySelectorAll('[data-demo="sync-group-picks"] input[type="checkbox"]')]
|
|
3196
|
+
picks[picks.length - 1]?.click()
|
|
3197
|
+
await wait(120)
|
|
3198
|
+
demo.querySelector('[data-demo="sync-group-add"]').click()
|
|
3199
|
+
await wait(260)
|
|
3200
|
+
check('编辑与保存:addChannelGroup 拆出新组 → 请求体是整份 channel.groups(id / lineIds / path)', firstChannelGroups().length === 2 && preview().includes('channel.groups') && preview().includes('"lineIds"') && preview().includes('"path"') && queue() === 5, `${firstChannelGroups().length} 组`)
|
|
3201
|
+
const secondGroup = firstChannelGroups()[1]
|
|
3202
|
+
demo.querySelector(`[data-demo="sync-group-remove-${secondGroup.id}"]`)?.click()
|
|
3203
|
+
await wait(260)
|
|
3204
|
+
check('编辑与保存:删除分组走 mergeChannelGroups(整组并回去,组数回到 1)', firstChannelGroups().length === 1 && preview().includes('channel.groups') && queue() === 6, `${firstChannelGroups().length} 组`)
|
|
3205
|
+
// 整份保存:document = exportData({ format: "full" })
|
|
3206
|
+
demo.querySelector('[data-demo="sync-save-document"]').click()
|
|
3207
|
+
await wait(400)
|
|
3208
|
+
check('编辑与保存:整份保存 PUT /gis/documents(document 是八列标准文档)', preview().includes('document.save') && preview().includes('gis-view') && queue() === 7, text('[data-demo="sync-queue"]'))
|
|
3209
|
+
// 提交:一次带上 baseVersion / opId,服务端版本 +1、队列清零
|
|
3210
|
+
const versionBefore = setup.server.version
|
|
3211
|
+
demo.querySelector('[data-demo="sync-submit"]').click()
|
|
3212
|
+
await wait(240)
|
|
3213
|
+
check('编辑与保存:提交整批 → 服务端版本 +1、队列清零', queue() === 0 && setup.server.version === versionBefore + 1, `v${versionBefore} → v${setup.server.version}`)
|
|
3214
|
+
// 409:勾「撞版本」再提交,队列留着,可以回灌服务端那一份
|
|
3215
|
+
demo.querySelector('[data-demo="sync-conflict"]').click()
|
|
3216
|
+
await wait(60)
|
|
3217
|
+
demo.querySelector('[data-demo="sync-station-move"]').click()
|
|
3218
|
+
await wait(200)
|
|
3219
|
+
demo.querySelector('[data-demo="sync-submit"]').click()
|
|
3220
|
+
await wait(240)
|
|
3221
|
+
check('编辑与保存:撞版本 → 409、队列原样留着', text('[data-demo="sync-conflict-text"]').includes('409') && queue() === 1, text('[data-demo="sync-conflict-text"]').slice(0, 80))
|
|
3222
|
+
demo.querySelector('[data-demo="sync-pull"]').click()
|
|
3223
|
+
await wait(320)
|
|
3224
|
+
check('编辑与保存:回灌服务端那一份走 importData,队列清空', queue() === 0 && text('[data-demo="sync-conflict-text"]').includes('回灌'), text('[data-demo="sync-conflict-text"]').slice(0, 60))
|
|
3225
|
+
// 视口导出:只出屏幕上看得见的那一撮
|
|
3226
|
+
demo.querySelector('[data-demo="sync-export-view"]').click()
|
|
3227
|
+
await wait(200)
|
|
3228
|
+
// 视口导出:只出屏幕上看得见的那一撮
|
|
3229
|
+
demo.querySelector('[data-demo="sync-export-view"]').click()
|
|
3230
|
+
await wait(200)
|
|
3167
3231
|
check('编辑与保存:exportData({ scope: viewport }) 出视口内的节点 / 线路 / 通道 + viewRect 读数', text('[data-demo="sync-viewport-text"]').includes('节点') && text('[data-demo="sync-viewport-text"]').includes('线路') && text('[data-demo="sync-viewport-sample"]').includes('viewRect'), text('[data-demo="sync-viewport-text"]'))
|
|
3168
|
-
// 推送策略:默认攒批;「改完就传」按同一目标合并 + 延迟自动发;画布拖动只在松手落地时进队列
|
|
3169
|
-
const autoToggle = demo.querySelector('[data-demo="sync-auto"]')
|
|
3170
|
-
const pushMode = () => text('[data-demo="sync-push-mode"]')
|
|
3171
|
-
check('编辑与保存:推送策略面板默认攒批(改动只进待提交,点提交一起传)',
|
|
3172
|
-
Boolean(autoToggle) && setup.autoPush === false && pushMode().includes('攒批') && queue() === 0,
|
|
3173
|
-
pushMode())
|
|
3174
|
-
// 拖节点:中间态(start / drag)一条请求都不发,松手落地才进一条 point.move
|
|
3175
|
-
const dragCanvas = demo.querySelector('.dv-canvas canvas') ?? demo.querySelector('canvas')
|
|
3176
|
-
const dragBox = dragCanvas.getBoundingClientRect()
|
|
3177
|
-
const dragPoint = syncView().getData().points[0]
|
|
3178
|
-
const dragAt = syncView().project(dragPoint.x, dragPoint.y)
|
|
3179
|
-
const dragFrom = { clientX: dragBox.left + dragAt[0], clientY: dragBox.top + dragAt[1] }
|
|
3180
|
-
const dragTo = { clientX: dragFrom.clientX + 26, clientY: dragFrom.clientY + 14 }
|
|
3181
|
-
const dragFire = (type, at, buttons) => dragCanvas.dispatchEvent(new PointerEvent(type, {
|
|
3182
|
-
bubbles: true, cancelable: true, composed: true, pointerId: 81, pointerType: 'mouse', isPrimary: true, button: 0, buttons, ...at
|
|
3183
|
-
}))
|
|
3184
|
-
dragFire('pointerdown', dragFrom, 1)
|
|
3185
|
-
dragFire('pointermove', dragTo, 1)
|
|
3186
|
-
await wait(140)
|
|
3187
|
-
const dragMidQueue = queue()
|
|
3188
|
-
const dragMidText = text('[data-demo="sync-drag"]')
|
|
3189
|
-
dragFire('pointerup', dragTo, 0)
|
|
3190
|
-
await wait(220)
|
|
3191
|
-
check('编辑与保存:拖节点中间态一条请求都不发(start / drag 只写读数),松手落地才发一条 point.move',
|
|
3192
|
-
dragMidQueue === 0 && dragMidText.includes('拖动中') && queue() === 1 && text('[data-demo="sync-drag"]').includes('松手落地'),
|
|
3193
|
-
`中间 ${dragMidQueue} 条 → 松手 ${queue()} 条 / ${text('[data-demo="sync-drag"]')}`)
|
|
3194
|
-
demo.querySelector('[data-demo="sync-clear"]').click()
|
|
3195
|
-
await wait(120)
|
|
3196
|
-
// 改完就传:连改两次同一个场站 → 同一目标只留最后一份,延迟一小段自动发出
|
|
3197
|
-
autoToggle.click()
|
|
3198
|
-
await wait(80)
|
|
3199
|
-
check('编辑与保存:勾上「改完就传」→ 策略切到自动推送(同一目标合并成最后一份)',
|
|
3200
|
-
setup.autoPush === true && pushMode().includes('改完就传'),
|
|
3201
|
-
pushMode())
|
|
3202
|
-
const mergedBefore = setup.mergedCount
|
|
3203
|
-
demo.querySelector('[data-demo="sync-merge-demo"]').click()
|
|
3204
|
-
await wait(80)
|
|
3205
|
-
check('编辑与保存:连改两次同一个场站 → 同一目标只留最后一份(队列 1 条、合并省掉 1 条)',
|
|
3206
|
-
setup.mergedCount === mergedBefore + 1 && queue() === 1,
|
|
3207
|
-
`合并 ${setup.mergedCount - mergedBefore} 条 / 队列 ${queue()}`)
|
|
3208
|
-
const pushedBefore = setup.pushedCount
|
|
3209
|
-
const versionBeforeAuto = setup.server.version
|
|
3210
|
-
await wait(420)
|
|
3211
|
-
check('编辑与保存:改完就传延迟一小段自动发出(队列清零、服务端 +1)',
|
|
3212
|
-
queue() === 0 && setup.pushedCount === pushedBefore + 1 && setup.server.version === versionBeforeAuto + 1,
|
|
3213
|
-
`自动发出 ${setup.pushedCount - pushedBefore} 条 / v${versionBeforeAuto} → v${setup.server.version}`)
|
|
3214
|
-
autoToggle.click()
|
|
3215
|
-
await wait(80)
|
|
3216
|
-
check('编辑与保存:关掉「改完就传」→ 回到攒批(改动不再自动发)',
|
|
3217
|
-
setup.autoPush === false && pushMode().includes('攒批'),
|
|
3218
|
-
pushMode())
|
|
3219
|
-
// 通道拐点:声明与导出文档里的分组 path 跟着一起动(不再弹回旧骨架)
|
|
3220
|
-
const wpChannelId = firstChannelId()
|
|
3221
|
-
const livePath = () => (syncView().getChannelGroups(wpChannelId) ?? [])[0]?.path ?? []
|
|
3222
|
-
const declaredPath = () => (syncView().getOptions().channels ?? []).find(channel => String(channel.id) === String(wpChannelId))?.groups?.[0]?.path ?? []
|
|
3223
|
-
const exportedPath = () => (syncView().exportData({ format: 'full' }).channels ?? []).find(channel => String(channel.id) === String(wpChannelId))?.groups?.[0]?.path ?? []
|
|
3224
|
-
demo.querySelector('[data-demo="sync-wp-add"]').click()
|
|
3225
|
-
await wait(240)
|
|
3226
|
-
const liveWp = livePath()[0] ?? null
|
|
3227
|
-
check('编辑与保存:加通道拐点后声明与整份导出文档里的分组 path 都同步(不再弹回旧骨架)',
|
|
3228
|
-
livePath().length > 0 && declaredPath().length === livePath().length && exportedPath().length === livePath().length,
|
|
3229
|
-
`live ${livePath().length} / options ${declaredPath().length} / doc ${exportedPath().length}`)
|
|
3230
|
-
demo.querySelector('[data-demo="sync-wp-remove"]').click()
|
|
3231
|
-
await wait(240)
|
|
3232
|
-
demo.querySelector('[data-demo="sync-clear"]').click()
|
|
3233
|
-
await wait(120)
|
|
3234
|
-
// 组件口径:分过组的通道 setChannelPath 写第一组的声明(顶层 path 会被分组声明盖住)
|
|
3235
|
-
const setPathPoint = liveWp ? [liveWp[0] + 0.5, liveWp[1] + 0.5] : [0, 0]
|
|
3236
|
-
syncView().setChannelPath(wpChannelId, [setPathPoint])
|
|
3237
|
-
await wait(160)
|
|
3238
|
-
check('编辑与保存:setChannelPath 写分过组通道的第一组骨架(不是被分组声明盖住的顶层 path)',
|
|
3239
|
-
livePath().length === 1 && Math.abs(livePath()[0][0] - setPathPoint[0]) < 1e-9 && declaredPath().length === 1,
|
|
3240
|
-
`path ${JSON.stringify(livePath())}`)
|
|
3241
|
-
}
|
|
3242
|
-
else {
|
|
3243
|
-
check('编辑与保存:画布与视图都起来了', false, '没等到 canvas 或 view')
|
|
3244
|
-
}
|
|
3232
|
+
// 推送策略:默认攒批;「改完就传」按同一目标合并 + 延迟自动发;画布拖动只在松手落地时进队列
|
|
3233
|
+
const autoToggle = demo.querySelector('[data-demo="sync-auto"]')
|
|
3234
|
+
const pushMode = () => text('[data-demo="sync-push-mode"]')
|
|
3235
|
+
check('编辑与保存:推送策略面板默认攒批(改动只进待提交,点提交一起传)',
|
|
3236
|
+
Boolean(autoToggle) && setup.autoPush === false && pushMode().includes('攒批') && queue() === 0,
|
|
3237
|
+
pushMode())
|
|
3238
|
+
// 拖节点:中间态(start / drag)一条请求都不发,松手落地才进一条 point.move
|
|
3239
|
+
const dragCanvas = demo.querySelector('.dv-canvas canvas') ?? demo.querySelector('canvas')
|
|
3240
|
+
const dragBox = dragCanvas.getBoundingClientRect()
|
|
3241
|
+
const dragPoint = syncView().getData().points[0]
|
|
3242
|
+
const dragAt = syncView().project(dragPoint.x, dragPoint.y)
|
|
3243
|
+
const dragFrom = { clientX: dragBox.left + dragAt[0], clientY: dragBox.top + dragAt[1] }
|
|
3244
|
+
const dragTo = { clientX: dragFrom.clientX + 26, clientY: dragFrom.clientY + 14 }
|
|
3245
|
+
const dragFire = (type, at, buttons) => dragCanvas.dispatchEvent(new PointerEvent(type, {
|
|
3246
|
+
bubbles: true, cancelable: true, composed: true, pointerId: 81, pointerType: 'mouse', isPrimary: true, button: 0, buttons, ...at
|
|
3247
|
+
}))
|
|
3248
|
+
dragFire('pointerdown', dragFrom, 1)
|
|
3249
|
+
dragFire('pointermove', dragTo, 1)
|
|
3250
|
+
await wait(140)
|
|
3251
|
+
const dragMidQueue = queue()
|
|
3252
|
+
const dragMidText = text('[data-demo="sync-drag"]')
|
|
3253
|
+
dragFire('pointerup', dragTo, 0)
|
|
3254
|
+
await wait(220)
|
|
3255
|
+
check('编辑与保存:拖节点中间态一条请求都不发(start / drag 只写读数),松手落地才发一条 point.move',
|
|
3256
|
+
dragMidQueue === 0 && dragMidText.includes('拖动中') && queue() === 1 && text('[data-demo="sync-drag"]').includes('松手落地'),
|
|
3257
|
+
`中间 ${dragMidQueue} 条 → 松手 ${queue()} 条 / ${text('[data-demo="sync-drag"]')}`)
|
|
3258
|
+
demo.querySelector('[data-demo="sync-clear"]').click()
|
|
3259
|
+
await wait(120)
|
|
3260
|
+
// 改完就传:连改两次同一个场站 → 同一目标只留最后一份,延迟一小段自动发出
|
|
3261
|
+
autoToggle.click()
|
|
3262
|
+
await wait(80)
|
|
3263
|
+
check('编辑与保存:勾上「改完就传」→ 策略切到自动推送(同一目标合并成最后一份)',
|
|
3264
|
+
setup.autoPush === true && pushMode().includes('改完就传'),
|
|
3265
|
+
pushMode())
|
|
3266
|
+
const mergedBefore = setup.mergedCount
|
|
3267
|
+
demo.querySelector('[data-demo="sync-merge-demo"]').click()
|
|
3268
|
+
await wait(80)
|
|
3269
|
+
check('编辑与保存:连改两次同一个场站 → 同一目标只留最后一份(队列 1 条、合并省掉 1 条)',
|
|
3270
|
+
setup.mergedCount === mergedBefore + 1 && queue() === 1,
|
|
3271
|
+
`合并 ${setup.mergedCount - mergedBefore} 条 / 队列 ${queue()}`)
|
|
3272
|
+
const pushedBefore = setup.pushedCount
|
|
3273
|
+
const versionBeforeAuto = setup.server.version
|
|
3274
|
+
await wait(420)
|
|
3275
|
+
check('编辑与保存:改完就传延迟一小段自动发出(队列清零、服务端 +1)',
|
|
3276
|
+
queue() === 0 && setup.pushedCount === pushedBefore + 1 && setup.server.version === versionBeforeAuto + 1,
|
|
3277
|
+
`自动发出 ${setup.pushedCount - pushedBefore} 条 / v${versionBeforeAuto} → v${setup.server.version}`)
|
|
3278
|
+
autoToggle.click()
|
|
3279
|
+
await wait(80)
|
|
3280
|
+
check('编辑与保存:关掉「改完就传」→ 回到攒批(改动不再自动发)',
|
|
3281
|
+
setup.autoPush === false && pushMode().includes('攒批'),
|
|
3282
|
+
pushMode())
|
|
3283
|
+
// 通道拐点:声明与导出文档里的分组 path 跟着一起动(不再弹回旧骨架)
|
|
3284
|
+
const wpChannelId = firstChannelId()
|
|
3285
|
+
const livePath = () => (syncView().getChannelGroups(wpChannelId) ?? [])[0]?.path ?? []
|
|
3286
|
+
const declaredPath = () => (syncView().getOptions().channels ?? []).find(channel => String(channel.id) === String(wpChannelId))?.groups?.[0]?.path ?? []
|
|
3287
|
+
const exportedPath = () => (syncView().exportData({ format: 'full' }).channels ?? []).find(channel => String(channel.id) === String(wpChannelId))?.groups?.[0]?.path ?? []
|
|
3288
|
+
demo.querySelector('[data-demo="sync-wp-add"]').click()
|
|
3289
|
+
await wait(240)
|
|
3290
|
+
const liveWp = livePath()[0] ?? null
|
|
3291
|
+
check('编辑与保存:加通道拐点后声明与整份导出文档里的分组 path 都同步(不再弹回旧骨架)',
|
|
3292
|
+
livePath().length > 0 && declaredPath().length === livePath().length && exportedPath().length === livePath().length,
|
|
3293
|
+
`live ${livePath().length} / options ${declaredPath().length} / doc ${exportedPath().length}`)
|
|
3294
|
+
demo.querySelector('[data-demo="sync-wp-remove"]').click()
|
|
3295
|
+
await wait(240)
|
|
3296
|
+
demo.querySelector('[data-demo="sync-clear"]').click()
|
|
3297
|
+
await wait(120)
|
|
3298
|
+
// 组件口径:分过组的通道 setChannelPath 写第一组的声明(顶层 path 会被分组声明盖住)
|
|
3299
|
+
const setPathPoint = liveWp ? [liveWp[0] + 0.5, liveWp[1] + 0.5] : [0, 0]
|
|
3300
|
+
syncView().setChannelPath(wpChannelId, [setPathPoint])
|
|
3301
|
+
await wait(160)
|
|
3302
|
+
check('编辑与保存:setChannelPath 写分过组通道的第一组骨架(不是被分组声明盖住的顶层 path)',
|
|
3303
|
+
livePath().length === 1 && Math.abs(livePath()[0][0] - setPathPoint[0]) < 1e-9 && declaredPath().length === 1,
|
|
3304
|
+
`path ${JSON.stringify(livePath())}`)
|
|
3305
|
+
}
|
|
3306
|
+
else {
|
|
3307
|
+
check('编辑与保存:画布与视图都起来了', false, '没等到 canvas 或 view')
|
|
3308
|
+
}
|
|
3245
3309
|
// 2.9 综合示例的「实例方法速查」面板:整份清单、搜索、点一条填表达式、求值、options 清单
|
|
3246
3310
|
if (await clickTab('综合')) {
|
|
3247
3311
|
const ready = await canvasReady()
|
|
@@ -3377,19 +3441,19 @@
|
|
|
3377
3441
|
}
|
|
3378
3442
|
// 面板在弹层里(v-show):先切到「开关 · 历史」页签,再点出「实例方法速查」那一块
|
|
3379
3443
|
;[...document.querySelectorAll('.demo-tabs button')].find(node => node.textContent.includes('开关'))?.click()
|
|
3380
|
-
await wait(80)
|
|
3381
|
-
;[...document.querySelectorAll('.demo-panel-buttons button')].find(node => node.textContent.includes('实例方法速查'))?.click()
|
|
3382
|
-
await wait(150)
|
|
3383
|
-
const apiItems = () => [...document.querySelectorAll('.demo-api-list:not(.demo-api-list--options) .demo-api-item')]
|
|
3384
|
-
const optionItems = () => [...document.querySelectorAll('.demo-api-list--options .demo-api-item')]
|
|
3385
|
-
check('实例方法速查:整份成员清单按分组列全(16 组 /
|
|
3386
|
-
Boolean(ready) && document.querySelectorAll('.demo-api-group').length === 16 && apiItems().length ===
|
|
3444
|
+
await wait(80)
|
|
3445
|
+
;[...document.querySelectorAll('.demo-panel-buttons button')].find(node => node.textContent.includes('实例方法速查'))?.click()
|
|
3446
|
+
await wait(150)
|
|
3447
|
+
const apiItems = () => [...document.querySelectorAll('.demo-api-list:not(.demo-api-list--options) .demo-api-item')]
|
|
3448
|
+
const optionItems = () => [...document.querySelectorAll('.demo-api-list--options .demo-api-item')]
|
|
3449
|
+
check('实例方法速查:整份成员清单按分组列全(16 组 / 285 个成员)',
|
|
3450
|
+
Boolean(ready) && document.querySelectorAll('.demo-api-group').length === 16 && apiItems().length === 285,
|
|
3387
3451
|
`${document.querySelectorAll('.demo-api-group').length} 组 / ${apiItems().length} 个成员`)
|
|
3388
|
-
const apiNames = apiItems().map(node => node.querySelector('b')?.textContent)
|
|
3389
|
-
const roundNames = ['registerSymbol', 'unregisterSymbol', 'listSymbols']
|
|
3390
|
-
check('实例方法速查:本轮新增的三个方法都在清单里(registerSymbol / unregisterSymbol / listSymbols)',
|
|
3391
|
-
roundNames.every(name => apiNames.includes(name)),
|
|
3392
|
-
roundNames.filter(name => !apiNames.includes(name)).join(' / ') || '三个都在')
|
|
3452
|
+
const apiNames = apiItems().map(node => node.querySelector('b')?.textContent)
|
|
3453
|
+
const roundNames = ['registerSymbol', 'unregisterSymbol', 'listSymbols']
|
|
3454
|
+
check('实例方法速查:本轮新增的三个方法都在清单里(registerSymbol / unregisterSymbol / listSymbols)',
|
|
3455
|
+
roundNames.every(name => apiNames.includes(name)),
|
|
3456
|
+
roundNames.filter(name => !apiNames.includes(name)).join(' / ') || '三个都在')
|
|
3393
3457
|
const roundNamesAuto = ['getZoomBaseScale', 'setZoomBaseScale']
|
|
3394
3458
|
check('实例方法速查:自适应比例的读 / 写两个方法在清单里(getZoomBaseScale / setZoomBaseScale)',
|
|
3395
3459
|
roundNamesAuto.every(name => apiNames.includes(name)),
|
|
@@ -3398,183 +3462,183 @@
|
|
|
3398
3462
|
check('实例方法速查:相机目标、线路样式快照与数据版本三个成员在清单里',
|
|
3399
3463
|
roundNamesCameraApi.every(name => apiNames.includes(name)),
|
|
3400
3464
|
roundNamesCameraApi.filter(name => !apiNames.includes(name)).join(' / ') || '三个都在')
|
|
3401
|
-
const roundNames1597 = ['addChannelGroupLines', 'joinChannelGroupLines', 'endAddChannel']
|
|
3402
|
-
check('实例方法速查:这一轮拆出来的方法也在清单里(addChannelGroupLines / joinChannelGroupLines / endAddChannel)',
|
|
3403
|
-
roundNames1597.every(name => apiNames.includes(name)),
|
|
3404
|
-
roundNames1597.filter(name => !apiNames.includes(name)).join(' / ') || '三个都在')
|
|
3405
|
-
check('实例方法速查:options 折叠清单列全
|
|
3406
|
-
const apiSearch = document.querySelector('.demo-api-search input[type="search"]')
|
|
3407
|
-
apiSearch.value = 'focusOn'
|
|
3408
|
-
apiSearch.dispatchEvent(new Event('input', { bubbles: true }))
|
|
3409
|
-
await wait(80)
|
|
3410
|
-
const hitNames = apiItems().map(node => node.querySelector('b')?.textContent)
|
|
3411
|
-
check('实例方法速查:搜索按名字 / 参数 / 返回 / 说明命中', hitNames.includes('focusOn') && hitNames.length < 8, hitNames.join(' / '))
|
|
3412
|
-
apiItems().find(node => node.querySelector('b')?.textContent === 'focusOn')?.click()
|
|
3413
|
-
await wait(80)
|
|
3414
|
-
const apiDetail = document.querySelector('.demo-api-detail')?.textContent ?? ''
|
|
3415
|
-
const evalInput = document.querySelector('.demo-api-eval input[type="text"]')
|
|
3416
|
-
check('实例方法速查:点一条摆出参数与说明,并把 view.名字( 填进求值框',
|
|
3417
|
-
apiDetail.includes('focusOn') && apiDetail.includes('参数') && evalInput?.value === 'view.focusOn(',
|
|
3418
|
-
apiDetail.replace(/\s+/g, ' ').slice(0, 140))
|
|
3419
|
-
;[...document.querySelectorAll('.demo-api-eval button')].find(node => node.textContent.includes('示例'))?.click()
|
|
3420
|
-
await wait(240)
|
|
3421
|
-
const evalText = document.querySelector('.demo-api-eval + .demo-controls .demo-groups')?.textContent ?? ''
|
|
3422
|
-
check('实例方法速查:求值当场把结果打在面板上(示例 = listPoints 读数)', evalText.includes('index'), evalText.replace(/\s+/g, ' ').slice(0, 140))
|
|
3423
|
-
// 2.10 空白处右键新增场站:画布菜单 → demo 自己写的表单 → addPoint
|
|
3424
|
-
const stationCanvas = document.querySelector('canvas.gis-view__canvas')
|
|
3425
|
-
const stationRect = stationCanvas.getBoundingClientRect()
|
|
3426
|
-
// 数据基本铺满画布:候选点按「越可能是空白」排前面(综合示例只有右下角一带是数据外沿)
|
|
3427
|
-
const blankSpots = [
|
|
3428
|
-
[stationRect.right - 8, stationRect.bottom - 8],
|
|
3429
|
-
[stationRect.right - 8, stationRect.bottom - 30],
|
|
3430
|
-
[stationRect.right - 30, stationRect.bottom - 8],
|
|
3431
|
-
[stationRect.right - 8, stationRect.top + 8],
|
|
3432
|
-
[stationRect.left + 8, stationRect.top + 8],
|
|
3433
|
-
[stationRect.left + stationRect.width / 2, stationRect.bottom - 8]
|
|
3434
|
-
]
|
|
3435
|
-
let stationMenu = null
|
|
3436
|
-
for (const [x, y] of blankSpots) {
|
|
3437
|
-
contextMenuOn(stationCanvas, x, y)
|
|
3438
|
-
const opened = await until(() => document.querySelector('.demo-canvas-menu'), 1200)
|
|
3439
|
-
if (opened && opened.textContent.includes('空白处')) {
|
|
3440
|
-
stationMenu = opened
|
|
3441
|
-
break
|
|
3442
|
-
}
|
|
3443
|
-
opened?.querySelector('.demo-channel-menu-head button')?.click()
|
|
3444
|
-
await wait(60)
|
|
3445
|
-
}
|
|
3446
|
-
check('画布菜单:空白处右键弹出自绘菜单,并多了一行「新增场站」', Boolean(stationMenu) && stationMenu.textContent.includes('新增场站'), stationMenu ? stationMenu.textContent.replace(/\s+/g, ' ').slice(0, 140) : '候选点都命中了要素')
|
|
3447
|
-
const stationRow = stationMenu?.querySelector('[data-demo="canvas-menu-addStation"]')
|
|
3448
|
-
check('画布菜单:新增场站那一行带 data-demo 钩子、标签点名 addPoint', Boolean(stationRow) && stationRow.textContent.includes('addPoint'), stationRow?.textContent ?? '没有这一行')
|
|
3449
|
-
const pointsBeforeStation = Number((document.querySelector('.demo-overlay')?.textContent ?? '').match(/(\d+)\s*点/)?.[1] ?? 0)
|
|
3450
|
-
stationRow?.click()
|
|
3451
|
-
await wait(160)
|
|
3452
|
-
const stationConfirm = document.querySelector('[data-demo="add-station-confirm"]')
|
|
3453
|
-
const stationIdInput = document.querySelector('[data-demo="add-station-id"]')
|
|
3454
|
-
const stationNameInput = document.querySelector('[data-demo="add-station-name"]')
|
|
3455
|
-
const stationXInput = document.querySelector('[data-demo="add-station-x"]')
|
|
3456
|
-
check('新增场站:点这一行弹出 demo 自己写的表单(编号 / 名称 / x / y / 电压等级都在,落点已预填)',
|
|
3457
|
-
Boolean(stationConfirm) && ['add-station-id', 'add-station-name', 'add-station-x', 'add-station-y', 'add-station-voltage'].every(key => document.querySelector(`[data-demo="${key}"]`)) && Number.isFinite(Number(stationXInput?.value)),
|
|
3458
|
-
stationConfirm ? `预填 x=${stationXInput?.value} / y=${document.querySelector('[data-demo="add-station-y"]')?.value} / 编号=${stationIdInput?.value}` : '没弹出表单')
|
|
3459
|
-
// 校验:名称清空不许提交(弹框不关、数据不动)
|
|
3460
|
-
if (stationNameInput) {
|
|
3461
|
-
stationNameInput.value = ''
|
|
3462
|
-
stationNameInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
3463
|
-
}
|
|
3464
|
-
stationConfirm?.click()
|
|
3465
|
-
await wait(140)
|
|
3466
|
-
const emptyNameError = document.querySelector('.demo-station-error')?.textContent ?? ''
|
|
3467
|
-
check('新增场站:名称是空的时候拦在表单里(弹框不关、数据不动)', emptyNameError.includes('名称不能为空') && Boolean(document.querySelector('[data-demo="add-station-confirm"]')), emptyNameError || '没有报错')
|
|
3468
|
-
const newStationId = stationIdInput?.value ?? ''
|
|
3469
|
-
if (stationNameInput) {
|
|
3470
|
-
stationNameInput.value = '自测新场站'
|
|
3471
|
-
stationNameInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
3472
|
-
}
|
|
3473
|
-
document.querySelector('[data-demo="add-station-confirm"]')?.click()
|
|
3474
|
-
await wait(520)
|
|
3475
|
-
const pointsAfterStation = Number((document.querySelector('.demo-overlay')?.textContent ?? '').match(/(\d+)\s*点/)?.[1] ?? 0)
|
|
3476
|
-
const stationEvent = document.querySelector('.gis-demo-event')?.textContent ?? ''
|
|
3477
|
-
check('新增场站:确认走 addPoint —— 表单收起来、要素数 +1、日志点名 addPoint / 落点下标 / 已选中',
|
|
3478
|
-
!document.querySelector('[data-demo="add-station-confirm"]') && pointsAfterStation === pointsBeforeStation + 1 && stationEvent.includes('addPoint') && stationEvent.includes('已选中') && stationEvent.includes(newStationId),
|
|
3479
|
-
`${pointsBeforeStation} → ${pointsAfterStation} · ${stationEvent.slice(0, 150)}`)
|
|
3480
|
-
// 2.11 beginAddChannel 的结束事件(ready):读数摆到面板上,endAddChannel 收掉不新建
|
|
3481
|
-
const draftState = document.querySelector('.demo-view')?.__vueParentComponent?.setupState
|
|
3482
|
-
const draftView = draftState?.viewRef ?? null
|
|
3483
|
-
if (draftView) {
|
|
3484
|
-
// 打开「数据 · 通道 → 通道与拐点」面板,点「新建通道(点两个节点)」
|
|
3485
|
-
;[...document.querySelectorAll('.demo-tabs button')].find(node => node.textContent.includes('数据'))?.click()
|
|
3486
|
-
await wait(80)
|
|
3487
|
-
const openChannelPanel = async () => {
|
|
3488
|
-
;[...document.querySelectorAll('.demo-panel-buttons button')].find(node => node.textContent.trim() === '通道与拐点')?.click()
|
|
3489
|
-
await wait(140)
|
|
3465
|
+
const roundNames1597 = ['addChannelGroupLines', 'joinChannelGroupLines', 'endAddChannel']
|
|
3466
|
+
check('实例方法速查:这一轮拆出来的方法也在清单里(addChannelGroupLines / joinChannelGroupLines / endAddChannel)',
|
|
3467
|
+
roundNames1597.every(name => apiNames.includes(name)),
|
|
3468
|
+
roundNames1597.filter(name => !apiNames.includes(name)).join(' / ') || '三个都在')
|
|
3469
|
+
check('实例方法速查:options 折叠清单列全 100 个键', optionItems().length === 100, `${optionItems().length} 个键`)
|
|
3470
|
+
const apiSearch = document.querySelector('.demo-api-search input[type="search"]')
|
|
3471
|
+
apiSearch.value = 'focusOn'
|
|
3472
|
+
apiSearch.dispatchEvent(new Event('input', { bubbles: true }))
|
|
3473
|
+
await wait(80)
|
|
3474
|
+
const hitNames = apiItems().map(node => node.querySelector('b')?.textContent)
|
|
3475
|
+
check('实例方法速查:搜索按名字 / 参数 / 返回 / 说明命中', hitNames.includes('focusOn') && hitNames.length < 8, hitNames.join(' / '))
|
|
3476
|
+
apiItems().find(node => node.querySelector('b')?.textContent === 'focusOn')?.click()
|
|
3477
|
+
await wait(80)
|
|
3478
|
+
const apiDetail = document.querySelector('.demo-api-detail')?.textContent ?? ''
|
|
3479
|
+
const evalInput = document.querySelector('.demo-api-eval input[type="text"]')
|
|
3480
|
+
check('实例方法速查:点一条摆出参数与说明,并把 view.名字( 填进求值框',
|
|
3481
|
+
apiDetail.includes('focusOn') && apiDetail.includes('参数') && evalInput?.value === 'view.focusOn(',
|
|
3482
|
+
apiDetail.replace(/\s+/g, ' ').slice(0, 140))
|
|
3483
|
+
;[...document.querySelectorAll('.demo-api-eval button')].find(node => node.textContent.includes('示例'))?.click()
|
|
3484
|
+
await wait(240)
|
|
3485
|
+
const evalText = document.querySelector('.demo-api-eval + .demo-controls .demo-groups')?.textContent ?? ''
|
|
3486
|
+
check('实例方法速查:求值当场把结果打在面板上(示例 = listPoints 读数)', evalText.includes('index'), evalText.replace(/\s+/g, ' ').slice(0, 140))
|
|
3487
|
+
// 2.10 空白处右键新增场站:画布菜单 → demo 自己写的表单 → addPoint
|
|
3488
|
+
const stationCanvas = document.querySelector('canvas.gis-view__canvas')
|
|
3489
|
+
const stationRect = stationCanvas.getBoundingClientRect()
|
|
3490
|
+
// 数据基本铺满画布:候选点按「越可能是空白」排前面(综合示例只有右下角一带是数据外沿)
|
|
3491
|
+
const blankSpots = [
|
|
3492
|
+
[stationRect.right - 8, stationRect.bottom - 8],
|
|
3493
|
+
[stationRect.right - 8, stationRect.bottom - 30],
|
|
3494
|
+
[stationRect.right - 30, stationRect.bottom - 8],
|
|
3495
|
+
[stationRect.right - 8, stationRect.top + 8],
|
|
3496
|
+
[stationRect.left + 8, stationRect.top + 8],
|
|
3497
|
+
[stationRect.left + stationRect.width / 2, stationRect.bottom - 8]
|
|
3498
|
+
]
|
|
3499
|
+
let stationMenu = null
|
|
3500
|
+
for (const [x, y] of blankSpots) {
|
|
3501
|
+
contextMenuOn(stationCanvas, x, y)
|
|
3502
|
+
const opened = await until(() => document.querySelector('.demo-canvas-menu'), 1200)
|
|
3503
|
+
if (opened && opened.textContent.includes('空白处')) {
|
|
3504
|
+
stationMenu = opened
|
|
3505
|
+
break
|
|
3506
|
+
}
|
|
3507
|
+
opened?.querySelector('.demo-channel-menu-head button')?.click()
|
|
3508
|
+
await wait(60)
|
|
3509
|
+
}
|
|
3510
|
+
check('画布菜单:空白处右键弹出自绘菜单,并多了一行「新增场站」', Boolean(stationMenu) && stationMenu.textContent.includes('新增场站'), stationMenu ? stationMenu.textContent.replace(/\s+/g, ' ').slice(0, 140) : '候选点都命中了要素')
|
|
3511
|
+
const stationRow = stationMenu?.querySelector('[data-demo="canvas-menu-addStation"]')
|
|
3512
|
+
check('画布菜单:新增场站那一行带 data-demo 钩子、标签点名 addPoint', Boolean(stationRow) && stationRow.textContent.includes('addPoint'), stationRow?.textContent ?? '没有这一行')
|
|
3513
|
+
const pointsBeforeStation = Number((document.querySelector('.demo-overlay')?.textContent ?? '').match(/(\d+)\s*点/)?.[1] ?? 0)
|
|
3514
|
+
stationRow?.click()
|
|
3515
|
+
await wait(160)
|
|
3516
|
+
const stationConfirm = document.querySelector('[data-demo="add-station-confirm"]')
|
|
3517
|
+
const stationIdInput = document.querySelector('[data-demo="add-station-id"]')
|
|
3518
|
+
const stationNameInput = document.querySelector('[data-demo="add-station-name"]')
|
|
3519
|
+
const stationXInput = document.querySelector('[data-demo="add-station-x"]')
|
|
3520
|
+
check('新增场站:点这一行弹出 demo 自己写的表单(编号 / 名称 / x / y / 电压等级都在,落点已预填)',
|
|
3521
|
+
Boolean(stationConfirm) && ['add-station-id', 'add-station-name', 'add-station-x', 'add-station-y', 'add-station-voltage'].every(key => document.querySelector(`[data-demo="${key}"]`)) && Number.isFinite(Number(stationXInput?.value)),
|
|
3522
|
+
stationConfirm ? `预填 x=${stationXInput?.value} / y=${document.querySelector('[data-demo="add-station-y"]')?.value} / 编号=${stationIdInput?.value}` : '没弹出表单')
|
|
3523
|
+
// 校验:名称清空不许提交(弹框不关、数据不动)
|
|
3524
|
+
if (stationNameInput) {
|
|
3525
|
+
stationNameInput.value = ''
|
|
3526
|
+
stationNameInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
3527
|
+
}
|
|
3528
|
+
stationConfirm?.click()
|
|
3529
|
+
await wait(140)
|
|
3530
|
+
const emptyNameError = document.querySelector('.demo-station-error')?.textContent ?? ''
|
|
3531
|
+
check('新增场站:名称是空的时候拦在表单里(弹框不关、数据不动)', emptyNameError.includes('名称不能为空') && Boolean(document.querySelector('[data-demo="add-station-confirm"]')), emptyNameError || '没有报错')
|
|
3532
|
+
const newStationId = stationIdInput?.value ?? ''
|
|
3533
|
+
if (stationNameInput) {
|
|
3534
|
+
stationNameInput.value = '自测新场站'
|
|
3535
|
+
stationNameInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
3536
|
+
}
|
|
3537
|
+
document.querySelector('[data-demo="add-station-confirm"]')?.click()
|
|
3538
|
+
await wait(520)
|
|
3539
|
+
const pointsAfterStation = Number((document.querySelector('.demo-overlay')?.textContent ?? '').match(/(\d+)\s*点/)?.[1] ?? 0)
|
|
3540
|
+
const stationEvent = document.querySelector('.gis-demo-event')?.textContent ?? ''
|
|
3541
|
+
check('新增场站:确认走 addPoint —— 表单收起来、要素数 +1、日志点名 addPoint / 落点下标 / 已选中',
|
|
3542
|
+
!document.querySelector('[data-demo="add-station-confirm"]') && pointsAfterStation === pointsBeforeStation + 1 && stationEvent.includes('addPoint') && stationEvent.includes('已选中') && stationEvent.includes(newStationId),
|
|
3543
|
+
`${pointsBeforeStation} → ${pointsAfterStation} · ${stationEvent.slice(0, 150)}`)
|
|
3544
|
+
// 2.11 beginAddChannel 的结束事件(ready):读数摆到面板上,endAddChannel 收掉不新建
|
|
3545
|
+
const draftState = document.querySelector('.demo-view')?.__vueParentComponent?.setupState
|
|
3546
|
+
const draftView = draftState?.viewRef ?? null
|
|
3547
|
+
if (draftView) {
|
|
3548
|
+
// 打开「数据 · 通道 → 通道与拐点」面板,点「新建通道(点两个节点)」
|
|
3549
|
+
;[...document.querySelectorAll('.demo-tabs button')].find(node => node.textContent.includes('数据'))?.click()
|
|
3550
|
+
await wait(80)
|
|
3551
|
+
const openChannelPanel = async () => {
|
|
3552
|
+
;[...document.querySelectorAll('.demo-panel-buttons button')].find(node => node.textContent.trim() === '通道与拐点')?.click()
|
|
3553
|
+
await wait(140)
|
|
3490
3554
|
return [...document.querySelectorAll('.demo-panel-inline button')].find(node => node.textContent.trim() === '新建通道(点两个节点)') ?? null
|
|
3491
|
-
}
|
|
3492
|
-
let beginDraftButton = await openChannelPanel()
|
|
3493
|
-
if (!beginDraftButton) {
|
|
3494
|
-
beginDraftButton = await openChannelPanel()
|
|
3495
|
-
}
|
|
3496
|
-
check('新增通道:面板上点得到「新建通道(点两个节点)」', Boolean(beginDraftButton), beginDraftButton ? beginDraftButton.textContent.trim() : '没找到按钮')
|
|
3497
|
-
// 结束事件(ready)与 cancel 的差别就在「不补 cancel」:用事件订阅记一份相位表(不动 demo 自己的 onChannelDraft)
|
|
3498
|
-
const draftPhases = []
|
|
3499
|
-
const offDraftEvent = draftView.on('channelDraft', payload => draftPhases.push(payload.phase))
|
|
3500
|
-
beginDraftButton?.click()
|
|
3501
|
-
await wait(150)
|
|
3502
|
-
// 找一对投影在画布里的节点:先适应视图,再挑屏幕距离最远的一对
|
|
3503
|
-
draftView.fitView()
|
|
3504
|
-
await wait(220)
|
|
3505
|
-
const draftCanvas = document.querySelector('.demo-view canvas.gis-view__canvas')
|
|
3506
|
-
const draftBox = draftCanvas.getBoundingClientRect()
|
|
3507
|
-
const draftOnScreen = draftView.getData().points.filter(point => {
|
|
3508
|
-
const screen = draftView.project(point.x, point.y)
|
|
3509
|
-
return screen[0] > 6 && screen[1] > 6 && screen[0] < draftBox.width - 6 && screen[1] < draftBox.height - 6
|
|
3510
|
-
})
|
|
3511
|
-
// 数据多的时候不做全量两两组合:按等距抽样取最多 60 个候选
|
|
3512
|
-
const draftStride = Math.max(Math.ceil(draftOnScreen.length / 60), 1)
|
|
3513
|
-
const draftPicks = draftOnScreen.filter((_point, index) => index % draftStride === 0)
|
|
3514
|
-
let pickFrom = null
|
|
3515
|
-
let pickTo = null
|
|
3516
|
-
let pickSpan = -1
|
|
3517
|
-
let fallbackFrom = null
|
|
3518
|
-
let fallbackTo = null
|
|
3519
|
-
let fallbackSpan = -1
|
|
3520
|
-
for (const from of draftPicks) {
|
|
3521
|
-
for (const to of draftPicks) {
|
|
3522
|
-
if (String(from.id) === String(to.id)) {
|
|
3523
|
-
continue
|
|
3524
|
-
}
|
|
3525
|
-
const a = draftView.project(from.x, from.y)
|
|
3526
|
-
const b = draftView.project(to.x, to.y)
|
|
3527
|
-
const span = Math.hypot(a[0] - b[0], a[1] - b[1])
|
|
3528
|
-
if (span > fallbackSpan) {
|
|
3529
|
-
fallbackSpan = span
|
|
3530
|
-
fallbackFrom = from
|
|
3531
|
-
fallbackTo = to
|
|
3532
|
-
}
|
|
3533
|
-
// 优先挑一对「还没有通道」的(getChannelBetween 很贵:只在可能刷新记录时才查)
|
|
3534
|
-
if (span > pickSpan && !draftView.getChannelBetween(from.id, to.id)?.hasChannel) {
|
|
3535
|
-
pickSpan = span
|
|
3536
|
-
pickFrom = from
|
|
3537
|
-
pickTo = to
|
|
3538
|
-
}
|
|
3539
|
-
}
|
|
3540
|
-
}
|
|
3541
|
-
if (!pickFrom && fallbackFrom) {
|
|
3542
|
-
pickFrom = fallbackFrom
|
|
3543
|
-
pickTo = fallbackTo
|
|
3544
|
-
}
|
|
3545
|
-
const draftClick = screen => {
|
|
3546
|
-
const base = { bubbles: true, cancelable: true, composed: true, pointerId: 88, pointerType: 'mouse', isPrimary: true, button: 0, clientX: draftBox.left + screen[0], clientY: draftBox.top + screen[1] }
|
|
3547
|
-
draftCanvas.dispatchEvent(new PointerEvent('pointermove', { ...base, buttons: 0 }))
|
|
3548
|
-
draftCanvas.dispatchEvent(new PointerEvent('pointerdown', { ...base, buttons: 1 }))
|
|
3549
|
-
draftCanvas.dispatchEvent(new PointerEvent('pointerup', { ...base, buttons: 0 }))
|
|
3550
|
-
}
|
|
3551
|
-
if (pickFrom && pickTo) {
|
|
3552
|
-
draftClick(draftView.project(pickFrom.x, pickFrom.y))
|
|
3553
|
-
await wait(90)
|
|
3554
|
-
draftClick(draftView.project(pickTo.x, pickTo.y))
|
|
3555
|
-
await wait(180)
|
|
3556
|
-
}
|
|
3557
|
-
const readyPayload = draftState?.channelReadyPayload ?? null
|
|
3558
|
-
check('新增通道:结束事件(ready)载荷进了 demo(两个节点 / 没有通道的读数 / 草稿还在)',
|
|
3559
|
-
readyPayload?.phase === 'ready' && String(readyPayload.from?.id) === String(pickFrom?.id) && String(readyPayload.to?.id) === String(pickTo?.id) && readyPayload.hasChannel === false && readyPayload.hasGroup === false && readyPayload.groups.length === 0 && readyPayload.channel === null && readyPayload.existingChannelId === null && draftView.getAddChannelDraft()?.phase === 'ready',
|
|
3560
|
-
readyPayload ? `from=${readyPayload.from?.id} to=${readyPayload.to?.id} hasChannel=${readyPayload.hasChannel}` : '没有收到 ready 载荷')
|
|
3561
|
-
const readyText = document.querySelector('[data-demo="channel-ready"]')?.textContent ?? ''
|
|
3562
|
-
check('新增通道:面板把 ready 读数摆出来(hasChannel / 分组明细 / 当前分组 / 通道 / existingChannelId)',
|
|
3563
|
-
readyText.includes('hasChannel') && readyText.includes('分组明细') && readyText.includes('当前分组') && readyText.includes('通道') && readyText.includes('existingChannelId'), readyText.replace(/\s+/g, ' ').slice(0, 140))
|
|
3564
|
-
const draftBeforeEnd = draftView.getData()
|
|
3565
|
-
const endDraftButton = document.querySelector('[data-demo="channel-ready-end"]')
|
|
3566
|
-
check('新增通道:结束事件读数旁边给出 endAddChannel 按钮(ready 时可点)', Boolean(endDraftButton) && endDraftButton.disabled === false, endDraftButton ? endDraftButton.textContent.trim() + (endDraftButton.disabled ? '(灰)' : '') : '没有按钮')
|
|
3567
|
-
endDraftButton?.click()
|
|
3568
|
-
await wait(200)
|
|
3569
|
-
check('新增通道:endAddChannel 静默收掉假通道 —— 草稿没了、数据一动不动、不补 cancel',
|
|
3570
|
-
draftPhases.includes('ready') && !draftPhases.includes('cancel') && draftView.getAddChannelDraft() === null && draftView.getData().points.length === draftBeforeEnd.points.length && draftView.getData().lines.length === draftBeforeEnd.lines.length,
|
|
3571
|
-
`相位 ${draftPhases.join(' / ')} · 草稿 ${draftView.getAddChannelDraft() === null ? '已收' : '还在'} / 节点 ${draftBeforeEnd.points.length} → ${draftView.getData().points.length} / 线路 ${draftBeforeEnd.lines.length} → ${draftView.getData().lines.length}`)
|
|
3572
|
-
offDraftEvent()
|
|
3573
|
-
}
|
|
3574
|
-
else {
|
|
3575
|
-
check('新增通道:综合示例的实例拿得到(setupState.viewRef)', false, '没有 setupState.viewRef')
|
|
3576
|
-
}
|
|
3577
|
-
}
|
|
3555
|
+
}
|
|
3556
|
+
let beginDraftButton = await openChannelPanel()
|
|
3557
|
+
if (!beginDraftButton) {
|
|
3558
|
+
beginDraftButton = await openChannelPanel()
|
|
3559
|
+
}
|
|
3560
|
+
check('新增通道:面板上点得到「新建通道(点两个节点)」', Boolean(beginDraftButton), beginDraftButton ? beginDraftButton.textContent.trim() : '没找到按钮')
|
|
3561
|
+
// 结束事件(ready)与 cancel 的差别就在「不补 cancel」:用事件订阅记一份相位表(不动 demo 自己的 onChannelDraft)
|
|
3562
|
+
const draftPhases = []
|
|
3563
|
+
const offDraftEvent = draftView.on('channelDraft', payload => draftPhases.push(payload.phase))
|
|
3564
|
+
beginDraftButton?.click()
|
|
3565
|
+
await wait(150)
|
|
3566
|
+
// 找一对投影在画布里的节点:先适应视图,再挑屏幕距离最远的一对
|
|
3567
|
+
draftView.fitView()
|
|
3568
|
+
await wait(220)
|
|
3569
|
+
const draftCanvas = document.querySelector('.demo-view canvas.gis-view__canvas')
|
|
3570
|
+
const draftBox = draftCanvas.getBoundingClientRect()
|
|
3571
|
+
const draftOnScreen = draftView.getData().points.filter(point => {
|
|
3572
|
+
const screen = draftView.project(point.x, point.y)
|
|
3573
|
+
return screen[0] > 6 && screen[1] > 6 && screen[0] < draftBox.width - 6 && screen[1] < draftBox.height - 6
|
|
3574
|
+
})
|
|
3575
|
+
// 数据多的时候不做全量两两组合:按等距抽样取最多 60 个候选
|
|
3576
|
+
const draftStride = Math.max(Math.ceil(draftOnScreen.length / 60), 1)
|
|
3577
|
+
const draftPicks = draftOnScreen.filter((_point, index) => index % draftStride === 0)
|
|
3578
|
+
let pickFrom = null
|
|
3579
|
+
let pickTo = null
|
|
3580
|
+
let pickSpan = -1
|
|
3581
|
+
let fallbackFrom = null
|
|
3582
|
+
let fallbackTo = null
|
|
3583
|
+
let fallbackSpan = -1
|
|
3584
|
+
for (const from of draftPicks) {
|
|
3585
|
+
for (const to of draftPicks) {
|
|
3586
|
+
if (String(from.id) === String(to.id)) {
|
|
3587
|
+
continue
|
|
3588
|
+
}
|
|
3589
|
+
const a = draftView.project(from.x, from.y)
|
|
3590
|
+
const b = draftView.project(to.x, to.y)
|
|
3591
|
+
const span = Math.hypot(a[0] - b[0], a[1] - b[1])
|
|
3592
|
+
if (span > fallbackSpan) {
|
|
3593
|
+
fallbackSpan = span
|
|
3594
|
+
fallbackFrom = from
|
|
3595
|
+
fallbackTo = to
|
|
3596
|
+
}
|
|
3597
|
+
// 优先挑一对「还没有通道」的(getChannelBetween 很贵:只在可能刷新记录时才查)
|
|
3598
|
+
if (span > pickSpan && !draftView.getChannelBetween(from.id, to.id)?.hasChannel) {
|
|
3599
|
+
pickSpan = span
|
|
3600
|
+
pickFrom = from
|
|
3601
|
+
pickTo = to
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
if (!pickFrom && fallbackFrom) {
|
|
3606
|
+
pickFrom = fallbackFrom
|
|
3607
|
+
pickTo = fallbackTo
|
|
3608
|
+
}
|
|
3609
|
+
const draftClick = screen => {
|
|
3610
|
+
const base = { bubbles: true, cancelable: true, composed: true, pointerId: 88, pointerType: 'mouse', isPrimary: true, button: 0, clientX: draftBox.left + screen[0], clientY: draftBox.top + screen[1] }
|
|
3611
|
+
draftCanvas.dispatchEvent(new PointerEvent('pointermove', { ...base, buttons: 0 }))
|
|
3612
|
+
draftCanvas.dispatchEvent(new PointerEvent('pointerdown', { ...base, buttons: 1 }))
|
|
3613
|
+
draftCanvas.dispatchEvent(new PointerEvent('pointerup', { ...base, buttons: 0 }))
|
|
3614
|
+
}
|
|
3615
|
+
if (pickFrom && pickTo) {
|
|
3616
|
+
draftClick(draftView.project(pickFrom.x, pickFrom.y))
|
|
3617
|
+
await wait(90)
|
|
3618
|
+
draftClick(draftView.project(pickTo.x, pickTo.y))
|
|
3619
|
+
await wait(180)
|
|
3620
|
+
}
|
|
3621
|
+
const readyPayload = draftState?.channelReadyPayload ?? null
|
|
3622
|
+
check('新增通道:结束事件(ready)载荷进了 demo(两个节点 / 没有通道的读数 / 草稿还在)',
|
|
3623
|
+
readyPayload?.phase === 'ready' && String(readyPayload.from?.id) === String(pickFrom?.id) && String(readyPayload.to?.id) === String(pickTo?.id) && readyPayload.hasChannel === false && readyPayload.hasGroup === false && readyPayload.groups.length === 0 && readyPayload.channel === null && readyPayload.existingChannelId === null && draftView.getAddChannelDraft()?.phase === 'ready',
|
|
3624
|
+
readyPayload ? `from=${readyPayload.from?.id} to=${readyPayload.to?.id} hasChannel=${readyPayload.hasChannel}` : '没有收到 ready 载荷')
|
|
3625
|
+
const readyText = document.querySelector('[data-demo="channel-ready"]')?.textContent ?? ''
|
|
3626
|
+
check('新增通道:面板把 ready 读数摆出来(hasChannel / 分组明细 / 当前分组 / 通道 / existingChannelId)',
|
|
3627
|
+
readyText.includes('hasChannel') && readyText.includes('分组明细') && readyText.includes('当前分组') && readyText.includes('通道') && readyText.includes('existingChannelId'), readyText.replace(/\s+/g, ' ').slice(0, 140))
|
|
3628
|
+
const draftBeforeEnd = draftView.getData()
|
|
3629
|
+
const endDraftButton = document.querySelector('[data-demo="channel-ready-end"]')
|
|
3630
|
+
check('新增通道:结束事件读数旁边给出 endAddChannel 按钮(ready 时可点)', Boolean(endDraftButton) && endDraftButton.disabled === false, endDraftButton ? endDraftButton.textContent.trim() + (endDraftButton.disabled ? '(灰)' : '') : '没有按钮')
|
|
3631
|
+
endDraftButton?.click()
|
|
3632
|
+
await wait(200)
|
|
3633
|
+
check('新增通道:endAddChannel 静默收掉假通道 —— 草稿没了、数据一动不动、不补 cancel',
|
|
3634
|
+
draftPhases.includes('ready') && !draftPhases.includes('cancel') && draftView.getAddChannelDraft() === null && draftView.getData().points.length === draftBeforeEnd.points.length && draftView.getData().lines.length === draftBeforeEnd.lines.length,
|
|
3635
|
+
`相位 ${draftPhases.join(' / ')} · 草稿 ${draftView.getAddChannelDraft() === null ? '已收' : '还在'} / 节点 ${draftBeforeEnd.points.length} → ${draftView.getData().points.length} / 线路 ${draftBeforeEnd.lines.length} → ${draftView.getData().lines.length}`)
|
|
3636
|
+
offDraftEvent()
|
|
3637
|
+
}
|
|
3638
|
+
else {
|
|
3639
|
+
check('新增通道:综合示例的实例拿得到(setupState.viewRef)', false, '没有 setupState.viewRef')
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3578
3642
|
else {
|
|
3579
3643
|
check('实例方法速查:综合示例的页签在', false, '没找到「综合」页签')
|
|
3580
3644
|
}
|
|
@@ -3721,64 +3785,64 @@
|
|
|
3721
3785
|
check('接口能力 demo:页签在', false, '没找到「接口能力」页签')
|
|
3722
3786
|
}
|
|
3723
3787
|
// 2.10 假后端契约:头 / body 必须一致,同 opId 换 body 必须报幂等冲突
|
|
3724
|
-
{
|
|
3725
|
-
const { createFakeBackend } = await import('../demos/shared/fakeBackend.js')
|
|
3726
|
-
const backend = createFakeBackend({ initialVersion: 12, latency: 0, prefix: 'contract' })
|
|
3727
|
-
const request = backend.makeRequest({
|
|
3728
|
-
url: '/gis/copies/batch',
|
|
3729
|
-
body: {
|
|
3730
|
-
op: 'copy.batch',
|
|
3731
|
-
points: [{ id: 'contract-p1', name: '契约点 1' }]
|
|
3732
|
-
}
|
|
3733
|
-
})
|
|
3734
|
-
const success = await backend.submit(request, { latencyMs: 0 })
|
|
3735
|
-
check('假后端契约:合法请求头 / body 一致时 200 并推进版本',
|
|
3736
|
-
success.status === 200 && success.data?.idempotent === false && success.data?.version === 13 && success.data?.result?.counts?.points === 1,
|
|
3737
|
-
`HTTP ${success.status} · v${backend.snapshot().version} · ${success.code}`)
|
|
3738
|
-
const versionAfterSuccess = backend.snapshot().version
|
|
3739
|
-
const replay = await backend.submit(request, { latencyMs: 0 })
|
|
3740
|
-
const replaySnapshot = backend.snapshot()
|
|
3741
|
-
check('假后端契约:同 opId 同 body 原样重放 → 200 / idempotent / 版本与资源不重复',
|
|
3742
|
-
replay.status === 200 && replay.data?.idempotent === true && replay.data?.version === versionAfterSuccess && replaySnapshot.version === versionAfterSuccess && replay.data?.result?.counts?.points === 1,
|
|
3743
|
-
`HTTP ${replay.status} · idempotent=${replay.data?.idempotent} · v${replaySnapshot.version}`)
|
|
3744
|
-
const differentBody = {
|
|
3745
|
-
...request,
|
|
3746
|
-
body: {
|
|
3747
|
-
...request.body,
|
|
3748
|
-
points: [{ id: 'contract-p2', name: '契约点 2' }]
|
|
3749
|
-
}
|
|
3750
|
-
}
|
|
3751
|
-
const conflict = await backend.submit(differentBody, { latencyMs: 0 })
|
|
3752
|
-
check('假后端契约:同 opId 换 body → 409 IDEMPOTENCY_CONFLICT,版本不前进',
|
|
3753
|
-
conflict.status === 409 && conflict.code === 'IDEMPOTENCY_CONFLICT' && conflict.body?.data?.conflict?.type === 'idempotency-key' && backend.snapshot().version === versionAfterSuccess,
|
|
3754
|
-
`HTTP ${conflict.status} · ${conflict.code} · v${backend.snapshot().version}`)
|
|
3755
|
-
const mismatchHeader = backend.makeRequest({ url: '/gis/copies/batch', body: { op: 'copy.batch', points: [] } })
|
|
3756
|
-
mismatchHeader.headers['Idempotency-Key'] = 'contract-wrong'
|
|
3757
|
-
const headerMismatch = await backend.submit(mismatchHeader, { latencyMs: 0 })
|
|
3758
|
-
check('假后端契约:Idempotency-Key 与 body.opId 不一致 → 400 BAD_REQUEST',
|
|
3759
|
-
headerMismatch.status === 400 && headerMismatch.code === 'BAD_REQUEST' && /Idempotency-Key/.test(headerMismatch.reason ?? ''),
|
|
3760
|
-
`HTTP ${headerMismatch.status} · ${headerMismatch.code}`)
|
|
3761
|
-
const mismatchIfMatch = backend.makeRequest({ url: '/gis/copies/batch', body: { op: 'copy.batch', points: [] } })
|
|
3762
|
-
mismatchIfMatch.headers['If-Match'] = '"v999"'
|
|
3763
|
-
const ifMatchMismatch = await backend.submit(mismatchIfMatch, { latencyMs: 0 })
|
|
3764
|
-
check('假后端契约:If-Match 与 body.baseVersion 不一致 → 400 BAD_REQUEST',
|
|
3765
|
-
ifMatchMismatch.status === 400 && ifMatchMismatch.code === 'BAD_REQUEST' && /If-Match/.test(ifMatchMismatch.reason ?? ''),
|
|
3766
|
-
`HTTP ${ifMatchMismatch.status} · ${ifMatchMismatch.code}`)
|
|
3767
|
-
const versionBeforeInvalid = backend.snapshot().version
|
|
3768
|
-
const invalidVersion = backend.makeRequest({ url: '/gis/copies/batch', body: { op: 'copy.batch', points: [] } })
|
|
3769
|
-
invalidVersion.body.baseVersion = 'not-a-number'
|
|
3770
|
-
const badVersion = await backend.submit(invalidVersion, { latencyMs: 0 })
|
|
3771
|
-
check('假后端契约:baseVersion 不是有限数字 → 400,版本不动',
|
|
3772
|
-
badVersion.status === 400 && badVersion.code === 'BAD_REQUEST' && backend.snapshot().version === versionBeforeInvalid,
|
|
3773
|
-
`HTTP ${badVersion.status} · ${badVersion.code} · v${backend.snapshot().version}`)
|
|
3774
|
-
}
|
|
3775
|
-
}
|
|
3776
|
-
catch (error) {
|
|
3777
|
-
check('自测过程没有抛错', false, error instanceof Error ? `${error.message}\n${error.stack ?? ''}` : String(error))
|
|
3778
|
-
}
|
|
3779
|
-
const failures = results.filter(line => line.startsWith('FAIL'))
|
|
3780
|
-
document.getElementById('out').textContent = `${document.title} ${failures.length ? 'FAIL' : 'PASS'}\n${results.join('\n')}`
|
|
3781
|
-
document.title = `${document.title} ${failures.length ? 'FAIL' : 'PASS'}`
|
|
3782
|
-
</script>
|
|
3783
|
-
</body>
|
|
3784
|
-
</html>
|
|
3788
|
+
{
|
|
3789
|
+
const { createFakeBackend } = await import('../demos/shared/fakeBackend.js')
|
|
3790
|
+
const backend = createFakeBackend({ initialVersion: 12, latency: 0, prefix: 'contract' })
|
|
3791
|
+
const request = backend.makeRequest({
|
|
3792
|
+
url: '/gis/copies/batch',
|
|
3793
|
+
body: {
|
|
3794
|
+
op: 'copy.batch',
|
|
3795
|
+
points: [{ id: 'contract-p1', name: '契约点 1' }]
|
|
3796
|
+
}
|
|
3797
|
+
})
|
|
3798
|
+
const success = await backend.submit(request, { latencyMs: 0 })
|
|
3799
|
+
check('假后端契约:合法请求头 / body 一致时 200 并推进版本',
|
|
3800
|
+
success.status === 200 && success.data?.idempotent === false && success.data?.version === 13 && success.data?.result?.counts?.points === 1,
|
|
3801
|
+
`HTTP ${success.status} · v${backend.snapshot().version} · ${success.code}`)
|
|
3802
|
+
const versionAfterSuccess = backend.snapshot().version
|
|
3803
|
+
const replay = await backend.submit(request, { latencyMs: 0 })
|
|
3804
|
+
const replaySnapshot = backend.snapshot()
|
|
3805
|
+
check('假后端契约:同 opId 同 body 原样重放 → 200 / idempotent / 版本与资源不重复',
|
|
3806
|
+
replay.status === 200 && replay.data?.idempotent === true && replay.data?.version === versionAfterSuccess && replaySnapshot.version === versionAfterSuccess && replay.data?.result?.counts?.points === 1,
|
|
3807
|
+
`HTTP ${replay.status} · idempotent=${replay.data?.idempotent} · v${replaySnapshot.version}`)
|
|
3808
|
+
const differentBody = {
|
|
3809
|
+
...request,
|
|
3810
|
+
body: {
|
|
3811
|
+
...request.body,
|
|
3812
|
+
points: [{ id: 'contract-p2', name: '契约点 2' }]
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
const conflict = await backend.submit(differentBody, { latencyMs: 0 })
|
|
3816
|
+
check('假后端契约:同 opId 换 body → 409 IDEMPOTENCY_CONFLICT,版本不前进',
|
|
3817
|
+
conflict.status === 409 && conflict.code === 'IDEMPOTENCY_CONFLICT' && conflict.body?.data?.conflict?.type === 'idempotency-key' && backend.snapshot().version === versionAfterSuccess,
|
|
3818
|
+
`HTTP ${conflict.status} · ${conflict.code} · v${backend.snapshot().version}`)
|
|
3819
|
+
const mismatchHeader = backend.makeRequest({ url: '/gis/copies/batch', body: { op: 'copy.batch', points: [] } })
|
|
3820
|
+
mismatchHeader.headers['Idempotency-Key'] = 'contract-wrong'
|
|
3821
|
+
const headerMismatch = await backend.submit(mismatchHeader, { latencyMs: 0 })
|
|
3822
|
+
check('假后端契约:Idempotency-Key 与 body.opId 不一致 → 400 BAD_REQUEST',
|
|
3823
|
+
headerMismatch.status === 400 && headerMismatch.code === 'BAD_REQUEST' && /Idempotency-Key/.test(headerMismatch.reason ?? ''),
|
|
3824
|
+
`HTTP ${headerMismatch.status} · ${headerMismatch.code}`)
|
|
3825
|
+
const mismatchIfMatch = backend.makeRequest({ url: '/gis/copies/batch', body: { op: 'copy.batch', points: [] } })
|
|
3826
|
+
mismatchIfMatch.headers['If-Match'] = '"v999"'
|
|
3827
|
+
const ifMatchMismatch = await backend.submit(mismatchIfMatch, { latencyMs: 0 })
|
|
3828
|
+
check('假后端契约:If-Match 与 body.baseVersion 不一致 → 400 BAD_REQUEST',
|
|
3829
|
+
ifMatchMismatch.status === 400 && ifMatchMismatch.code === 'BAD_REQUEST' && /If-Match/.test(ifMatchMismatch.reason ?? ''),
|
|
3830
|
+
`HTTP ${ifMatchMismatch.status} · ${ifMatchMismatch.code}`)
|
|
3831
|
+
const versionBeforeInvalid = backend.snapshot().version
|
|
3832
|
+
const invalidVersion = backend.makeRequest({ url: '/gis/copies/batch', body: { op: 'copy.batch', points: [] } })
|
|
3833
|
+
invalidVersion.body.baseVersion = 'not-a-number'
|
|
3834
|
+
const badVersion = await backend.submit(invalidVersion, { latencyMs: 0 })
|
|
3835
|
+
check('假后端契约:baseVersion 不是有限数字 → 400,版本不动',
|
|
3836
|
+
badVersion.status === 400 && badVersion.code === 'BAD_REQUEST' && backend.snapshot().version === versionBeforeInvalid,
|
|
3837
|
+
`HTTP ${badVersion.status} · ${badVersion.code} · v${backend.snapshot().version}`)
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
catch (error) {
|
|
3841
|
+
check('自测过程没有抛错', false, error instanceof Error ? `${error.message}\n${error.stack ?? ''}` : String(error))
|
|
3842
|
+
}
|
|
3843
|
+
const failures = results.filter(line => line.startsWith('FAIL'))
|
|
3844
|
+
document.getElementById('out').textContent = `${document.title} ${failures.length ? 'FAIL' : 'PASS'}\n${results.join('\n')}`
|
|
3845
|
+
document.title = `${document.title} ${failures.length ? 'FAIL' : 'PASS'}`
|
|
3846
|
+
</script>
|
|
3847
|
+
</body>
|
|
3848
|
+
</html>
|