gis-view-webgl2 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -8
- package/dist/gis-view.css +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1036 -249
- package/dist/index.js +1 -1
- package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +194 -0
- package/docs/GisView.md +9 -8
- package/docs/GisView/345/256/236/347/216/260/345/216/237/347/220/206.md +2 -2
- 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 +172 -0
- package/docs/api/index.html +545 -59
- package/docs/demos/BackendSyncDemo.vue +10 -8
- package/docs/demos/BasicDemo.vue +101 -101
- package/docs/demos/BatchAddDemo.vue +5 -5
- package/docs/demos/ChannelDemo.vue +617 -10
- package/docs/demos/ConnectDemo.vue +392 -0
- package/docs/demos/CopyMergeDemo.vue +980 -929
- package/docs/demos/DecorationDraftDemo.vue +1300 -0
- package/docs/demos/DemoShell.vue +569 -240
- package/docs/demos/DragDropDemo.vue +11 -11
- package/docs/demos/EffectsDemo.vue +6 -6
- package/docs/demos/EventDemo.vue +327 -327
- package/docs/demos/ExtensionDemo.vue +458 -0
- package/docs/demos/GisViewDemo.vue +61 -48
- package/docs/demos/GraphicDemo.vue +338 -0
- package/docs/demos/LabelDemo.vue +553 -497
- package/docs/demos/MinimapDemo.vue +358 -0
- package/docs/demos/PerfDemo.vue +29 -0
- package/docs/demos/SearchFilterDemo.vue +412 -0
- package/docs/demos/StyleDemo.vue +121 -121
- package/docs/demos/api-index.js +98 -77
- package/docs/demos/demos.js +159 -135
- package/docs/demos/shared/demoData.js +107 -107
- package/docs/demos/shared/demoStyle.css +249 -249
- package/docs/demos/shared/fakeBackend.js +93 -6
- 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 +28 -8
- package/docs/format/04-/351/200/232/351/201/223/344/270/216/346/240/207/347/255/276.md +5 -4
- package/docs/format/06-/345/257/274/345/205/245/345/257/274/345/207/272/344/270/216/346/240/207/345/207/206/346/226/207/346/241/243.md +8 -6
- package/docs/format/07-/344/272/213/344/273/266/350/275/275/350/215/267/344/270/216/347/244/272/344/276/213/346/225/260/346/215/256.md +6 -6
- package/docs/guide/sample-document.html +4 -4
- package/docs/index.html +180 -9
- package/docs/internals/11-/350/246/206/347/233/226/345/261/202/346/240/207/347/255/276/344/270/216/346/217/220/347/244/272.md +1 -1
- package/docs/internals/13-/351/271/260/347/234/274/345/233/276.md +3 -2
- package/docs/internals/15-03-/351/200/220/350/275/256/350/256/260/345/275/225-/346/200/247/350/203/275/344/270/216/344/277/256/345/244/215-/344/270/213.md +16 -0
- package/docs/internals/16-03-/344/274/230/345/214/226/344/270/216/345/217/226/350/210/215-/344/270/213.md +11 -0
- package/docs/internals/17-/350/260/203/350/257/225/344/270/216/346/211/251/345/261/225/345/273/272/350/256/256.md +1 -1
- package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +18 -4
- package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +110 -25
- package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +13 -8
- 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 +111 -10
- 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 +47 -9
- package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +17 -15
- package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +61 -11
- 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 +74 -7
- package/docs/manual/11-/346/240/267/345/274/217/344/270/216/350/247/204/345/210/231.md +3 -2
- package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +260 -3
- package/docs/manual/13-/345/212/250/346/225/210/344/270/216/345/233/276/345/205/203.md +6 -5
- package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +27 -14
- package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +142 -49
- package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +31 -29
- package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +55 -39
- package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +189 -64
- package/docs/manual/19-/350/257/273/346/225/260/344/270/216/346/213/226/346/213/275/351/242/204/350/247/210.md +4 -4
- package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +42 -2
- 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 +22 -6
- package/docs/playground/index.html +5 -5
- package/docs/steps/README.md +14 -10
- package/docs/steps/check-doc-truth.mjs +1 -1
- package/docs/steps/selftest-actions.html +10 -10
- package/docs/steps/selftest-data.html +96 -14
- package/docs/steps/selftest-decoration.html +577 -0
- package/docs/steps/selftest-demo.html +6 -5
- package/docs/steps/selftest-demos.html +2201 -1612
- package/docs/steps/selftest-docs.html +89 -16
- package/docs/steps/selftest-effects.html +5 -5
- package/docs/steps/selftest-features.html +757 -86
- package/docs/steps/selftest-full.html +583 -583
- package/docs/steps/selftest-labels.html +1034 -652
- package/docs/steps/selftest-perf.html +1899 -1844
- package/docs/steps/selftest-scale.html +117 -7
- package/docs/steps/selftest-select.html +24 -17
- package/docs/steps/step-10-full.js +1 -1
- package/docs//346/216/245/345/217/243/345/215/217/350/256/256.md +167 -17
- package/docs//346/225/260/346/215/256/346/240/274/345/274/217.md +1 -1
- package/package.json +43 -43
- package/docs/demos/HistoryDemo.vue +0 -230
- package/docs/demos/InterfaceDemo.vue +0 -309
- package/docs/demos/SampleDemo.vue +0 -283
|
@@ -1,1686 +1,2275 @@
|
|
|
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
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
node.dispatchEvent(new PointerEvent('
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
Element.prototype.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
check('
|
|
93
|
-
check('
|
|
94
|
-
check('
|
|
95
|
-
check('
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
check('
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
27
|
+
import { channelDraftArrowPlacement } from '/src/gis-view/engine.js'
|
|
28
|
+
const results = []
|
|
29
|
+
const flush = () => { document.getElementById('out').textContent = results.join('\n') }
|
|
30
|
+
const check = (name, ok, extra = '') => {
|
|
31
|
+
results.push(`${ok ? 'PASS' : 'FAIL'} ${name}${extra === '' ? '' : ` — ${extra}`}`)
|
|
32
|
+
flush()
|
|
33
|
+
}
|
|
34
|
+
const diag = (name, extra) => { results.push(`DIAG ${name} — ${extra}`); flush() }
|
|
35
|
+
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
36
|
+
const until = async (fn, timeout = 20000) => {
|
|
37
|
+
const start = performance.now()
|
|
38
|
+
while (performance.now() - start < timeout) {
|
|
39
|
+
const value = await fn()
|
|
40
|
+
if (value) {
|
|
41
|
+
return value
|
|
42
|
+
}
|
|
43
|
+
await wait(120)
|
|
44
|
+
}
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
const tabs = () => [...document.querySelectorAll('.demo-shell__tab')]
|
|
48
|
+
const findTab = text => tabs().find(tab => tab.textContent.trim().includes(text))
|
|
49
|
+
const clickTab = async (text) => {
|
|
50
|
+
const tab = findTab(text)
|
|
51
|
+
if (!tab) {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
tab.click()
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
57
|
+
const canvasReady = async (timeout = 20000) => until(() => document.querySelector('canvas.gis-view__canvas'), timeout)
|
|
58
|
+
const noticeText = () => document.querySelector('.demo-shell__notice')?.textContent.trim() ?? ''
|
|
59
|
+
const logText = () => [...document.querySelectorAll('.dv-log li')].map(node => node.textContent.trim()).join(' | ')
|
|
60
|
+
/** 合成一次「点一下」:引擎是按 pointerdown / pointerup 一对判的 */
|
|
61
|
+
const tap = (node, options = {}) => {
|
|
62
|
+
const rect = node.getBoundingClientRect()
|
|
63
|
+
const init = {
|
|
64
|
+
bubbles: true,
|
|
65
|
+
cancelable: true,
|
|
66
|
+
composed: true,
|
|
67
|
+
pointerId: 1,
|
|
68
|
+
pointerType: 'mouse',
|
|
69
|
+
isPrimary: true,
|
|
70
|
+
button: 0,
|
|
71
|
+
clientX: options.clientX ?? rect.left + rect.width / 2,
|
|
72
|
+
clientY: options.clientY ?? rect.top + rect.height / 2
|
|
73
|
+
}
|
|
74
|
+
node.dispatchEvent(new PointerEvent('pointerdown', init))
|
|
75
|
+
node.dispatchEvent(new PointerEvent('pointerup', init))
|
|
76
|
+
}
|
|
77
|
+
const contextMenuOn = (node, clientX, clientY) => {
|
|
78
|
+
const event = new MouseEvent('contextmenu', { bubbles: true, cancelable: true, composed: true, clientX, clientY })
|
|
79
|
+
node.dispatchEvent(event)
|
|
80
|
+
return event
|
|
81
|
+
}
|
|
82
|
+
// 合成事件下没有真实指针,setPointerCapture 由自测自己兜住(与 selftest-labels.html 同一套)
|
|
83
|
+
if (!Element.prototype.setPointerCapture) {
|
|
84
|
+
Element.prototype.setPointerCapture = () => {}
|
|
85
|
+
Element.prototype.releasePointerCapture = () => {}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
createApp(App).mount('#app')
|
|
90
|
+
// 1. 外壳:默认综合示例,页签齐全
|
|
91
|
+
const loaded = await canvasReady()
|
|
92
|
+
check('演示外壳把默认演示(综合示例)挂起来了', Boolean(loaded), loaded ? 'canvas 就绪' : '没等到 canvas')
|
|
93
|
+
check('页签列出全部演示(20 个:19 个功能 + 综合)', tabs().length === 20, `${tabs().length} 个页签:${tabs().map(tab => tab.textContent.trim()).join(' / ')}`)
|
|
94
|
+
check('页头的演示数量直接跟随注册表(20 个)', (document.querySelector('.page-header__count')?.textContent ?? '').includes('20 个'), document.querySelector('.page-header__count')?.textContent?.trim() ?? '没有页头计数')
|
|
95
|
+
check('默认页签是综合示例', (document.querySelector('.demo-shell__tab--active')?.textContent ?? '').includes('综合'), document.querySelector('.demo-shell__tab--active')?.textContent?.trim() ?? '')
|
|
96
|
+
check('默认页签写进地址栏(刷新还停在同一个演示)', location.search.includes('demo=full'), location.search)
|
|
97
|
+
check('综合示例的面板照旧在(拆分没有动它)', document.querySelectorAll('.demo-panel-buttons button').length >= 6, `${document.querySelectorAll('.demo-panel-buttons button').length} 个面板按钮`)
|
|
98
|
+
// 1.1 外壳的展现:分组页签、对应文档的直达链接、键盘翻页、上次看过的记在 localStorage
|
|
99
|
+
const groupTabs = [...document.querySelectorAll('.demo-shell__group-tab')]
|
|
100
|
+
const groupLabels = groupTabs.map(tab => tab.querySelector('.demo-shell__group-label')?.textContent.trim() ?? '')
|
|
101
|
+
const groupLabelsVisible = groupTabs.every(tab => {
|
|
102
|
+
const label = tab.querySelector('.demo-shell__group-label')
|
|
103
|
+
return label && getComputedStyle(label).display !== 'none'
|
|
104
|
+
})
|
|
105
|
+
check('页签按分类分组(基础 / 交互 / 样式 / 数据 / 性能 / 综合),组名直接可读', groupLabels.join(',') === '基础,交互,样式,数据,性能,综合' && groupLabelsVisible, groupLabels.join(' / '))
|
|
106
|
+
const visibleGroups = [...document.querySelectorAll('.demo-shell__group--active')]
|
|
107
|
+
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(' / ')}`)
|
|
108
|
+
const docLink = document.querySelector('.demo-shell__doc')
|
|
109
|
+
check('说明旁边给对应文档的直达链接(文档中心 + 文件名)', /docs\/index\.html/.test(docLink?.getAttribute('href') ?? '') && (docLink?.textContent ?? '').includes('对应文档'), docLink?.getAttribute('href') ?? '无')
|
|
110
|
+
const quickLinks = [...document.querySelectorAll('.demo-shell__quick-link')]
|
|
111
|
+
const quickLabels = quickLinks.map(link => link.textContent.trim())
|
|
112
|
+
check('演示外壳常驻文档中心 / API 速查 / 调试页 / 分步演示 / 自测五个入口', quickLinks.length === 5 && ['文档中心', 'API 速查', '调试页', '分步演示', '自测'].every(label => quickLabels.includes(label)) && quickLinks.every(link => link.getAttribute('href')?.includes('/docs/')), quickLabels.join(' / '))
|
|
113
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: ']' }))
|
|
114
|
+
// Vue 的 DOM 更新在微任务里:派发之后等一拍再读当前页签(不然读到的还是旧的那一个)
|
|
115
|
+
await wait(60)
|
|
116
|
+
const afterBracket = document.querySelector('.demo-shell__tab--active')?.textContent.trim() ?? ''
|
|
117
|
+
check('键盘 ] 切到下一个演示(综合示例在最后,绕回基础视图)', afterBracket === '基础视图' && location.search.includes('demo=basic'), `${afterBracket} / ${location.search}`)
|
|
118
|
+
check('上次看过的演示写进 localStorage(下次打开接着看)', window.localStorage.getItem('gis-view:last-demo') === 'basic', String(window.localStorage.getItem('gis-view:last-demo')))
|
|
119
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: '[' }))
|
|
120
|
+
await wait(60)
|
|
121
|
+
const afterBack = document.querySelector('.demo-shell__tab--active')?.textContent.trim() ?? ''
|
|
122
|
+
check('键盘 [ 往回翻(基础视图回到综合示例)', afterBack.includes('综合'), afterBack)
|
|
123
|
+
const eyebrow = document.querySelector('.demo-shell__eyebrow')?.textContent.trim() ?? ''
|
|
124
|
+
check('当前演示显示分组与序号(综合 · 20 / 20)', eyebrow.includes('综合') && eyebrow.includes('20 / 20'), eyebrow)
|
|
125
|
+
document.querySelector('[data-demo=demo-next]')?.click()
|
|
126
|
+
await wait(60)
|
|
127
|
+
const afterStepNext = document.querySelector('.demo-shell__tab--active')?.textContent.trim() ?? ''
|
|
128
|
+
document.querySelector('[data-demo=demo-prev]')?.click()
|
|
129
|
+
await wait(60)
|
|
130
|
+
const afterStepPrev = document.querySelector('.demo-shell__tab--active')?.textContent.trim() ?? ''
|
|
131
|
+
check('上一项 / 下一项按钮与 [ ] 同一套顺序,并从头尾绕回', afterStepNext === '基础视图' && afterStepPrev.includes('综合'), `${afterStepNext} → ${afterStepPrev}`)
|
|
132
|
+
const keyGuard = document.createElement('input')
|
|
133
|
+
document.body.appendChild(keyGuard)
|
|
134
|
+
keyGuard.dispatchEvent(new KeyboardEvent('keydown', { key: ']', bubbles: true }))
|
|
135
|
+
await wait(60)
|
|
136
|
+
const afterGuard = document.querySelector('.demo-shell__tab--active')?.textContent.trim() ?? ''
|
|
137
|
+
keyGuard.remove()
|
|
138
|
+
check('焦点在输入框里时键盘 [ / ] 不抢键', afterGuard.includes('综合'), afterGuard)
|
|
139
|
+
// 2. 每个功能演示都切一遍:画布起得来、说明与读数有东西
|
|
140
|
+
const demoSpecs = [
|
|
141
|
+
{ tab: '基础视图', id: 'basic' },
|
|
124
142
|
{ tab: '标签交互', id: 'labels' },
|
|
143
|
+
{ tab: '鹰眼图配置', id: 'minimap' },
|
|
125
144
|
{ tab: '样式与规则', id: 'style' },
|
|
126
|
-
{ tab: '通道与数据', id: 'channel' },
|
|
127
|
-
{ tab: '事件与方法', id: 'events' },
|
|
128
|
-
{ tab: '批量复制与合并', id: 'copy-merge' },
|
|
145
|
+
{ tab: '通道与数据', id: 'channel' },
|
|
146
|
+
{ tab: '事件与方法', id: 'events' },
|
|
147
|
+
{ tab: '批量复制与合并', id: 'copy-merge' },
|
|
129
148
|
{ tab: '拖拽落点', id: 'drag' },
|
|
130
|
-
{ tab: '
|
|
149
|
+
{ tab: '可视化连线', id: 'connect' },
|
|
150
|
+
{ tab: '动态装饰器', id: 'decoration' },
|
|
131
151
|
{ tab: '批量新增', id: 'batch-add' },
|
|
132
|
-
{ tab: '
|
|
133
|
-
{ tab: '
|
|
134
|
-
{ tab: '编辑与保存', id: 'backend' },
|
|
152
|
+
{ tab: '搜索与筛选', id: 'search-filter' },
|
|
153
|
+
{ tab: '扩展能力', id: 'extension' },
|
|
154
|
+
{ tab: '编辑与保存', id: 'backend' },
|
|
135
155
|
{ tab: '性能与质量', id: 'perf' },
|
|
136
|
-
{ tab: '动画效果', id: 'effects' }
|
|
137
|
-
|
|
156
|
+
{ tab: '动画效果', id: 'effects' },
|
|
157
|
+
{ tab: '扩展图形', id: 'graphic' }
|
|
158
|
+
]
|
|
138
159
|
for (const spec of demoSpecs) {
|
|
139
160
|
const clicked = await clickTab(spec.tab)
|
|
140
|
-
const ready = clicked ? await canvasReady() : null
|
|
141
|
-
const panel = document.querySelector('.dv-demo .dv-panel')
|
|
142
|
-
const active = document.querySelector('.demo-shell__tab--active')?.textContent.trim() ?? ''
|
|
143
|
-
check(`切到「${spec.tab}」:画布与读数面板都起来了`, Boolean(ready) && Boolean(panel) && active === spec.tab, `${ready ? 'canvas 就绪' : '没有 canvas'} / ${panel ? `${panel.querySelectorAll('.dv-group').length} 组读数` : '没有面板'} / 当前页签 ${active}`)
|
|
144
|
-
await wait(260)
|
|
161
|
+
const ready = clicked ? await canvasReady() : null
|
|
162
|
+
const panel = document.querySelector('.dv-demo .dv-panel')
|
|
163
|
+
const active = document.querySelector('.demo-shell__tab--active')?.textContent.trim() ?? ''
|
|
164
|
+
check(`切到「${spec.tab}」:画布与读数面板都起来了`, Boolean(ready) && Boolean(panel) && active === spec.tab, `${ready ? 'canvas 就绪' : '没有 canvas'} / ${panel ? `${panel.querySelectorAll('.dv-group').length} 组读数` : '没有面板'} / 当前页签 ${active}`)
|
|
165
|
+
await wait(260)
|
|
145
166
|
check(`「${spec.tab}」把演示 id 写进地址栏(?demo=${spec.id})`, location.search.includes(`demo=${spec.id}`), location.search)
|
|
146
167
|
}
|
|
147
168
|
|
|
148
|
-
// 2.
|
|
149
|
-
if (await clickTab('
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
click('batch-add-points')
|
|
163
|
-
const pointsAfter = await until(() => view.getData().points.length === pointsBefore + 4, 6000)
|
|
164
|
-
check('批量新增:addPoints 一次补 4 个节点', Boolean(pointsAfter) && text('batch-add-readout').includes('addPoints'), `${pointsBefore} → ${view.getData().points.length} / ${text('batch-add-readout')}`)
|
|
165
|
-
const linesBefore = view.getData().lines.length
|
|
166
|
-
click('batch-add-lines')
|
|
167
|
-
const linesAfter = await until(() => view.getData().lines.length === linesBefore + 4, 6000)
|
|
168
|
-
check('批量新增:addLines 一次补 4 条线路', Boolean(linesAfter) && text('batch-add-readout').includes('addLines'), `${linesBefore} → ${view.getData().lines.length}`)
|
|
169
|
-
const channelsBefore = view.getOptions().channels?.length ?? 0
|
|
170
|
-
click('batch-add-channels')
|
|
171
|
-
const channelsAfter = await until(() => (view.getOptions().channels?.length ?? 0) === channelsBefore + 2, 6000)
|
|
172
|
-
check('批量新增:addChannels 一次补 2 条通道', Boolean(channelsAfter) && text('batch-add-readout').includes('addChannels'), `${channelsBefore} → ${view.getOptions().channels?.length ?? 0}`)
|
|
173
|
-
const groupsBefore = groupsOf()
|
|
174
|
-
click('batch-add-channel-groups')
|
|
175
|
-
const groupsAfter = await until(() => groupsOf() === groupsBefore + 2, 6000)
|
|
176
|
-
check('批量新增:addChannelGroups 一次补 2 组、4 条回线', Boolean(groupsAfter) && text('batch-add-readout').includes('2 组 / 4 条回线'), `${groupsBefore} → ${groupsOf()} / ${text('batch-add-readout')}`)
|
|
177
|
-
const waypointsBefore = waypointCount()
|
|
178
|
-
click('batch-add-waypoints')
|
|
179
|
-
const waypointsAfter = await until(() => waypointCount() > waypointsBefore, 6000)
|
|
180
|
-
check('批量新增:addWaypoints 一次插多个拐点且不虚报失败坐标', Boolean(waypointsAfter) && text('batch-add-readout').includes('addWaypoints') && !text('batch-add-readout').includes('没有落地'), `${waypointsBefore} → ${waypointCount()} / ${text('batch-add-readout')}`)
|
|
169
|
+
// 2.0 动态装饰器:吸附方式与位置来源正交,两种吸附方式都能选三种位置
|
|
170
|
+
if (await clickTab('动态装饰器')) {
|
|
171
|
+
await canvasReady()
|
|
172
|
+
const modeSelect = document.querySelector('[data-demo=decoration-point-snap-mode]')
|
|
173
|
+
const modeValues = [...(modeSelect?.options ?? [])].map(option => option.value)
|
|
174
|
+
check('动态装饰器:吸附方式只保留「跟随 / 固定」两档', modeValues.join(',') === 'follow,fixed', modeValues.join(' / ') || '没有吸附方式下拉')
|
|
175
|
+
const positionSelect = document.querySelector('[data-demo=decoration-point-snap-position]')
|
|
176
|
+
const positionValues = [...(positionSelect?.options ?? [])].map(option => option.value)
|
|
177
|
+
const snapSetup = document.querySelector('.dv-demo')?.__vueParentComponent?.setupState
|
|
178
|
+
check('动态装饰器:跟随下位置来源有相对节点 / 固定位置 / 鼠标位置三档', positionValues.join(',') === 'offset,fixed,pointer' && snapSetup?.decoratorOptions?.placement === 'offset' && snapSetup?.decoratorOptions?.anchor === 'follow', `${positionValues.join(' / ') || '没有位置来源下拉'} · placement=${snapSetup?.decoratorOptions?.placement} · anchor=${snapSetup?.decoratorOptions?.anchor}`)
|
|
179
|
+
if (modeSelect) {
|
|
180
|
+
modeSelect.value = 'fixed'
|
|
181
|
+
modeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
182
|
+
await wait(120)
|
|
181
183
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
+
const fixedPositionValues = [...(positionSelect?.options ?? [])].map(option => option.value)
|
|
185
|
+
check('动态装饰器:固定 + 相对节点仍可添加,只切换 anchor', fixedPositionValues.join(',') === 'offset,fixed,pointer' && snapSetup?.decoratorOptions?.placement === 'offset' && snapSetup?.decoratorOptions?.anchor === 'fixed' && snapSetup?.decoratorOptions?.at === undefined, `${fixedPositionValues.join(' / ')} · placement=${snapSetup?.decoratorOptions?.placement} · anchor=${snapSetup?.decoratorOptions?.anchor} · at=${JSON.stringify(snapSetup?.decoratorOptions?.at)}`)
|
|
186
|
+
if (positionSelect) {
|
|
187
|
+
positionSelect.value = 'pointer'
|
|
188
|
+
positionSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
189
|
+
await wait(120)
|
|
190
|
+
}
|
|
191
|
+
check('动态装饰器:固定 + 鼠标位置映射为 pointer / fixed,且不传 at', snapSetup?.decoratorOptions?.placement === 'pointer' && snapSetup?.decoratorOptions?.anchor === 'fixed' && snapSetup?.decoratorOptions?.at === undefined, `placement=${snapSetup?.decoratorOptions?.placement} · anchor=${snapSetup?.decoratorOptions?.anchor} · at=${JSON.stringify(snapSetup?.decoratorOptions?.at)}`)
|
|
192
|
+
if (modeSelect) {
|
|
193
|
+
modeSelect.value = 'follow'
|
|
194
|
+
modeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
195
|
+
await wait(120)
|
|
184
196
|
}
|
|
197
|
+
check('动态装饰器:跟随 + 鼠标位置保留相同位置来源,只把 anchor 改为 follow', snapSetup?.decoratorOptions?.placement === 'pointer' && snapSetup?.decoratorOptions?.anchor === 'follow', `placement=${snapSetup?.decoratorOptions?.placement} · anchor=${snapSetup?.decoratorOptions?.anchor}`)
|
|
198
|
+
if (positionSelect) {
|
|
199
|
+
positionSelect.value = 'fixed'
|
|
200
|
+
positionSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
201
|
+
await wait(120)
|
|
202
|
+
}
|
|
203
|
+
check('动态装饰器:跟随 + 固定位置仍传世界坐标 at', snapSetup?.decoratorOptions?.placement === 'fixed' && snapSetup?.decoratorOptions?.anchor === 'follow' && Array.isArray(snapSetup?.decoratorOptions?.at), `placement=${snapSetup?.decoratorOptions?.placement} · anchor=${snapSetup?.decoratorOptions?.anchor} · at=${JSON.stringify(snapSetup?.decoratorOptions?.at)}`)
|
|
185
204
|
}
|
|
186
205
|
|
|
187
|
-
// 2.
|
|
188
|
-
if (await clickTab('
|
|
206
|
+
// 2.0 可视化连线:无通道数据、折线 / 箭头预览配置、取消与三种真实落地
|
|
207
|
+
if (await clickTab('可视化连线')) {
|
|
189
208
|
const ready = await canvasReady()
|
|
190
|
-
await wait(
|
|
209
|
+
await wait(320)
|
|
191
210
|
const demo = document.querySelector('.dv-demo')
|
|
192
211
|
const setup = demo?.__vueParentComponent?.setupState
|
|
193
212
|
const view = await until(() => setup?.view, 8000)
|
|
194
|
-
const click = key => document.querySelector(`[data-demo="${key}"]`)?.click()
|
|
195
213
|
const text = key => document.querySelector(`[data-demo="${key}"]`)?.textContent.trim() ?? ''
|
|
196
|
-
const
|
|
214
|
+
const channelList = () => (view?.getOptions().channels ?? [])
|
|
197
215
|
if (ready && view) {
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const allBox = { x: 0, y: 0, width: canvasRect.width, height: canvasRect.height }
|
|
205
|
-
view.updateOptions({ selectionKinds: ['point'] })
|
|
206
|
-
view.selectInBox(allBox)
|
|
207
|
-
const onlyPoints = view.getSelectionData()
|
|
208
|
-
check('批量复制与合并:框选类型只开 point 时不含线路 / 通道', onlyPoints.pointCount > 0 && onlyPoints.lineCount === 0 && onlyPoints.channelCount === 0, JSON.stringify({ points: onlyPoints.pointCount, lines: onlyPoints.lineCount, channels: onlyPoints.channelCount }))
|
|
209
|
-
view.updateOptions({ selectionKinds: ['line'] })
|
|
210
|
-
view.selectInBox(allBox)
|
|
211
|
-
const onlyLines = view.getSelectionData()
|
|
212
|
-
check('批量复制与合并:框选类型只开 line 时只有通道外线路', onlyLines.pointCount === 0 && onlyLines.lineCount > 0 && onlyLines.channelCount === 0, JSON.stringify({ points: onlyLines.pointCount, lines: onlyLines.lineCount, channels: onlyLines.channelCount }))
|
|
213
|
-
view.updateOptions({ selectionKinds: ['channel'] })
|
|
214
|
-
view.selectInBox(allBox)
|
|
215
|
-
const onlyChannels = view.getSelectionData()
|
|
216
|
-
check('批量复制与合并:框选类型只开 channel 时只有通道回线', onlyChannels.pointCount === 0 && onlyChannels.lineCount > 0 && onlyChannels.channelCount > 0, JSON.stringify({ points: onlyChannels.pointCount, lines: onlyChannels.lineCount, channels: onlyChannels.channelCount }))
|
|
217
|
-
view.updateOptions({ selectionKinds: ['point', 'line', 'channel'] })
|
|
218
|
-
click('copy-select-sources')
|
|
219
|
-
await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
220
|
-
const pointsBeforeCopy = view.getData().points.length
|
|
221
|
-
click('copy-selected')
|
|
222
|
-
const copied = await until(() => view.getData().points.length >= pointsBeforeCopy + 4, 6000)
|
|
223
|
-
const copyPayload = unwrap(setup.lastBatch)
|
|
224
|
-
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}`)
|
|
225
|
-
check('批量复制与合并:复制读数能从事件载荷看到来源与落地数据', text('copy-readout').includes('copySelectedPoints') && text('copy-readout').includes('来源') && text('copy-readout').includes('线路') && text('copy-readout').includes('通道'), text('copy-readout'))
|
|
226
|
-
click('copy-revert')
|
|
227
|
-
const reverted = await until(() => view.getData().points.length === pointsBeforeCopy, 6000)
|
|
228
|
-
check('批量复制与合并:revert / cancel 一次收回整组复制', Boolean(reverted) && text('copy-action').includes('已撤销整组复制'), `${pointsBeforeCopy} / ${view.getData().points.length} · ${text('copy-action')}`)
|
|
229
|
-
click('select-sample-channel')
|
|
230
|
-
const channelSelected = await until(() => view.getSelectionData().channelCount === 1 && view.getSelectionData().lineCount === 3, 5000)
|
|
231
|
-
const pointsBeforeChannelCopy = view.getData().points.length
|
|
232
|
-
click('copy-selected')
|
|
233
|
-
const channelCopied = await until(() => view.getData().points.length === pointsBeforeChannelCopy + 2, 6000)
|
|
234
|
-
const channelCopyPayload = unwrap(setup.lastBatch)
|
|
235
|
-
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}`)
|
|
236
|
-
const channelCopyLinks = channelCopyPayload?.lineChannels ?? []
|
|
237
|
-
check('批量复制与合并:lineChannels 与复制出来的回线一一对齐,并指向新通道 / 新分组',
|
|
238
|
-
channelCopyLinks.length === channelCopyPayload?.lines?.length &&
|
|
239
|
-
channelCopyLinks.every((slot, index) => slot &&
|
|
240
|
-
slot.lineIndex === channelCopyPayload.lineIndexes[index] &&
|
|
241
|
-
slot.lineId === channelCopyPayload.lines[index].id &&
|
|
242
|
-
slot.channelIndex === channelCopyPayload.channelIndexes[0] &&
|
|
243
|
-
slot.channelId === channelCopyPayload.channels[0].id &&
|
|
244
|
-
Array.isArray(slot.group?.lineIds) && slot.group.lineIds.includes(slot.lineId)) &&
|
|
245
|
-
text('copy-line-channels-readout').includes('→'),
|
|
246
|
-
`${text('copy-line-channels-readout')} / ${JSON.stringify(channelCopyLinks.map(slot => ({ line: slot?.lineIndex, channel: slot?.channelIndex, group: slot?.groupId })))}`)
|
|
247
|
-
click('copy-revert')
|
|
248
|
-
await until(() => view.getData().points.length === pointsBeforeChannelCopy, 6000)
|
|
249
|
-
click('copy-select-sources')
|
|
250
|
-
await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
251
|
-
click('copy-clipboard')
|
|
252
|
-
const clipboardReady = await until(() => text('clipboard-readout').includes('count=4'), 5000)
|
|
253
|
-
const clipboard = unwrap(setup.clipboardItem)
|
|
254
|
-
check('批量复制与合并:copyPointsToClipboard 留下整组来源快照', Boolean(clipboardReady) && clipboard?.sources?.length === 4 && clipboard?.count === 4, text('clipboard-readout'))
|
|
255
|
-
const pointsBeforePaste = view.getData().points.length
|
|
256
|
-
click('paste-points')
|
|
257
|
-
const pasted = await until(() => view.getData().points.length >= pointsBeforePaste + 4, 6000)
|
|
258
|
-
check('批量复制与合并:pastePoints 按整组锚点落回画布', Boolean(pasted) && text('copy-readout').includes('pastePoints'), `${pointsBeforePaste} → ${view.getData().points.length} / ${text('copy-readout')}`)
|
|
259
|
-
click('copy-revert')
|
|
260
|
-
await until(() => view.getData().points.length === pointsBeforePaste, 6000)
|
|
261
|
-
click('merge-select-sources')
|
|
262
|
-
await wait(120)
|
|
263
|
-
const targetOptionCount = document.querySelectorAll('[data-demo="merge-target"] option').length
|
|
264
|
-
check('批量复制与合并:保留目标下拉只列当前选中节点', targetOptionCount === 4, `${targetOptionCount} 个选项`)
|
|
265
|
-
const targetId = String(setup.mergeTargetId ?? '')
|
|
266
|
-
const pointsBeforeMerge = view.getData().points.length
|
|
267
|
-
click('merge-selected')
|
|
268
|
-
const mergedCount = await until(() => view.getData().points.length === pointsBeforeMerge - 3, 6000)
|
|
269
|
-
const target = view.getData().points.find(point => String(point.id) === targetId)
|
|
270
|
-
check('批量复制与合并:mergeSelectedPoints 指定保留目标并并掉 3 个来源', Boolean(mergedCount) && target?.demo_merged === true && target?.zone_name === '合并后(item 改写)', `${pointsBeforeMerge} → ${view.getData().points.length} / 目标 ${targetId} / ${target?.name ?? '—'}`)
|
|
271
|
-
check('批量复制与合并:合并事件读数带来源、目标与最终节点', text('merge-readout').includes('mergeSelectedPoints') && text('merge-readout').includes('保留') && text('merge-readout').includes('并掉') && text('merge-readout').includes('最终'), text('merge-readout'))
|
|
272
|
-
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'))
|
|
273
|
-
const mergeTransactionReady = await until(() => {
|
|
274
|
-
const preview = text('copy-merge-backend-request')
|
|
275
|
-
return preview.includes('POST /gis/merges') && preview.includes('200 OK')
|
|
276
|
-
}, 8000)
|
|
277
|
-
const mergeRequestPreview = text('copy-merge-backend-request')
|
|
278
|
-
check('批量复制与合并:mergeSelectedPoints 收成一条 /gis/merges 事务请求',
|
|
279
|
-
Boolean(mergeTransactionReady) &&
|
|
280
|
-
mergeRequestPreview.includes('POST /gis/merges') &&
|
|
281
|
-
mergeRequestPreview.includes('"changes"') &&
|
|
282
|
-
mergeRequestPreview.includes('"impact"') &&
|
|
283
|
-
mergeRequestPreview.includes('"baseVersion"') &&
|
|
284
|
-
mergeRequestPreview.includes('"opId"') &&
|
|
285
|
-
mergeRequestPreview.includes('200 OK'),
|
|
286
|
-
mergeRequestPreview.slice(0, 300).replace(/\s+/g, ' '))
|
|
287
|
-
check('批量复制与合并:合并成功响应回标准响应包与事务 id 集合',
|
|
288
|
-
['"code": "OK"', '"operation": "merge.transaction"', '"transactionId"', '"retainedPointIds"', '"removedPointIds"', '"removedLineIds"', '"removedChannelIds"', 'Idempotency-Key:', 'If-Match: "v'].every(key => mergeRequestPreview.includes(key)),
|
|
289
|
-
mergeRequestPreview.slice(-320).replace(/\s+/g, ' '))
|
|
290
|
-
const confirmButton = document.querySelector('[data-demo="merge-confirm"]')
|
|
291
|
-
const cancelButton = document.querySelector('[data-demo="merge-cancel"]')
|
|
292
|
-
check('批量复制与合并:待确认面板给出 confirmPointMerge / cancelPointMerge 两个入口', Boolean(confirmButton) && Boolean(cancelButton) && confirmButton.disabled && cancelButton.disabled, `${confirmButton?.textContent.trim()} / ${cancelButton?.textContent.trim()}`)
|
|
293
|
-
setup.resetScene()
|
|
294
|
-
await wait(300)
|
|
295
|
-
const dragCanvas = document.querySelector('.gis-view__canvas')
|
|
296
|
-
const dragBox = dragCanvas.getBoundingClientRect()
|
|
297
|
-
const dragSend = (type, screen, extra = {}) => {
|
|
298
|
-
dragCanvas.dispatchEvent(new PointerEvent(type, {
|
|
216
|
+
const canvas = demo.querySelector('canvas.gis-view__canvas') ?? demo.querySelector('canvas')
|
|
217
|
+
const box = canvas.getBoundingClientRect()
|
|
218
|
+
const clickNode = async (id) => {
|
|
219
|
+
const point = view.getData().points.find(item => item.id === id)
|
|
220
|
+
const screen = view.project(point.x, point.y)
|
|
221
|
+
const init = {
|
|
299
222
|
bubbles: true,
|
|
300
223
|
cancelable: true,
|
|
301
224
|
composed: true,
|
|
302
|
-
pointerId:
|
|
225
|
+
pointerId: 7,
|
|
303
226
|
pointerType: 'mouse',
|
|
304
227
|
isPrimary: true,
|
|
305
228
|
button: 0,
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
clientY: dragBox.top + screen[1],
|
|
309
|
-
...extra
|
|
310
|
-
}))
|
|
311
|
-
}
|
|
312
|
-
const dragSourcePoint = view.getData().points.find(point => point.id === 'node-0')
|
|
313
|
-
const dragTargetPoint = view.getData().points.find(point => point.id === 'node-7')
|
|
314
|
-
const dragSource = view.project(dragSourcePoint.x, dragSourcePoint.y)
|
|
315
|
-
const dragTarget = view.project(dragTargetPoint.x, dragTargetPoint.y)
|
|
316
|
-
view.select(null)
|
|
317
|
-
dragSend('pointermove', dragSource, { buttons: 0 })
|
|
318
|
-
dragSend('pointerdown', dragSource, { buttons: 1 })
|
|
319
|
-
dragSend('pointermove', [(dragSource[0] + dragTarget[0]) / 2, (dragSource[1] + dragTarget[1]) / 2], { buttons: 1 })
|
|
320
|
-
dragSend('pointermove', dragTarget, { buttons: 1 })
|
|
321
|
-
dragSend('pointerup', dragTarget, { buttons: 0 })
|
|
322
|
-
const dragPending = await until(() => Boolean(view.getPointMerge()) && view.getPointMerge().confirmed === false, 6000)
|
|
323
|
-
check('批量复制与合并:真实拖拽相撞进入 pending,数据先不动', Boolean(dragPending) && view.getData().points.length === 14, JSON.stringify({ phase: view.getPointMerge()?.phase, confirmed: view.getPointMerge()?.confirmed, points: view.getData().points.length }))
|
|
324
|
-
const pendingMergePayload = view.getPointMerge()
|
|
325
|
-
const pendingImpact = pendingMergePayload?.impact
|
|
326
|
-
const pendingImpactReadout = text('merge-pending-readout')
|
|
327
|
-
check('批量复制与合并:pending 载荷带 impact 预演(含线路 id、改线 / 删线与通道自环影响)',
|
|
328
|
-
Boolean(pendingImpact) && pendingImpact.lineCount === pendingImpact.lineIds.length && pendingImpact.lineCount >= pendingImpact.removedLines && pendingImpact.linesChanged > 0 && pendingImpact.removedLines > 0 && pendingImpact.removedChannels >= 1 && pendingImpactReadout.includes('影响') && pendingImpactReadout.includes('改线'),
|
|
329
|
-
pendingImpact ? `线路 ${pendingImpact.lineCount}(改 ${pendingImpact.linesChanged} / 删 ${pendingImpact.removedLines})· 通道删 ${pendingImpact.removedChannels} · ${pendingImpact.lineIds.join(', ')}` : '没有 impact')
|
|
330
|
-
click('merge-impact-details')
|
|
331
|
-
await wait(80)
|
|
332
|
-
const pendingImpactDetail = text('merge-impact-detail-readout')
|
|
333
|
-
check('批量复制与合并:按 impact 的 id 调 getLine / getChannel 能列出详情(删掉的通道在确认前仍可读)',
|
|
334
|
-
Boolean(pendingImpact) &&
|
|
335
|
-
pendingImpact.lineIds.every(id => pendingImpactDetail.includes(String(id))) &&
|
|
336
|
-
pendingImpact.channelIds.every(id => pendingImpactDetail.includes(String(id))) &&
|
|
337
|
-
!pendingImpactDetail.includes('已不存在') && pendingImpactDetail.includes('回'),
|
|
338
|
-
pendingImpactDetail.slice(0, 180))
|
|
339
|
-
setup.mergeKeep = 'source'
|
|
340
|
-
setup.mergedName = '拖拽保留来源'
|
|
341
|
-
setup.mergedZone = '拖拽来源最终信息'
|
|
342
|
-
setup.mergedColor = '#00ff88'
|
|
343
|
-
setup.mergedSize = '21'
|
|
344
|
-
setup.mergedSymbol = 'square'
|
|
345
|
-
setup.mergedStrokeColor = '#222222'
|
|
346
|
-
let confirmImpact = null
|
|
347
|
-
let landedMerge = null
|
|
348
|
-
const offMergeProbe = view.on('pointMerge', payload => {
|
|
349
|
-
if (payload.phase === 'confirm') {
|
|
350
|
-
confirmImpact = payload.impact
|
|
229
|
+
clientX: box.left + screen[0],
|
|
230
|
+
clientY: box.top + screen[1]
|
|
351
231
|
}
|
|
352
|
-
|
|
353
|
-
|
|
232
|
+
canvas.dispatchEvent(new PointerEvent('pointerdown', init))
|
|
233
|
+
canvas.dispatchEvent(new PointerEvent('pointerup', init))
|
|
234
|
+
await wait(80)
|
|
235
|
+
}
|
|
236
|
+
const clickCanvasAt = async (clientX, clientY) => {
|
|
237
|
+
const init = {
|
|
238
|
+
bubbles: true,
|
|
239
|
+
cancelable: true,
|
|
240
|
+
composed: true,
|
|
241
|
+
pointerId: 8,
|
|
242
|
+
pointerType: 'mouse',
|
|
243
|
+
isPrimary: true,
|
|
244
|
+
button: 0,
|
|
245
|
+
clientX,
|
|
246
|
+
clientY
|
|
354
247
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
const autoCopies = view.getData().points.filter(point => point.zone_name === '内置复制副本').length
|
|
375
|
-
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')}`)
|
|
376
|
-
await wait(560)
|
|
377
|
-
const copyRequestPreview = text('copy-merge-backend-request')
|
|
378
|
-
check('批量复制与合并:复制假接口请求体带批量字段、版本与幂等键',
|
|
379
|
-
copyRequestPreview.includes('POST /gis/copies/') &&
|
|
380
|
-
['"points"', '"lines"', '"channels"', '"lineChannels"', '"baseVersion"', '"opId"'].every(key => copyRequestPreview.includes(key)) &&
|
|
381
|
-
text('copy-merge-backend-status').includes('420ms'),
|
|
382
|
-
copyRequestPreview.slice(0, 200))
|
|
383
|
-
check('批量复制与合并:复制成功响应回标准响应包、created 与 idMap',
|
|
384
|
-
['"code": "OK"', '"operation": "copy.', '"transactionId"', '"created"', '"counts"', '"idMap"', 'Idempotency-Key:', 'If-Match: "v'].every(key => copyRequestPreview.includes(key)),
|
|
385
|
-
copyRequestPreview.slice(-320).replace(/\s+/g, ' '))
|
|
386
|
-
const copyReplayButton = document.querySelector('[data-demo="copy-backend-replay"]')
|
|
387
|
-
const copyVersionBeforeReplay = setup.backendSnapshot?.version
|
|
388
|
-
const copyAppliedBeforeReplay = setup.backendSnapshot?.applied
|
|
389
|
-
const copyReplayReady = Boolean(copyReplayButton) && !copyReplayButton.disabled
|
|
390
|
-
copyReplayButton?.click()
|
|
391
|
-
const copyReplayHit = await until(() => text('copy-backend-readout').includes('幂等回放') && text('copy-merge-backend-request').includes('"idempotent": true'), 5000)
|
|
392
|
-
check('批量复制与合并:重放原 opId 命中幂等缓存,版本与应用数都不前进',
|
|
393
|
-
copyReplayReady && Boolean(copyReplayHit) && setup.backendSnapshot?.version === copyVersionBeforeReplay && setup.backendSnapshot?.applied === copyAppliedBeforeReplay && text('copy-merge-backend-request').includes('Idempotency-Key 命中'),
|
|
394
|
-
`v${copyVersionBeforeReplay} / applied ${copyAppliedBeforeReplay} → v${setup.backendSnapshot?.version} / applied ${setup.backendSnapshot?.applied} / ${text('copy-backend-readout')}`)
|
|
395
|
-
const backendModeSelect = document.querySelector('[data-demo="copy-merge-backend-mode"]')
|
|
396
|
-
const copyRetryButton = document.querySelector('[data-demo="copy-backend-retry"]')
|
|
397
|
-
const copyRollbackButton = document.querySelector('[data-demo="copy-backend-rollback"]')
|
|
398
|
-
backendModeSelect.value = 'network'
|
|
399
|
-
backendModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
400
|
-
await wait(60)
|
|
401
|
-
click('copy-select-sources')
|
|
402
|
-
await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
403
|
-
const pointsBeforeFailedCopy = view.getData().points.length
|
|
404
|
-
click('copy-selected')
|
|
405
|
-
const copyFailed = await until(() => !copyRetryButton.disabled && !copyRollbackButton.disabled && text('copy-backend-readout').includes('失败'), 8000)
|
|
406
|
-
const failedCopyReadout = text('copy-backend-readout')
|
|
407
|
-
check('批量复制与合并:复制网络失败后保留本地结果,重试与回滚按钮可用', Boolean(copyFailed) && view.getData().points.length === pointsBeforeFailedCopy + 4 && failedCopyReadout.includes('网络请求没有到达服务端'), `${pointsBeforeFailedCopy} → ${view.getData().points.length} / retry=${copyRetryButton.disabled} rollback=${copyRollbackButton.disabled} / ${failedCopyReadout}`)
|
|
408
|
-
copyRetryButton.click()
|
|
409
|
-
const copyRetried = await until(() => text('copy-backend-readout').includes('200 OK'), 5000)
|
|
410
|
-
check('批量复制与合并:复制失败后重试同一请求成功并推进服务端版本', Boolean(copyRetried) && !text('copy-backend-readout').includes('失败'), text('copy-backend-readout'))
|
|
411
|
-
backendModeSelect.value = 'server'
|
|
412
|
-
backendModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
413
|
-
click('copy-select-sources')
|
|
414
|
-
await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
415
|
-
const pointsBeforeRollback = view.getData().points.length
|
|
416
|
-
click('copy-selected')
|
|
417
|
-
const rollbackReady = await until(() => !copyRollbackButton.disabled && text('copy-backend-readout').includes('失败'), 8000)
|
|
418
|
-
copyRollbackButton.click()
|
|
419
|
-
const copyRolledBack = await until(() => view.getData().points.length === pointsBeforeRollback && text('copy-backend-readout').includes('回滚'), 5000)
|
|
420
|
-
check('批量复制与合并:复制失败后可回滚本地整批,服务端版本不前进', Boolean(rollbackReady) && Boolean(copyRolledBack), `${pointsBeforeRollback} → ${view.getData().points.length} / ${text('copy-backend-readout')}`)
|
|
421
|
-
const conflictVersion = setup.backendSnapshot?.version
|
|
422
|
-
const conflictApplied = setup.backendSnapshot?.applied
|
|
423
|
-
backendModeSelect.value = 'conflict'
|
|
424
|
-
backendModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
425
|
-
click('copy-select-sources')
|
|
426
|
-
await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
427
|
-
click('copy-selected')
|
|
428
|
-
const copyConflict = await until(() => !copyRetryButton.disabled && text('copy-backend-readout').includes('409'), 8000)
|
|
429
|
-
const conflictPreview = text('copy-merge-backend-request')
|
|
430
|
-
check('批量复制与合并:版本冲突回 409 响应包与 expectedVersion / currentVersion',
|
|
431
|
-
Boolean(copyConflict) && conflictPreview.includes('409 Conflict') && conflictPreview.includes('"code": "VERSION_CONFLICT"') && conflictPreview.includes(`"expectedVersion": ${conflictVersion}`) && conflictPreview.includes(`"currentVersion": ${conflictVersion + 1}`) && setup.backendSnapshot?.applied === conflictApplied,
|
|
432
|
-
`${conflictPreview.slice(-320).replace(/\s+/g, ' ')} / ${text('copy-backend-readout')}`)
|
|
433
|
-
copyRetryButton.click()
|
|
434
|
-
const copyRebased = await until(() => text('copy-backend-readout').includes('200 OK') && !text('copy-backend-readout').includes('失败'), 5000)
|
|
435
|
-
check('批量复制与合并:409 后 rebase 生成新 opId 并成功落库',
|
|
436
|
-
Boolean(copyRebased) && setup.backendSnapshot?.version === conflictVersion + 2 && setup.backendSnapshot?.applied === conflictApplied + 1,
|
|
437
|
-
`v${conflictVersion} → v${setup.backendSnapshot?.version} / applied ${conflictApplied} → ${setup.backendSnapshot?.applied} / ${text('copy-backend-readout')}`)
|
|
438
|
-
}
|
|
439
|
-
else {
|
|
440
|
-
check('批量复制与合并:画布与实例都起来了', false, ready ? '没有 setupState.view' : '没有 canvas')
|
|
441
|
-
}
|
|
442
|
-
}
|
|
248
|
+
canvas.dispatchEvent(new PointerEvent('pointerdown', init))
|
|
249
|
+
canvas.dispatchEvent(new PointerEvent('pointerup', init))
|
|
250
|
+
await wait(100)
|
|
251
|
+
}
|
|
252
|
+
const selectPair = async (first, second) => {
|
|
253
|
+
document.querySelector('[data-demo=connect-start]').click()
|
|
254
|
+
await wait(80)
|
|
255
|
+
await clickNode(first)
|
|
256
|
+
await clickNode(second)
|
|
257
|
+
return until(() => {
|
|
258
|
+
const current = view.getAddChannelDraft()
|
|
259
|
+
return current?.phase === 'ready' ? current : null
|
|
260
|
+
}, 2500)
|
|
261
|
+
}
|
|
262
|
+
const initial = view.getData()
|
|
263
|
+
check('可视化连线:初始数据没有任何通道,点与线路仍能独立渲染', channelList().length === 0 && initial.lines.length >= 4 && view.getChannels().length === 0, `${initial.points.length} 点 / ${initial.lines.length} 线 / ${channelList().length} 通道 / getChannels ${view.getChannels().length}`)
|
|
264
|
+
check('可视化连线:普通线路本身可以带折线与箭头,不依赖通道声明', initial.lines.some(line => (line.path?.length ?? 0) > 2) && initial.lines.some(line => Boolean(line.direction)), initial.lines.map(line => `${line.id}:${line.path?.length ?? 2}${line.direction ? '+arrow' : ''}`).join(' / '))
|
|
265
|
+
const draftArrow = channelDraftArrowPlacement([[0, 0], [60, 0], [60, 80]])
|
|
266
|
+
check('可视化连线:预览箭头中心按整条折线弧长取中点(不是贴在最后一段末端),与落地线路的单箭头同口径', Boolean(draftArrow) && Math.abs(draftArrow.center[0] - 60) < 1e-9 && Math.abs(draftArrow.center[1] - 10) < 1e-9 && draftArrow.unitX === 0 && draftArrow.unitY === 1, JSON.stringify(draftArrow))
|
|
443
267
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const switched = await until(() => readoutText().includes('生成场景'), 8000)
|
|
454
|
-
check('接口数据:整份切场景(updateData)后读数跟着换', Boolean(switched), readoutText())
|
|
455
|
-
const appendButton = [...demo.querySelectorAll('.dv-toolbar button')].find(node => node.textContent.trim() === '依次补通道')
|
|
456
|
-
appendButton?.click()
|
|
457
|
-
const appended = await until(() => readoutText().includes('补进'), 8000)
|
|
458
|
-
check('接口数据:addChannels 批量补通道后给出条数读数', Boolean(appended), readoutText())
|
|
459
|
-
// 2.0.1 先建实例、后拉数据:实例在 onMounted 就建好了,「模拟接口」按钮先清空 + 挂 loading,接口(900ms 后)回来再走 updateData
|
|
460
|
-
const apiButton = [...demo.querySelectorAll('.dv-toolbar button')].find(node => node.textContent.trim() === '模拟接口(延迟 900ms)')
|
|
461
|
-
check('接口数据:有「模拟接口(延迟 900ms)」按钮(演示先 createGisView 再 updateData)', Boolean(apiButton), apiButton ? '已找到' : '没找到')
|
|
462
|
-
if (apiButton) {
|
|
463
|
-
apiButton.click()
|
|
464
|
-
await wait(250)
|
|
465
|
-
const loadingHint = document.querySelector('.dv-stage .dv-hint')
|
|
466
|
-
const waiting = String(readoutText()).includes('接口请求中') && Boolean(loadingHint) && String(loadingHint.textContent).includes('正在加载')
|
|
467
|
-
const arrived = await until(() => (readoutText().includes('接口场景(延迟 900ms 后到货)') ? readoutText() : null), 8000)
|
|
468
|
-
check('接口数据:先空场景 + loading,接口返回后读数切到「接口场景」', waiting && Boolean(arrived), `${waiting ? '等待中读数与 loading 提示都在' : '没见到等待中的样子'} / ${arrived ? '到货' : '没到货'}`)
|
|
469
|
-
}
|
|
470
|
-
}
|
|
268
|
+
const cancelBefore = view.getData()
|
|
269
|
+
const cancelDraft = await selectPair('node-0', 'node-3')
|
|
270
|
+
const style = cancelDraft?.style ?? view.getOptions().channelDraftStyle
|
|
271
|
+
check('可视化连线:正交路由在草稿载荷里补出中间拐点', cancelDraft?.routing === 'orthogonal' && cancelDraft?.path?.length === 1, cancelDraft ? `${cancelDraft.routing} / path ${cancelDraft.path?.length ?? 0}` : '没有 ready 草稿')
|
|
272
|
+
check('可视化连线:预览样式开放了颜色 / 线宽 / 箭头 / 箭头大小', style?.arrow === true && Number(style?.arrowSize) === 12 && Number(style?.width) > 0 && Boolean(style?.color), JSON.stringify(style))
|
|
273
|
+
document.querySelector('[data-demo=connect-cancel]').click()
|
|
274
|
+
await wait(80)
|
|
275
|
+
const cancelAfter = view.getData()
|
|
276
|
+
check('可视化连线:取消添加只收草稿,线路与通道数量都不动', view.getAddChannelDraft() === null && cancelAfter.lines.length === cancelBefore.lines.length && channelList().length === 0, `${cancelBefore.lines.length} -> ${cancelAfter.lines.length} 线 / ${channelList().length} 通道`)
|
|
471
277
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const readoutText = () => [...demo.querySelectorAll('.dv-readout')].map(node => node.textContent.trim()).join(' | ')
|
|
480
|
-
const countWaypoints = () => (view() ? view().getWaypoints().length : 0)
|
|
481
|
-
const countDecorations = () => (view() ? view().getData().lines.reduce((total, line) => total + (Array.isArray(line.decorations) ? line.decorations.length : 0), 0) : 0)
|
|
482
|
-
const domLabels = await until(() => document.querySelectorAll('.dv-demo .gis-view__label').length > 0, 8000)
|
|
483
|
-
const labelsSample = { waypoints: countWaypoints(), decorations: countDecorations() }
|
|
484
|
-
// 与文件对账:没动过数据时导出结果应该与文件逐字节一致
|
|
485
|
-
const compareButton = [...demo.querySelectorAll('.dv-button')].find(node => node.textContent.trim() === '与文件对一遍')
|
|
486
|
-
compareButton?.click()
|
|
487
|
-
const matched = await until(() => readoutText().includes('逐字节一致'), 5000)
|
|
488
|
-
// 换到「通道与分组」再按导出重建:通道数 / 拐点数都不丢
|
|
489
|
-
const channelChip = [...demo.querySelectorAll('.dv-chip')].find(node => node.textContent.trim() === '通道与分组')
|
|
490
|
-
channelChip?.click()
|
|
491
|
-
const switched = await until(() => (view()?.getOptions().channels.length ?? 0) === 3, 8000)
|
|
492
|
-
const waypointsBefore = countWaypoints()
|
|
493
|
-
const rebuildButton = [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.trim() === '按导出重建')
|
|
494
|
-
rebuildButton?.click()
|
|
495
|
-
const rebuilt = await until(() => readoutText().includes('已用导出的 input 口径重建'), 8000)
|
|
496
|
-
sampleResult = {
|
|
497
|
-
ready: Boolean(ready),
|
|
498
|
-
domLabels: Boolean(domLabels),
|
|
499
|
-
labels: document.querySelectorAll('.dv-demo .gis-view__label').length,
|
|
500
|
-
labelsSample,
|
|
501
|
-
matched: Boolean(matched),
|
|
502
|
-
switched: Boolean(switched),
|
|
503
|
-
rebuilt: Boolean(rebuilt),
|
|
504
|
-
channels: view()?.getOptions().channels.length ?? 0,
|
|
505
|
-
waypointsBefore,
|
|
506
|
-
waypointsAfter: countWaypoints()
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
check('完整样本:拐点 / 标记 / 标签都画到了画布上', Boolean(sampleResult?.ready && sampleResult?.domLabels) && sampleResult.labelsSample.waypoints >= 4 && sampleResult.labelsSample.decorations >= 3, sampleResult ? `拐点 ${sampleResult.labelsSample.waypoints} / 标记 ${sampleResult.labelsSample.decorations} / 标签 ${sampleResult.labels} 个` : '没切到完整样本')
|
|
510
|
-
check('完整样本:导出的 input 口径与文件逐字节一致', Boolean(sampleResult?.matched), sampleResult?.matched ? '读数里出现「与文件逐字节一致」' : '没有对上')
|
|
511
|
-
check('完整样本:换样本 + 按导出重建之后通道与拐点一个不少', Boolean(sampleResult?.switched && sampleResult?.rebuilt) && sampleResult.channels === 3 && sampleResult.waypointsAfter >= 17, sampleResult ? `通道 ${sampleResult.channels} / 拐点 ${sampleResult.waypointsBefore} -> ${sampleResult.waypointsAfter}` : '没有结果')
|
|
512
|
-
// 2.0b 全字段齐全:一份里同时有多拐点 / 多标签 / 多分组,逐项数出来
|
|
513
|
-
const fullDemo = document.querySelector('.dv-demo')
|
|
514
|
-
const fullView = () => fullDemo?.__vueParentComponent?.setupState?.view ?? null
|
|
515
|
-
const fullCountDecorations = () => (fullView() ? fullView().getData().lines.reduce((total, line) => total + (Array.isArray(line.decorations) ? line.decorations.length : 0), 0) : 0)
|
|
516
|
-
const fullChip = [...fullDemo.querySelectorAll('.dv-chip')].find(node => node.textContent.trim() === '全字段齐全')
|
|
517
|
-
fullChip?.click()
|
|
518
|
-
const fullReady = await until(() => (fullView()?.getData().lines.length ?? 0) === 22, 8000)
|
|
519
|
-
const fullLabels = await until(() => document.querySelectorAll('.dv-demo .gis-view__label').length >= 20, 8000)
|
|
520
|
-
const fullWaypoints = fullView() ? fullView().getWaypoints().length : 0
|
|
521
|
-
const fullDecorations = fullCountDecorations()
|
|
522
|
-
check('完整样本:全字段样本把多拐点 / 多标签 / 多分组摆齐', Boolean(fullReady) && Boolean(fullLabels) && fullWaypoints >= 30 && fullDecorations === 23, `拐点 ${fullWaypoints} / 标记 ${fullDecorations} / 标签 ${document.querySelectorAll('.dv-demo .gis-view__label').length} 个`)
|
|
278
|
+
const beforeDefault = view.getData()
|
|
279
|
+
const defaultDraft = await selectPair('node-4', 'node-7')
|
|
280
|
+
document.querySelector('[data-demo=connect-default]').click()
|
|
281
|
+
await wait(120)
|
|
282
|
+
const afterDefault = view.getData()
|
|
283
|
+
const defaultLine = afterDefault.lines.at(-1)
|
|
284
|
+
check('可视化连线:默认入口走 commitAddChannel,独立线路落完整 path', Boolean(defaultDraft) && afterDefault.lines.length === beforeDefault.lines.length + 1 && channelList().length === 0 && (defaultLine?.path?.length ?? 0) > 2, `${beforeDefault.lines.length} -> ${afterDefault.lines.length} 线 / path ${defaultLine?.path?.length ?? 0} / ${text('connect-action-readout')}`)
|
|
523
285
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
let cancelResult = null
|
|
532
|
-
let strayResult = null
|
|
533
|
-
let ghostHidden = false
|
|
534
|
-
let ghostNode = null
|
|
535
|
-
if (dragReady && paletteItems.length === 7) {
|
|
536
|
-
const dragView = document.querySelector('.dv-demo').__vueParentComponent.setupState.view
|
|
537
|
-
const pointsBefore = dragView.getData().points.length
|
|
538
|
-
const canvasNode = document.querySelector('.dv-demo .dv-canvas')
|
|
539
|
-
const canvasBox = canvasNode.getBoundingClientRect()
|
|
540
|
-
const transfer = new DataTransfer()
|
|
541
|
-
// 拖影:看板按钮默认会把整个按钮当拖影(跟着指针的那个「变电站」框)。这里盯着 setDragImage 的入参,
|
|
542
|
-
// 确认换成了 1×1 的透明元素 —— 屏幕上只剩地图上那个点
|
|
543
|
-
let ghostCall = null
|
|
544
|
-
const setDragImage = transfer.setDragImage
|
|
545
|
-
transfer.setDragImage = (node, x, y) => {
|
|
546
|
-
ghostCall = { node, x, y }
|
|
547
|
-
return setDragImage?.call(transfer, node, x, y)
|
|
548
|
-
}
|
|
549
|
-
const dragItem = (index, x, y) => {
|
|
550
|
-
paletteItems[index].dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: transfer }))
|
|
551
|
-
const init = { bubbles: true, cancelable: true, dataTransfer: transfer, clientX: x, clientY: y }
|
|
552
|
-
canvasNode.dispatchEvent(new DragEvent('dragover', init))
|
|
553
|
-
return init
|
|
554
|
-
}
|
|
555
|
-
// 第 1 项是 230kV 档:规则给 9px 的圆
|
|
556
|
-
const dropInit = {
|
|
557
|
-
bubbles: true,
|
|
558
|
-
cancelable: true,
|
|
559
|
-
dataTransfer: transfer,
|
|
560
|
-
clientX: canvasBox.left + canvasBox.width * 0.3,
|
|
561
|
-
clientY: canvasBox.top + canvasBox.height * 0.35
|
|
562
|
-
}
|
|
563
|
-
dragItem(0, dropInit.clientX, dropInit.clientY)
|
|
564
|
-
const previewShown = await until(() => dragView.getDropPreview(), 4000)
|
|
565
|
-
const previewSize = previewShown?.size ?? null
|
|
566
|
-
const previewSymbol = previewShown?.symbol ?? null
|
|
567
|
-
ghostNode = document.querySelector('.dv-drag-ghost')
|
|
568
|
-
ghostHidden = Boolean(ghostCall?.node) && ghostCall.node === ghostNode
|
|
569
|
-
&& ghostCall.node.getBoundingClientRect().width <= 2
|
|
570
|
-
&& getComputedStyle(ghostCall.node).opacity === '0'
|
|
571
|
-
const readoutPreviewed = await until(() => [...document.querySelectorAll('.dv-demo .dv-readout')].some(node => node.textContent.includes('预览图标')), 4000)
|
|
572
|
-
canvasNode.dispatchEvent(new DragEvent('drop', dropInit))
|
|
573
|
-
const draftShown = await until(() => dragView.getDropPreview(), 4000)
|
|
574
|
-
const untouched = dragView.getData().points.length === pointsBefore
|
|
575
|
-
// 真浏览器在 drop 之后还会给源元素补一条 dragend(拖拽演示里就是看板按钮上绑的那一个)。
|
|
576
|
-
// 这一条要是顺手把预览收掉,「松手先显示」就只剩一眨眼 —— 这里照着补发一条,核对点还在落点上
|
|
577
|
-
const draftWorld = draftShown?.world ?? null
|
|
578
|
-
paletteItems[0].dispatchEvent(new DragEvent('dragend', { bubbles: true, cancelable: true, dataTransfer: transfer }))
|
|
579
|
-
await wait(200)
|
|
580
|
-
const afterDragEnd = dragView.getDropPreview()
|
|
581
|
-
const keptAfterDragEnd = Boolean(afterDragEnd) && Boolean(draftWorld)
|
|
582
|
-
&& Math.abs(afterDragEnd.world[0] - draftWorld[0]) < 1e-6 && Math.abs(afterDragEnd.world[1] - draftWorld[1]) < 1e-6
|
|
583
|
-
// 草稿还挂着的时候又拖一项进来(dragover 会先按新的一项画预览),中途松手没落上:
|
|
584
|
-
// 临时预览收掉、点回到草稿自己的落点上,数据依旧一个字节没动
|
|
585
|
-
const strayInit = { ...dropInit, clientX: dropInit.clientX + 140, clientY: dropInit.clientY + 80 }
|
|
586
|
-
dragItem(4, strayInit.clientX, strayInit.clientY)
|
|
587
|
-
// 预览合并到下一帧才算:等它真的挪到新落点上(还留着草稿的那一份就直接算没挪)
|
|
588
|
-
const strayShown = await until(() => {
|
|
589
|
-
const shown = dragView.getDropPreview()
|
|
590
|
-
return shown && draftWorld && (Math.abs(shown.world[0] - draftWorld[0]) > 1e-6 || Math.abs(shown.world[1] - draftWorld[1]) > 1e-6) ? shown : null
|
|
591
|
-
}, 4000)
|
|
592
|
-
const strayMoved = Boolean(strayShown)
|
|
593
|
-
paletteItems[4].dispatchEvent(new DragEvent('dragend', { bubbles: true, cancelable: true, dataTransfer: transfer }))
|
|
594
|
-
await wait(200)
|
|
595
|
-
const restored = dragView.getDropPreview()
|
|
596
|
-
strayResult = {
|
|
597
|
-
moved: strayMoved,
|
|
598
|
-
restored: Boolean(restored) && Boolean(draftWorld)
|
|
599
|
-
&& Math.abs(restored.world[0] - draftWorld[0]) < 1e-6 && Math.abs(restored.world[1] - draftWorld[1]) < 1e-6,
|
|
600
|
-
untouched: dragView.getData().points.length === pointsBefore
|
|
601
|
-
}
|
|
602
|
-
// 面板上指定属性:改成 525kV(规则给 13px),预览与读数必须跟着重画
|
|
603
|
-
const kvSelect = [...document.querySelectorAll('.dv-demo select')].find(node => node.textContent.includes('525 kV'))
|
|
604
|
-
if (kvSelect) {
|
|
605
|
-
kvSelect.value = '525'
|
|
606
|
-
kvSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
607
|
-
}
|
|
608
|
-
const resized = await until(() => (dragView.getDropPreview()?.size ?? 0) === 13, 4000)
|
|
609
|
-
const specifiedSize = dragView.getDropPreview()?.size ?? null
|
|
610
|
-
check('拖拽落点:dragover 期间组件就解析出落点图标(尺寸 / 符号 / 颜色都有读数)', Boolean(previewShown) && Number(previewSize) > 0 && Boolean(previewSymbol), previewShown ? `${previewSize}px / ${previewSymbol} / ${previewShown.color}` : '没有落点预览')
|
|
611
|
-
check('拖拽落点:预览读数写进面板(不是照着样子仿的)', Boolean(readoutPreviewed), readoutPreviewed ? '读数里出现「预览图标」' : '读数里没有预览尺寸')
|
|
612
|
-
document.querySelector('.dv-demo .dv-confirm .dv-button--primary')?.click()
|
|
613
|
-
const landed = await until(() => dragView.getData().points.length === pointsBefore + 1, 4000)
|
|
614
|
-
const landedItem = landed ? dragView.getData().points.at(-1) : null
|
|
615
|
-
// 落地后的那一份:同一条规则链解析出来的尺寸与符号必须与预览一致,属性就是面板上指定的那一个
|
|
616
|
-
const landedStyle = landed ? dragView.getPointStyle(pointsBefore) : null
|
|
617
|
-
const sameAsPreview = Boolean(landedStyle) && landedStyle.size === specifiedSize && previewSymbol === 'circle' && landedItem?.symbol === 'circle'
|
|
618
|
-
const specified = Boolean(kvSelect) && Number(landedItem?.vn_kv) === 525 && Boolean(resized)
|
|
619
|
-
const cleared = landed ? dragView.getDropPreview() === null : false
|
|
620
|
-
dragResult = { before: pointsBefore, draft: Boolean(draftShown), untouched, keptAfterDragEnd, landed: Boolean(landed), sameAsPreview, specified, size: specifiedSize, cleared, after: dragView.getData().points.length }
|
|
621
|
-
// 第二轮:拖一个进去之后点「取消新增」—— 地图上的点收掉,数据一个字节没动
|
|
622
|
-
const cancelBefore = dragView.getData().points.length
|
|
623
|
-
const cancelX = dropInit.clientX + 24
|
|
624
|
-
const cancelY = dropInit.clientY + 24
|
|
625
|
-
dragItem(1, cancelX, cancelY)
|
|
626
|
-
await until(() => dragView.getDropPreview(), 4000)
|
|
627
|
-
canvasNode.dispatchEvent(new DragEvent('drop', { ...dropInit, clientX: cancelX, clientY: cancelY }))
|
|
628
|
-
// 这一段同样要过一遍 dragend:取消新增之前,点必须一直留在地图上
|
|
629
|
-
paletteItems[1].dispatchEvent(new DragEvent('dragend', { bubbles: true, cancelable: true, dataTransfer: transfer }))
|
|
630
|
-
const cancelShown = await until(() => dragView.getDropPreview(), 4000)
|
|
631
|
-
const cancelButton = [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.trim() === '取消新增')
|
|
632
|
-
cancelButton?.click()
|
|
633
|
-
await wait(200)
|
|
634
|
-
cancelResult = { shown: Boolean(cancelShown), button: Boolean(cancelButton), cleared: dragView.getDropPreview() === null, untouched: dragView.getData().points.length === cancelBefore }
|
|
635
|
-
}
|
|
636
|
-
check('拖拽落点:拖拽期间不出现浏览器默认的看板拖影(只剩地图上的点)', Boolean(ghostNode) && ghostHidden, ghostHidden ? 'setDragImage 指向 1×1 透明节点' : '拖影还是看板按钮本身')
|
|
637
|
-
check('拖拽落点:松手之后预览不收起,先显示在地图上、数据一个字节没动', Boolean(dragResult?.draft) && Boolean(dragResult?.untouched), dragResult ? `点 ${dragResult.before} -> ${dragResult.after}` : `看板 ${paletteItems.length} 项 / 画布 ${Boolean(dragReady)}`)
|
|
638
|
-
check('拖拽落点:drop 之后那条 dragend 不会把原来拖动的点收掉(点一直留在落点上)', Boolean(dragResult?.keptAfterDragEnd), dragResult?.keptAfterDragEnd ? 'dragend 之后点还在,世界坐标没挪' : 'dragend 之后预览被收掉了')
|
|
639
|
-
check('拖拽落点:草稿挂着时又拖一项进来、中途松手 —— 点回到落点,数据不动', Boolean(strayResult?.moved) && Boolean(strayResult?.restored) && Boolean(strayResult?.untouched), `临时预览 ${strayResult?.moved ? '挪走了' : '没挪'} / 回到落点 ${strayResult?.restored ? '是' : '否'} / 数据 ${strayResult?.untouched ? '没动' : '动了'}`)
|
|
640
|
-
check('拖拽落点:面板指定属性(525kV)之后预览跟着重画', Boolean(dragResult?.size) && dragResult.size === 13, dragResult ? `指定之后 ${dragResult.size}px` : '没有拖放结果')
|
|
641
|
-
check('拖拽落点:点「按属性新增」才写进数据,写进去的就是指定的那一份属性', Boolean(dragResult?.landed) && Boolean(dragResult?.specified), dragResult ? `确认后 ${dragResult.after} 个节点 / vn_kv=525` : '没有拖放结果')
|
|
642
|
-
check('拖拽落点:落地后的图标尺寸 / 符号与预览完全一致(规则链同一条)', Boolean(dragResult?.sameAsPreview), dragResult ? `预览与落地 ${dragResult.sameAsPreview ? '一致' : '不一致'}` : '没有拖放结果')
|
|
643
|
-
check('拖拽落点:新增之后落点预览收掉,不会留在画面上', Boolean(dragResult?.cleared), dragResult?.cleared ? 'getDropPreview() 回到 null' : '预览还在')
|
|
644
|
-
check('拖拽落点:「取消新增」把点从地图上收掉、数据不动', Boolean(cancelResult?.shown) && Boolean(cancelResult?.button) && Boolean(cancelResult?.cleared) && Boolean(cancelResult?.untouched), cancelResult ? `取消按钮 ${cancelResult.button ? '有' : '没有'} / 预览 ${cancelResult.cleared ? '已收' : '还在'}` : '没有第二轮结果')
|
|
286
|
+
const beforeLines = view.getData()
|
|
287
|
+
const lineDraft = await selectPair('node-2', 'node-8')
|
|
288
|
+
document.querySelector('[data-demo=connect-add-lines]').click()
|
|
289
|
+
await wait(120)
|
|
290
|
+
const afterLines = view.getData()
|
|
291
|
+
const addedLine = afterLines.lines.at(-1)
|
|
292
|
+
check('可视化连线:addLines 入口真实追加普通线路,且不新增通道', Boolean(lineDraft) && afterLines.lines.length === beforeLines.lines.length + 1 && channelList().length === 0 && (addedLine?.path?.length ?? 0) > 2 && Boolean(addedLine?.direction), `${beforeLines.lines.length} -> ${afterLines.lines.length} 线 / path ${addedLine?.path?.length ?? 0} / ${text('connect-action-readout')}`)
|
|
645
293
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
294
|
+
const kindSelect = document.querySelector('[data-demo=connect-kind]')
|
|
295
|
+
kindSelect.value = 'channel-3'
|
|
296
|
+
kindSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
297
|
+
const arrowPositionSelect = document.querySelector('[data-demo=connect-arrow-positions]')
|
|
298
|
+
arrowPositionSelect.value = '0.25,0.75'
|
|
299
|
+
arrowPositionSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
300
|
+
const spacingInput = document.querySelector('[data-demo=connect-spacing]')
|
|
301
|
+
spacingInput.value = '26'
|
|
302
|
+
spacingInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
303
|
+
await wait(100)
|
|
304
|
+
const beforeBlankCancel = view.getData()
|
|
305
|
+
document.querySelector('[data-demo=connect-start]').click()
|
|
306
|
+
await wait(80)
|
|
307
|
+
await clickNode('node-0')
|
|
308
|
+
const stagedDraft = await until(() => {
|
|
309
|
+
const current = view.getAddChannelDraft()
|
|
310
|
+
return current?.phase === 'from' ? current : null
|
|
311
|
+
}, 2500)
|
|
312
|
+
check('可视化连线:3 回通道草稿把箭头位置与回线间距直接带进本次样式', stagedDraft?.kind === 'channel' && stagedDraft?.circuits === 3 && stagedDraft?.style?.arrowPositions?.join(',') === '0.25,0.75' && stagedDraft?.style?.circuitSpacing === 26, stagedDraft ? JSON.stringify({ kind: stagedDraft.kind, circuits: stagedDraft.circuits, arrows: stagedDraft.style?.arrowPositions, spacing: stagedDraft.style?.circuitSpacing }) : '没有 from 草稿')
|
|
313
|
+
await clickCanvasAt(box.left + 18, box.top + box.height - 18)
|
|
314
|
+
const blankCancelled = await until(() => view.getAddChannelDraft() === null, 2500)
|
|
315
|
+
const afterBlankCancel = view.getData()
|
|
316
|
+
check('可视化连线:选好起点后点空白立即取消,readout 报 click-outside-node 且数据不增加', Boolean(blankCancelled) && text('connect-cancel-readout').includes('click-outside-node') && afterBlankCancel.lines.length === beforeBlankCancel.lines.length && afterBlankCancel.points.length === beforeBlankCancel.points.length && channelList().length === 0, `${text('connect-cancel-readout')} / ${beforeBlankCancel.lines.length} -> ${afterBlankCancel.lines.length} 线`)
|
|
317
|
+
|
|
318
|
+
// 恢复独立线路配置,继续验证后面的真实落地入口
|
|
319
|
+
kindSelect.value = 'line'
|
|
320
|
+
kindSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
321
|
+
arrowPositionSelect.value = '0.5'
|
|
322
|
+
arrowPositionSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
323
|
+
spacingInput.value = '12'
|
|
324
|
+
spacingInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
325
|
+
await wait(100)
|
|
326
|
+
kindSelect.value = 'channel-2'
|
|
327
|
+
kindSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
328
|
+
await wait(80)
|
|
329
|
+
const beforeChannels = view.getData()
|
|
330
|
+
const channelDraft = await selectPair('node-1', 'node-5')
|
|
331
|
+
document.querySelector('[data-demo=connect-add-channels]').click()
|
|
332
|
+
await wait(160)
|
|
333
|
+
const afterChannels = view.getData()
|
|
334
|
+
const addedChannel = channelList().at(-1)
|
|
335
|
+
check('可视化连线:addChannels 入口真实新建通道并按 circuits 加回线', Boolean(channelDraft) && channelList().length === 1 && afterChannels.lines.length === beforeChannels.lines.length + 2 && addedChannel?.path?.length === 1, `${channelList().length} 通道 / ${beforeChannels.lines.length} -> ${afterChannels.lines.length} 线 / path ${addedChannel?.path?.length ?? 0} / ${text('connect-action-readout')}`)
|
|
336
|
+
check('可视化连线:读数面板同步报出纯点 + 线 / 通道数量', text('connect-data-readout').includes('节点') && text('connect-data-readout').includes('通道'), text('connect-data-readout'))
|
|
672
337
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
await wait(260)
|
|
676
|
-
document.querySelector('.dv-demo .dv-confirm .dv-button--primary')?.click()
|
|
677
|
-
await wait(320)
|
|
678
|
-
const countsBefore = tweakView.getData().points.length
|
|
679
|
-
const labelsBefore = tweakView.getLabelStates().length
|
|
680
|
-
panelButton('存档 → 重建')?.click()
|
|
681
|
-
await wait(900)
|
|
682
|
-
const rebuiltView = document.querySelector('.dv-demo').__vueParentComponent.setupState.view
|
|
683
|
-
tweakResult = {
|
|
684
|
-
sizeBefore,
|
|
685
|
-
sizeAfter,
|
|
686
|
-
symbol: typeof symbol === 'string' ? symbol : symbol?.type,
|
|
687
|
-
countsBefore,
|
|
688
|
-
countsAfter: rebuiltView.getData().points.length,
|
|
689
|
-
labelStates: rebuiltView.getLabelStates().length,
|
|
690
|
-
labelsBefore
|
|
338
|
+
else {
|
|
339
|
+
check('可视化连线:画布与实例都起来了', false, ready ? '没有 setupState.view' : '没有 canvas')
|
|
691
340
|
}
|
|
692
341
|
}
|
|
693
|
-
check('拖拽落点:微调按钮走 setPointSize / setPointSymbol 写回数据', Boolean(tweakResult) && tweakResult.sizeAfter === tweakResult.sizeBefore + 4 && tweakResult.symbol === 'square', tweakResult ? `size ${tweakResult.sizeBefore} -> ${tweakResult.sizeAfter} / symbol ${tweakResult.symbol}` : '没有可微调的目标')
|
|
694
|
-
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})` : '没有重建读数')
|
|
695
|
-
|
|
696
|
-
// 3. 回到标签演示,逐条验这一轮新增的交互
|
|
697
|
-
await clickTab('标签交互')
|
|
698
|
-
await canvasReady()
|
|
699
|
-
await until(() => document.querySelectorAll('.dv-demo .gis-view__label').length > 0, 15000)
|
|
700
|
-
const labelNodes = () => [...document.querySelectorAll('.dv-demo .gis-view__label')]
|
|
701
|
-
check('标签演示:节点标签都画出来了', labelNodes().length > 0, `${labelNodes().length} 个标签节点`)
|
|
702
|
-
check('标签节点上是可点可拖的那一类(draggable / editable)', labelNodes().every(node => node.classList.contains('gis-view__label--draggable')), labelNodes()[0]?.className ?? '')
|
|
703
|
-
// 3.0 一键换标签内容:setLabelContent 只换全局 label / lineLabel 的 content(节点 + 线路一起)
|
|
704
|
-
const labelView = () => document.querySelector('.dv-demo')?.__vueParentComponent?.setupState?.view ?? null
|
|
705
|
-
const overlapHideButton = document.querySelector('[data-demo="label-overlap-hide"]')
|
|
706
|
-
overlapHideButton?.click()
|
|
707
|
-
await wait(220)
|
|
708
|
-
const hideOverlapOptions = labelView()?.getOptions()
|
|
709
|
-
check('标签演示:「碰撞直接隐藏」把节点 / 线路标签一起切到 overlapMode: hide',
|
|
710
|
-
hideOverlapOptions?.label?.overlapMode === 'hide' && hideOverlapOptions?.lineLabel?.overlapMode === 'hide' &&
|
|
711
|
-
overlapHideButton?.classList.contains('dv-chip--active') === true,
|
|
712
|
-
`${hideOverlapOptions?.label?.overlapMode} / ${hideOverlapOptions?.lineLabel?.overlapMode}`)
|
|
713
|
-
const overlapShiftButton = document.querySelector('[data-demo="label-overlap-shift"]')
|
|
714
|
-
overlapShiftButton?.click()
|
|
715
|
-
await wait(220)
|
|
716
|
-
const shiftOverlapOptions = labelView()?.getOptions()
|
|
717
|
-
check('标签演示:「主动挪标签」切回 overlapMode: shift(默认避让策略)',
|
|
718
|
-
shiftOverlapOptions?.label?.overlapMode === 'shift' && shiftOverlapOptions?.lineLabel?.overlapMode === 'shift' &&
|
|
719
|
-
overlapShiftButton?.classList.contains('dv-chip--active') === true,
|
|
720
|
-
`${shiftOverlapOptions?.label?.overlapMode} / ${shiftOverlapOptions?.lineLabel?.overlapMode}`)
|
|
721
|
-
const contentSelect = document.querySelector('[data-demo=label-content-field]')
|
|
722
|
-
contentSelect.value = '{vn_kv} kV'
|
|
723
|
-
contentSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
724
|
-
await wait(500)
|
|
725
|
-
const optionsAfter = labelView()?.getOptions() ?? null
|
|
726
|
-
const voltageShown = [...document.querySelectorAll('.dv-demo .gis-view__label')].some(node => node.textContent.includes('kV'))
|
|
727
|
-
const contentReadout = document.querySelector('[data-demo=label-content-text]')?.textContent.trim() ?? ''
|
|
728
|
-
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`)
|
|
729
|
-
contentSelect.value = '{name}'
|
|
730
|
-
contentSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
731
|
-
await wait(300)
|
|
732
|
-
|
|
733
|
-
// 3.1 点一下标签 → labelClick
|
|
734
|
-
const target = labelNodes()[0]
|
|
735
|
-
tap(target)
|
|
736
|
-
const clickLogged = await until(() => logText().includes('labelClick'), 8000)
|
|
737
|
-
check('点一下标签发 labelClick,日志记下「哪一条、内容是什么」', Boolean(clickLogged), logText().slice(0, 160))
|
|
738
|
-
const editReadout = document.querySelector('.dv-demo .dv-input')?.closest('.dv-group')?.querySelector('.dv-readout')?.textContent?.trim() ?? ''
|
|
739
|
-
check('点完之后编辑面板认到了这一条', !editReadout.includes('点一下标签') && editReadout.length > 0, editReadout)
|
|
740
342
|
|
|
741
|
-
//
|
|
742
|
-
|
|
743
|
-
const menuEvent = contextMenuOn(target, rect.left + 4, rect.top + 4)
|
|
744
|
-
check('右键标签的系统菜单被拦下(preventContextMenu 缺省开)', menuEvent.defaultPrevented, `defaultPrevented=${menuEvent.defaultPrevented}`)
|
|
745
|
-
const menu = await until(() => document.querySelector('.dv-menu'), 6000)
|
|
746
|
-
check('右键弹出演示自己的菜单', Boolean(menu), menu ? [...menu.querySelectorAll('button')].map(node => node.textContent.trim()).join(' / ') : '没弹出')
|
|
747
|
-
const items = menu ? [...menu.querySelectorAll('button')].map(node => node.textContent.trim()) : []
|
|
748
|
-
check('菜单里有改内容 / 换位置 / 重置偏移 / 隐藏 / 删除这五项', ['改内容', '换位置', '重置偏移', '删除这一条'].every(text => items.some(item => item.includes(text))), items.join(' / '))
|
|
749
|
-
const positionButton = menu ? [...menu.querySelectorAll('button')].find(node => node.textContent.includes('换位置')) : null
|
|
750
|
-
positionButton?.click()
|
|
751
|
-
await wait(260)
|
|
752
|
-
check('菜单「换位置」落到 updateLabel 上(日志看得到)', logText().includes('updateLabel'), logText().slice(0, 160))
|
|
753
|
-
check('点完菜单项之后菜单收起来了', !document.querySelector('.dv-menu'), document.querySelector('.dv-menu') ? '还开着' : '已收')
|
|
754
|
-
// 3.3 删除一条:自绘菜单的删除落到 removeLabel / updateLabel(hidden)
|
|
755
|
-
const beforeDelete = labelNodes()
|
|
756
|
-
const labelForDelete = beforeDelete[beforeDelete.length - 1]
|
|
757
|
-
// 按内容认「删的是哪一条」:避让是「没位置就不画」,删掉一条腾出来的小格会让别的候选补位,
|
|
758
|
-
// 所以这里盯的是「被删的那一条不再画」,不是「总数不涨」(13 → 14 是正常的补位)
|
|
759
|
-
const deletedLabelText = labelForDelete.textContent.trim()
|
|
760
|
-
contextMenuOn(labelForDelete, labelForDelete.getBoundingClientRect().left + 4, labelForDelete.getBoundingClientRect().top + 4)
|
|
761
|
-
const deleteMenu = await until(() => document.querySelector('.dv-menu'), 6000)
|
|
762
|
-
const deleteButton = deleteMenu ? [...deleteMenu.querySelectorAll('button')].find(node => node.textContent.includes('删除这一条')) : null
|
|
763
|
-
deleteButton?.click()
|
|
764
|
-
await wait(400)
|
|
765
|
-
check('菜单「删除这一条」记到了日志里', logText().includes('removeLabel') || logText().includes('updateLabel'), logText().slice(0, 160))
|
|
766
|
-
const remainingLabels = labelNodes()
|
|
767
|
-
check('删完之后被删的那一条不再画(腾出的位置允许别的标签补位)', deletedLabelText !== '' && !remainingLabels.some(node => node.textContent.trim() === deletedLabelText), `${deletedLabelText}|${beforeDelete.length} → ${remainingLabels.length}`)
|
|
768
|
-
|
|
769
|
-
// 3.4 批量 API:给全部节点加一条
|
|
770
|
-
const addAllButton = [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.includes('给全部加一条'))
|
|
771
|
-
addAllButton?.click()
|
|
772
|
-
await wait(500)
|
|
773
|
-
check('「给全部加一条」走 addLabels,日志里有新增计数', logText().includes('addLabels'), logText().slice(0, 160))
|
|
774
|
-
check('labelChange 也跟着发(addAll)', logText().includes('addAll'), logText().slice(0, 200))
|
|
775
|
-
|
|
776
|
-
// 3.5 点选即加:先选中第一个节点,再让「给选中加一条」把它加上
|
|
777
|
-
const selectButton = [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.includes('选中第一个节点'))
|
|
778
|
-
selectButton?.click()
|
|
779
|
-
await wait(400)
|
|
780
|
-
const selectionReadout = [...document.querySelectorAll('.dv-demo .dv-readout')].map(node => node.textContent.trim()).find(text => text.includes('选中'))
|
|
781
|
-
check('「选中第一个节点」让面板报出选中集', Boolean(selectionReadout) && !selectionReadout.includes('未选中'), selectionReadout ?? '没有选中读数')
|
|
782
|
-
const preview = [...document.querySelectorAll('.dv-demo .dv-hint')].map(node => node.textContent.trim()).find(text => text.includes('本次会加到'))
|
|
783
|
-
check('选中集预览用 resolveLabelTargets 算出「会加到几条」', Boolean(preview), preview ?? '没有预览读数')
|
|
784
|
-
const addSelectionButton = [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.includes('给选中加一条'))
|
|
785
|
-
check('「给选中加一条」选中集非空时可用', Boolean(addSelectionButton) && !addSelectionButton.disabled, addSelectionButton ? `disabled=${addSelectionButton.disabled}` : '没找到按钮')
|
|
786
|
-
addSelectionButton?.click()
|
|
787
|
-
await wait(500)
|
|
788
|
-
check('给选中集加标签走 addLabels(..., \'selection\', ...)', logText().includes('选中集:新增') || logText().includes('addLabels'), logText().slice(0, 200))
|
|
789
|
-
// 3.5.1 连点两次:加出来的标签默认要跟别的标签避让,压在一起就整条不画 ——
|
|
790
|
-
// 面板上看着像「只有第一次加上了」,这里盯住「每一次都多画一条」
|
|
791
|
-
const noteCount = () => [...document.querySelectorAll('.dv-demo .gis-view__label')].filter(node => node.textContent.includes('备注#')).length
|
|
792
|
-
const notesBefore = noteCount()
|
|
793
|
-
addSelectionButton?.click()
|
|
794
|
-
await wait(400)
|
|
795
|
-
addSelectionButton?.click()
|
|
796
|
-
await wait(400)
|
|
797
|
-
const notesAfter = noteCount()
|
|
798
|
-
check('连点「给选中加一条」每次都能多画出一条(不只第一次有效)', notesAfter >= notesBefore + 2, `备注标签 ${notesBefore} → ${notesAfter}`)
|
|
799
|
-
|
|
800
|
-
// 3.6 线路标签 + 整条通道:切到线路标签再加
|
|
801
|
-
const lineTab = [...document.querySelectorAll('.dv-demo .dv-chip')].find(node => node.textContent.includes('线路标签'))
|
|
802
|
-
lineTab?.click()
|
|
803
|
-
await wait(200)
|
|
804
|
-
const channelButton = [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.includes('整条通道加'))
|
|
805
|
-
channelButton?.click()
|
|
806
|
-
await wait(500)
|
|
807
|
-
check('「整条通道加」用 { channel } 一次给整条通道的回线加标签', logText().includes('通道'), logText().slice(0, 200))
|
|
808
|
-
|
|
809
|
-
// 3.7 双击标签:重置偏移
|
|
810
|
-
const dblTarget = labelNodes()[0]
|
|
811
|
-
if (dblTarget) {
|
|
812
|
-
const dblInit = { bubbles: true, cancelable: true, composed: true, clientX: dblTarget.getBoundingClientRect().left + 2, clientY: dblTarget.getBoundingClientRect().top + 2 }
|
|
813
|
-
dblTarget.dispatchEvent(new MouseEvent('dblclick', dblInit))
|
|
814
|
-
await wait(300)
|
|
815
|
-
check('双击标签发 labelDblClick(这里接成重置偏移)', logText().includes('labelDblClick') || logText().includes('重置偏移'), logText().slice(0, 160))
|
|
816
|
-
}
|
|
817
|
-
else {
|
|
818
|
-
check('双击标签发 labelDblClick(这里接成重置偏移)', false, '没有标签节点可点')
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
// 3.8 标签显示:动态切节点 / 线路标签内容(setLabelContent 的独立演示)
|
|
822
|
-
if (await clickTab('标签显示')) {
|
|
343
|
+
// 2.0 鹰眼图配置:自定义层、像素比上限与放大 / 复位
|
|
344
|
+
if (await clickTab('鹰眼图配置')) {
|
|
823
345
|
const ready = await canvasReady()
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
return node?.textContent.includes('改哪一类标签') ? node : null
|
|
827
|
-
}, 10000)
|
|
346
|
+
await wait(320)
|
|
347
|
+
const demo = document.querySelector('.dv-demo')
|
|
828
348
|
const setup = demo?.__vueParentComponent?.setupState
|
|
829
349
|
const view = await until(() => setup?.view, 8000)
|
|
830
|
-
const
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
check('
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
await wait(
|
|
843
|
-
|
|
844
|
-
const
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
const
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
check('标签显示:切回节点 + 线路后,一份模板同时落到两类标签上', Boolean(bothChanged) && readout('label-display-action').includes('节点 + 线路'), readout('label-display-action'))
|
|
858
|
-
|
|
859
|
-
const customInput = demo.querySelector('[data-demo="label-display-custom-input"]')
|
|
860
|
-
const customText = '自定义 {name} · {vn_kv}'
|
|
861
|
-
if (customInput) {
|
|
862
|
-
customInput.value = customText
|
|
863
|
-
customInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
350
|
+
const text = key => document.querySelector(`[data-demo="${key}"]`)?.textContent.trim() ?? ''
|
|
351
|
+
const minimapCanvas = () => document.querySelector('.gis-view__minimap-canvas')
|
|
352
|
+
if (ready && view) {
|
|
353
|
+
check('鹰眼图配置:小图与两块自定义绘制层都挂载', Boolean(minimapCanvas()) && setup.minimapOptions().layers.length === 2, minimapCanvas() ? `canvas ${minimapCanvas().width}×${minimapCanvas().height}` : '没有小图画布')
|
|
354
|
+
document.querySelector('[data-demo=minimap-layer-read]').click()
|
|
355
|
+
await wait(60)
|
|
356
|
+
check('鹰眼图配置:layers 的 draw 被真正调用', /网格层 [1-9]\d* 次/.test(text('minimap-layer-readout')) && /指北针层 [1-9]\d* 次/.test(text('minimap-layer-readout')), text('minimap-layer-readout'))
|
|
357
|
+
check('鹰眼图配置:placement 把网格层标成数据下面', text('minimap-layer-readout').includes('below-data'), text('minimap-layer-readout'))
|
|
358
|
+
setup.gridPlacement = 'above-data'
|
|
359
|
+
setup.applyMinimap()
|
|
360
|
+
await wait(160)
|
|
361
|
+
document.querySelector('[data-demo=minimap-layer-read]').click()
|
|
362
|
+
await wait(60)
|
|
363
|
+
check('鹰眼图配置:切到 above-data 后 drawInfo.stage 跟着变', text('minimap-layer-readout').includes('above-data'), text('minimap-layer-readout'))
|
|
364
|
+
const minimapRoot = document.querySelector('.gis-view__minimap')
|
|
365
|
+
const minimapBox = minimapRoot?.querySelector('.gis-view__minimap-box')
|
|
366
|
+
const boxRect = minimapBox?.getBoundingClientRect()
|
|
367
|
+
if (minimapRoot && boxRect && boxRect.width > 0 && boxRect.height > 0) {
|
|
368
|
+
const pointer = { bubbles: true, cancelable: true, composed: true, pointerId: 17, pointerType: 'mouse', isPrimary: true, button: 0 }
|
|
369
|
+
const startX = boxRect.left + boxRect.width / 2
|
|
370
|
+
const startY = boxRect.top + boxRect.height / 2
|
|
371
|
+
minimapRoot.dispatchEvent(new PointerEvent('pointerdown', { ...pointer, clientX: startX, clientY: startY }))
|
|
372
|
+
minimapRoot.dispatchEvent(new PointerEvent('pointermove', { ...pointer, clientX: startX + 7, clientY: startY + 5 }))
|
|
373
|
+
minimapRoot.dispatchEvent(new PointerEvent('pointerup', { ...pointer, clientX: startX + 7, clientY: startY + 5 }))
|
|
374
|
+
const interacted = await until(() => text('minimap-interaction').includes('box-drag'), 1500)
|
|
375
|
+
check('鹰眼图配置:框拖动结束触发 onInteraction,载荷带世界坐标与相机', Boolean(interacted) && text('minimap-interaction').includes('相机已同步'), text('minimap-interaction'))
|
|
376
|
+
view.fitView(44)
|
|
864
377
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
378
|
+
else {
|
|
379
|
+
check('鹰眼图配置:能找到可拖动的视口框', false, minimapBox ? '框尺寸为 0' : '没有视口框')
|
|
380
|
+
}
|
|
381
|
+
const widthBefore = setup.minimapOptions().width
|
|
382
|
+
setup.size = 'large'
|
|
383
|
+
setup.applyMinimap()
|
|
384
|
+
await wait(180)
|
|
385
|
+
check('鹰眼图配置:尺寸切换走 updateOptions 当场重铺画布', setup.minimapOptions().width > widthBefore && minimapCanvas()?.width > widthBefore && text('minimap-readout').includes(`${setup.minimapOptions().width}×${setup.minimapOptions().height}`), text('minimap-readout'))
|
|
386
|
+
const originalDevicePixelRatio = Object.getOwnPropertyDescriptor(window, 'devicePixelRatio')
|
|
387
|
+
try {
|
|
388
|
+
Object.defineProperty(window, 'devicePixelRatio', { configurable: true, value: 2 })
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
void error
|
|
392
|
+
}
|
|
393
|
+
setup.maxPixelRatio = '1'
|
|
394
|
+
setup.applyMinimap()
|
|
395
|
+
await wait(180)
|
|
396
|
+
const options = setup.minimapOptions()
|
|
397
|
+
check('鹰眼图配置:DPR 2 下 maxPixelRatio=1 把备份画布压到 CSS 尺寸(性能档生效)', window.devicePixelRatio === 2 && minimapCanvas()?.width === options.width && minimapCanvas()?.height === options.height, `DPR ${window.devicePixelRatio} / canvas ${minimapCanvas()?.width}×${minimapCanvas()?.height} / CSS ${options.width}×${options.height}`)
|
|
398
|
+
setup.maxPixelRatio = '2'
|
|
399
|
+
setup.applyMinimap()
|
|
400
|
+
await wait(180)
|
|
401
|
+
check('鹰眼图配置:maxPixelRatio=2 时按 DPR 2 重铺备份画布', minimapCanvas()?.width === options.width * 2 && minimapCanvas()?.height === options.height * 2, `canvas ${minimapCanvas()?.width}×${minimapCanvas()?.height} / CSS ${options.width}×${options.height}`)
|
|
402
|
+
if (originalDevicePixelRatio) {
|
|
403
|
+
Object.defineProperty(window, 'devicePixelRatio', originalDevicePixelRatio)
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
delete window.devicePixelRatio
|
|
407
|
+
}
|
|
408
|
+
setup.customLayers = false
|
|
409
|
+
setup.applyMinimap()
|
|
410
|
+
await wait(160)
|
|
411
|
+
check('鹰眼图配置:关掉扩展层后配置里不再下发 layers', setup.minimapOptions().layers.length === 0 && text('minimap-readout').includes('自定义层 0'), text('minimap-readout'))
|
|
412
|
+
const beforeZoom = view.getMinimapZoom()
|
|
413
|
+
document.querySelector('[data-demo=minimap-zoom-in]').click()
|
|
414
|
+
await wait(120)
|
|
415
|
+
check('鹰眼图配置:+ 按钮调用 zoomMinimapBy,倍数当场变化', view.getMinimapZoom() > beforeZoom && text('minimap-zoom-readout').includes(view.getMinimapZoom().toFixed(2)), text('minimap-zoom-readout'))
|
|
416
|
+
document.querySelector('[data-demo=minimap-zoom-reset]').click()
|
|
417
|
+
await wait(120)
|
|
418
|
+
check('鹰眼图配置:1× 按钮把倍数复位成 1', Math.abs(view.getMinimapZoom() - 1) < 1e-9, text('minimap-zoom-readout'))
|
|
878
419
|
}
|
|
879
420
|
else {
|
|
880
|
-
check('
|
|
421
|
+
check('鹰眼图配置:画布与视图都起来了', false, `${ready ? 'canvas 就绪' : '没有 canvas'} / ${view ? 'view 就绪' : '没有 view'}`)
|
|
881
422
|
}
|
|
882
423
|
}
|
|
883
424
|
|
|
884
|
-
//
|
|
885
|
-
if (await clickTab('
|
|
425
|
+
// 2.0a 扩展图形:三类 DOM、相机联动、updateGraphic 与批量增删改
|
|
426
|
+
if (await clickTab('扩展图形')) {
|
|
886
427
|
const ready = await canvasReady()
|
|
887
|
-
const demo = await until(() => {
|
|
888
|
-
const node = document.querySelector('.dv-demo')
|
|
889
|
-
return node?.textContent.includes('悬停样式') && node.textContent.includes('提示内容') ? node : null
|
|
890
|
-
}, 10000)
|
|
891
|
-
const setup = demo?.__vueParentComponent?.setupState
|
|
892
|
-
const view = await until(() => setup?.view, 8000)
|
|
893
428
|
await wait(260)
|
|
894
|
-
const
|
|
895
|
-
const
|
|
896
|
-
const
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
const
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
click('
|
|
930
|
-
const
|
|
931
|
-
check('
|
|
932
|
-
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
}
|
|
941
|
-
setSelect('tooltip-content-mode', 'function')
|
|
942
|
-
const functionMode = await until(() => (typeof view.getOptions().tooltip?.point === 'function' && tooltipContent().includes('函数') ? tooltipContent() : null), 5000)
|
|
943
|
-
check('悬停与提示:内容模式切到函数后,tooltip 用命中载荷当场算出文本', Boolean(functionMode), readout('tooltip-config-readout'))
|
|
944
|
-
|
|
945
|
-
setSelect('tooltip-content-mode', 'element')
|
|
946
|
-
const elementMode = await until(() => {
|
|
947
|
-
const node = tooltipNode()
|
|
948
|
-
return typeof view.getOptions().tooltip?.point === 'function' && node && !node.hidden && node.querySelector('strong') && node.textContent.includes('DOM 元素')
|
|
949
|
-
}, 5000)
|
|
950
|
-
check('悬停与提示:内容模式切到 DOM 元素后,函数返回的节点被直接插进提示框', Boolean(elementMode), readout('tooltip-readout'))
|
|
951
|
-
|
|
952
|
-
click('tooltip-html-preset')
|
|
953
|
-
const htmlMode = await until(() => {
|
|
954
|
-
const node = tooltipNode()
|
|
955
|
-
return view.getOptions().tooltip?.html === true && node && !node.hidden && node.innerHTML.includes('<b>')
|
|
956
|
-
}, 5000)
|
|
957
|
-
check('悬停与提示:html=true 时模板里的 <b> / <br> 按 HTML 渲染', Boolean(htmlMode), readout('tooltip-config-readout'))
|
|
958
|
-
|
|
959
|
-
setSelect('tooltip-channel-scope', 'line')
|
|
960
|
-
const scopeChanged = await until(() => view.getOptions().tooltip?.channelScope === 'line' && readout('tooltip-config-readout').includes('channelScope=line'), 5000)
|
|
961
|
-
check('悬停与提示:channelScope 可现场切换(channel / group / line)', Boolean(scopeChanged), readout('tooltip-config-readout'))
|
|
962
|
-
|
|
963
|
-
click('hover-clear')
|
|
964
|
-
const cleared = await until(() => (!tooltipNode() || tooltipNode().hidden) && readout('hover-readout').includes('已离开'), 5000)
|
|
965
|
-
check('悬停与提示:clearHover 收起提示并撤销悬停高亮', Boolean(cleared), readout('hover-readout'))
|
|
429
|
+
const demo = document.querySelector('.dv-demo')
|
|
430
|
+
const setup = demo?.__vueParentComponent?.setupState
|
|
431
|
+
const activeView = await until(() => setup?.view, 8000)
|
|
432
|
+
const text = key => document.querySelector(`[data-demo="${key}"]`)?.textContent.trim() ?? ''
|
|
433
|
+
const click = key => document.querySelector(`[data-demo="${key}"]`)?.click()
|
|
434
|
+
const graphicNode = id => document.querySelector(`[data-graphic-id=${id}]`)
|
|
435
|
+
if (ready && activeView) {
|
|
436
|
+
const initial = activeView.getGraphics()
|
|
437
|
+
check('扩展图形:初始化同一层里同时有 path / image / canvas 三类 DOM',
|
|
438
|
+
initial.length === 3 && graphicNode('graphic-zone')?.dataset.graphicType === 'path' && graphicNode('graphic-image')?.querySelector('img') instanceof HTMLImageElement && graphicNode('graphic-canvas')?.querySelector('canvas') instanceof HTMLCanvasElement,
|
|
439
|
+
initial.map(item => `${item.id}:${item.type}`).join(' / ') || '没有图形')
|
|
440
|
+
const imageNode = graphicNode('graphic-image')
|
|
441
|
+
const widthBeforeZoom = Number.parseFloat(imageNode.style.width)
|
|
442
|
+
activeView.zoomBy(1.4)
|
|
443
|
+
await wait(140)
|
|
444
|
+
const widthAfterZoom = Number.parseFloat(graphicNode('graphic-image').style.width)
|
|
445
|
+
check('扩展图形:默认 scaleWithView=false 时相机缩放不改变屏幕像素尺寸', Math.abs(widthAfterZoom - widthBeforeZoom) < 0.5, `${widthBeforeZoom} → ${widthAfterZoom}`)
|
|
446
|
+
document.querySelector('[data-demo=graphic-scale]').click()
|
|
447
|
+
await wait(100)
|
|
448
|
+
const widthLinked = Number.parseFloat(graphicNode('graphic-image').style.width)
|
|
449
|
+
activeView.zoomBy(1.2)
|
|
450
|
+
await wait(140)
|
|
451
|
+
const widthLinkedAfter = Number.parseFloat(graphicNode('graphic-image').style.width)
|
|
452
|
+
check('扩展图形:打开 scaleWithView 后图片 / Canvas 宽高跟随相机倍率', widthLinked > widthBeforeZoom + 1 && widthLinkedAfter > widthLinked, `固定 ${widthBeforeZoom} → 联动 ${widthLinked} → 再缩放 ${widthLinkedAfter}`)
|
|
453
|
+
setup.rotation = 33
|
|
454
|
+
setup.opacity = 0.55
|
|
455
|
+
setup.zIndex = 9
|
|
456
|
+
setup.applyImageStyle()
|
|
457
|
+
await wait(100)
|
|
458
|
+
const updated = activeView.getGraphic('graphic-image')
|
|
459
|
+
check('扩展图形:updateGraphic 同时改旋转 / 透明度 / zIndex,DOM 当场重建', updated?.rotation === 33 && Math.abs(updated.opacity - 0.55) < 1e-9 && updated?.zIndex === 9 && graphicNode('graphic-image').style.transform.includes('rotate(33deg)'), JSON.stringify({ rotation: updated?.rotation, opacity: updated?.opacity, zIndex: updated?.zIndex }))
|
|
460
|
+
const pointsBefore = activeView.getData().points.length
|
|
461
|
+
click('graphic-add')
|
|
462
|
+
const added = await until(() => activeView.getGraphics().length === 4, 3000)
|
|
463
|
+
check('扩展图形:addGraphic 只加图形层,不碰节点 / 线路数据', Boolean(added) && activeView.getData().points.length === pointsBefore && text('graphic-count').includes('4 个'), text('graphic-count'))
|
|
464
|
+
click('graphic-add-many')
|
|
465
|
+
const addedMany = await until(() => activeView.getGraphics().length === 6, 3000)
|
|
466
|
+
check('扩展图形:addGraphics 一次加入两条并只触发一次变更', Boolean(addedMany) && text('graphic-event').includes('addGraphics'), text('graphic-event'))
|
|
467
|
+
click('graphic-remove')
|
|
468
|
+
const removed = await until(() => activeView.getGraphics().length === 5, 3000)
|
|
469
|
+
check('扩展图形:removeGraphic 按最近新增的 id 删除', Boolean(removed) && text('graphic-event').includes('removeGraphic'), text('graphic-event'))
|
|
470
|
+
click('graphic-clear')
|
|
471
|
+
const cleared = await until(() => activeView.getGraphics().length === 0 && !graphicNode('graphic-zone'), 3000)
|
|
472
|
+
check('扩展图形:clearGraphics 清空整层并返回条数', Boolean(cleared) && text('graphic-event').includes('清空 5 个'), text('graphic-event'))
|
|
473
|
+
click('graphic-reset')
|
|
474
|
+
const restored = await until(() => activeView.getGraphics().length === 3 && Boolean(graphicNode('graphic-zone')) && Boolean(graphicNode('graphic-image')) && Boolean(graphicNode('graphic-canvas')), 4000)
|
|
475
|
+
check('扩展图形:updateOptions({ graphics }) 恢复三类初始图形与读数', Boolean(restored) && text('graphic-count').includes('3 个') && text('graphic-detail').includes('graphic-image'), text('graphic-count'))
|
|
966
476
|
}
|
|
967
477
|
else {
|
|
968
|
-
check('
|
|
478
|
+
check('扩展图形:画布与视图都起来了', false, `${ready ? 'canvas 就绪' : '没有 canvas'} / ${activeView ? 'view 就绪' : '没有 view'}`)
|
|
969
479
|
}
|
|
970
480
|
}
|
|
971
481
|
|
|
972
|
-
//
|
|
973
|
-
await clickTab('
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
482
|
+
// 2.0a 搜索与筛选:limit / scope / fields / 异步 Worker / setFilter 与可见数据
|
|
483
|
+
if (await clickTab('搜索与筛选')) {
|
|
484
|
+
const ready = await canvasReady()
|
|
485
|
+
await wait(220)
|
|
486
|
+
const demo = document.querySelector('.dv-demo')
|
|
487
|
+
const setup = demo?.__vueParentComponent?.setupState
|
|
488
|
+
const view = await until(() => setup?.view, 8000)
|
|
489
|
+
const click = key => document.querySelector(`[data-demo='${key}']`)?.click()
|
|
490
|
+
const readout = key => document.querySelector(`[data-demo='${key}']`)?.textContent.trim() ?? ''
|
|
491
|
+
if (ready && view) {
|
|
492
|
+
const initialReady = await until(() => readout('search-filter-summary').includes('命中'), 4000)
|
|
493
|
+
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')}`)
|
|
494
|
+
click('search-filter-preset-owner')
|
|
495
|
+
const ownerReady = await until(() => readout('search-filter-summary').includes('节点 8'), 4000)
|
|
496
|
+
check('搜索与筛选:fields=[props.owner] 能命中嵌套业务字段', Boolean(ownerReady) && readout('search-filter-meta').includes('truncated=否'), `${readout('search-filter-summary')} / ${readout('search-filter-meta')}`)
|
|
497
|
+
click('search-filter-preset-channel')
|
|
498
|
+
const channelReady = await until(() => readout('search-filter-summary').includes('通道 1'), 4000)
|
|
499
|
+
check('搜索与筛选:scope=channels 只把通道放进命中', Boolean(channelReady) && readout('search-filter-summary').includes('节点 0') && readout('search-filter-summary').includes('线路 0'), readout('search-filter-summary'))
|
|
500
|
+
click('search-filter-preset-symbol')
|
|
501
|
+
const symbolReady = await until(() => readout('search-filter-summary').includes('节点 8'), 4000)
|
|
502
|
+
check('搜索与筛选:显式 fields=[symbol] 能搜默认跳过的符号字段', Boolean(symbolReady) && readout('search-filter-hit-0').includes('symbol = square'), `${readout('search-filter-summary')} / ${readout('search-filter-hit-0')}`)
|
|
503
|
+
document.querySelector('[data-demo=search-filter-worker]')?.click()
|
|
504
|
+
click('search-filter-run-async')
|
|
505
|
+
const asyncReady = await until(() => readout('search-filter-meta').includes('searchAsync'), 6000)
|
|
506
|
+
check('搜索与筛选:searchAsync 低阈值走 Worker,结果与同步搜索一致', Boolean(asyncReady) && readout('search-filter-meta').includes('worker=是') && readout('search-filter-summary').includes('节点 8'), readout('search-filter-meta'))
|
|
507
|
+
click('search-filter-filter-voltage')
|
|
508
|
+
const filterReady = await until(() => readout('search-filter-filter').includes('可见 12 节点'), 4000)
|
|
509
|
+
check('搜索与筛选:setFilter 隐藏一半节点,getVisibleData 同步可见集合', Boolean(filterReady) && view.getVisibleData().points.length === 12 && view.getData().points.length === 24, `${readout('search-filter-filter')} / ${readout('search-filter-visible')}`)
|
|
510
|
+
click('search-filter-preset-symbol')
|
|
511
|
+
const stillSearches = await until(() => readout('search-filter-summary').includes('节点 8'), 4000)
|
|
512
|
+
check('搜索与筛选:筛选只影响显示,search 仍按全部 24 个节点计算', Boolean(stillSearches) && view.getVisibleData().points.length === 12, `${readout('search-filter-summary')} / 可见 ${view.getVisibleData().points.length}`)
|
|
513
|
+
click('search-filter-filter-clear')
|
|
514
|
+
const cleared = await until(() => view.getVisibleData().points.length === 24 && readout('search-filter-visible').includes('24 节点'), 4000)
|
|
515
|
+
check('搜索与筛选:setFilter(null) 后 getVisibleData 恢复全量', Boolean(cleared), readout('search-filter-visible'))
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
check('搜索与筛选:画布与实例都起来了', false, ready ? '没有 setupState.view' : '没有 canvas')
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// 2.0a 批量新增:五个入口各点一次,数据条数与方法返回都要变
|
|
523
|
+
if (await clickTab('批量新增')) {
|
|
524
|
+
const ready = await canvasReady()
|
|
525
|
+
await wait(220)
|
|
526
|
+
const demo = document.querySelector('.dv-demo')
|
|
527
|
+
const setup = demo?.__vueParentComponent?.setupState
|
|
528
|
+
const view = await until(() => setup?.view, 8000)
|
|
529
|
+
const click = key => document.querySelector(`[data-demo="${key}"]`)?.click()
|
|
530
|
+
const text = key => document.querySelector(`[data-demo="${key}"]`)?.textContent.trim() ?? ''
|
|
531
|
+
if (ready && view) {
|
|
532
|
+
const groupsOf = () => view.getChannels().length
|
|
533
|
+
const lineIndex = view.getData().lines.findIndex(line => String(line.id) === 'line-0')
|
|
534
|
+
const waypointCount = () => view.getWaypoints(lineIndex).length
|
|
535
|
+
const pointsBefore = view.getData().points.length
|
|
536
|
+
click('batch-add-points')
|
|
537
|
+
const pointsAfter = await until(() => view.getData().points.length === pointsBefore + 4, 6000)
|
|
538
|
+
check('批量新增:addPoints 一次补 4 个节点', Boolean(pointsAfter) && text('batch-add-readout').includes('addPoints'), `${pointsBefore} → ${view.getData().points.length} / ${text('batch-add-readout')}`)
|
|
539
|
+
const linesBefore = view.getData().lines.length
|
|
540
|
+
click('batch-add-lines')
|
|
541
|
+
const linesAfter = await until(() => view.getData().lines.length === linesBefore + 4, 6000)
|
|
542
|
+
check('批量新增:addLines 一次补 4 条线路', Boolean(linesAfter) && text('batch-add-readout').includes('addLines'), `${linesBefore} → ${view.getData().lines.length}`)
|
|
543
|
+
const channelsBefore = view.getOptions().channels?.length ?? 0
|
|
544
|
+
click('batch-add-channels')
|
|
545
|
+
const channelsAfter = await until(() => (view.getOptions().channels?.length ?? 0) === channelsBefore + 2, 6000)
|
|
546
|
+
check('批量新增:addChannels 一次补 2 条通道', Boolean(channelsAfter) && text('batch-add-readout').includes('addChannels'), `${channelsBefore} → ${view.getOptions().channels?.length ?? 0}`)
|
|
547
|
+
const groupsBefore = groupsOf()
|
|
548
|
+
click('batch-add-channel-groups')
|
|
549
|
+
const groupsAfter = await until(() => groupsOf() === groupsBefore + 2, 6000)
|
|
550
|
+
check('批量新增:addChannelGroups 一次补 2 组、4 条回线', Boolean(groupsAfter) && text('batch-add-readout').includes('2 组 / 4 条回线'), `${groupsBefore} → ${groupsOf()} / ${text('batch-add-readout')}`)
|
|
551
|
+
const waypointsBefore = waypointCount()
|
|
552
|
+
click('batch-add-waypoints')
|
|
553
|
+
const waypointsAfter = await until(() => waypointCount() > waypointsBefore, 6000)
|
|
554
|
+
check('批量新增:addWaypoints 一次插多个拐点且不虚报失败坐标', Boolean(waypointsAfter) && text('batch-add-readout').includes('addWaypoints') && !text('batch-add-readout').includes('没有落地'), `${waypointsBefore} → ${waypointCount()} / ${text('batch-add-readout')}`)
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
check('批量新增:画布与实例都起来了', false, ready ? '没有 setupState.view' : '没有 canvas')
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// 2.0b 批量复制与合并:整组复制带回来源、线路与通道,撤销后能恢复;指定目标合并能改写最终节点
|
|
562
|
+
if (await clickTab('批量复制与合并')) {
|
|
563
|
+
const ready = await canvasReady()
|
|
564
|
+
await wait(220)
|
|
565
|
+
const demo = document.querySelector('.dv-demo')
|
|
566
|
+
const setup = demo?.__vueParentComponent?.setupState
|
|
567
|
+
const view = await until(() => setup?.view, 8000)
|
|
568
|
+
const click = key => document.querySelector(`[data-demo="${key}"]`)?.click()
|
|
569
|
+
const text = key => document.querySelector(`[data-demo="${key}"]`)?.textContent.trim() ?? ''
|
|
570
|
+
const unwrap = value => value?.value ?? value
|
|
571
|
+
if (ready && view) {
|
|
572
|
+
click('copy-select-sources')
|
|
573
|
+
const selected = await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
574
|
+
check('批量复制与合并:框选来源按钮选中 4 个通道端点', Boolean(selected) && text('copy-merge-selection').includes('4 节点'), text('copy-merge-selection'))
|
|
575
|
+
const features = view.getFeatures()
|
|
576
|
+
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 }))
|
|
577
|
+
const canvasRect = document.querySelector('.gis-view__canvas').getBoundingClientRect()
|
|
578
|
+
const allBox = { x: 0, y: 0, width: canvasRect.width, height: canvasRect.height }
|
|
579
|
+
view.updateOptions({ selectionKinds: ['point'] })
|
|
580
|
+
view.selectInBox(allBox)
|
|
581
|
+
const onlyPoints = view.getSelectionData()
|
|
582
|
+
check('批量复制与合并:框选类型只开 point 时不含线路 / 通道', onlyPoints.pointCount > 0 && onlyPoints.lineCount === 0 && onlyPoints.channelCount === 0, JSON.stringify({ points: onlyPoints.pointCount, lines: onlyPoints.lineCount, channels: onlyPoints.channelCount }))
|
|
583
|
+
view.updateOptions({ selectionKinds: ['line'] })
|
|
584
|
+
view.selectInBox(allBox)
|
|
585
|
+
const onlyLines = view.getSelectionData()
|
|
586
|
+
check('批量复制与合并:框选类型只开 line 时只有通道外线路', onlyLines.pointCount === 0 && onlyLines.lineCount > 0 && onlyLines.channelCount === 0, JSON.stringify({ points: onlyLines.pointCount, lines: onlyLines.lineCount, channels: onlyLines.channelCount }))
|
|
587
|
+
view.updateOptions({ selectionKinds: ['channel'] })
|
|
588
|
+
view.selectInBox(allBox)
|
|
589
|
+
const onlyChannels = view.getSelectionData()
|
|
590
|
+
check('批量复制与合并:框选类型只开 channel 时只有通道回线', onlyChannels.pointCount === 0 && onlyChannels.lineCount > 0 && onlyChannels.channelCount > 0, JSON.stringify({ points: onlyChannels.pointCount, lines: onlyChannels.lineCount, channels: onlyChannels.channelCount }))
|
|
591
|
+
view.updateOptions({ selectionKinds: ['point', 'line', 'channel'] })
|
|
592
|
+
click('copy-select-sources')
|
|
593
|
+
await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
594
|
+
const pointsBeforeCopy = view.getData().points.length
|
|
595
|
+
click('copy-selected')
|
|
596
|
+
const copied = await until(() => view.getData().points.length >= pointsBeforeCopy + 4, 6000)
|
|
597
|
+
const copyPayload = unwrap(setup.lastBatch)
|
|
598
|
+
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}`)
|
|
599
|
+
check('批量复制与合并:复制读数能从事件载荷看到来源与落地数据', text('copy-readout').includes('copySelectedPoints') && text('copy-readout').includes('来源') && text('copy-readout').includes('线路') && text('copy-readout').includes('通道'), text('copy-readout'))
|
|
600
|
+
click('copy-revert')
|
|
601
|
+
const reverted = await until(() => view.getData().points.length === pointsBeforeCopy, 6000)
|
|
602
|
+
check('批量复制与合并:revert / cancel 一次收回整组复制', Boolean(reverted) && text('copy-action').includes('已撤销整组复制'), `${pointsBeforeCopy} / ${view.getData().points.length} · ${text('copy-action')}`)
|
|
603
|
+
click('select-sample-channel')
|
|
604
|
+
const channelSelected = await until(() => view.getSelectionData().channelCount === 1 && view.getSelectionData().lineCount === 3, 5000)
|
|
605
|
+
const pointsBeforeChannelCopy = view.getData().points.length
|
|
606
|
+
click('copy-selected')
|
|
607
|
+
const channelCopied = await until(() => view.getData().points.length === pointsBeforeChannelCopy + 2, 6000)
|
|
608
|
+
const channelCopyPayload = unwrap(setup.lastBatch)
|
|
609
|
+
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}`)
|
|
610
|
+
const channelCopyLinks = channelCopyPayload?.lineChannels ?? []
|
|
611
|
+
check('批量复制与合并:lineChannels 与复制出来的回线一一对齐,并指向新通道 / 新分组',
|
|
612
|
+
channelCopyLinks.length === channelCopyPayload?.lines?.length &&
|
|
613
|
+
channelCopyLinks.every((slot, index) => slot &&
|
|
614
|
+
slot.lineIndex === channelCopyPayload.lineIndexes[index] &&
|
|
615
|
+
slot.lineId === channelCopyPayload.lines[index].id &&
|
|
616
|
+
slot.channelIndex === channelCopyPayload.channelIndexes[0] &&
|
|
617
|
+
slot.channelId === channelCopyPayload.channels[0].id &&
|
|
618
|
+
Array.isArray(slot.group?.lineIds) && slot.group.lineIds.includes(slot.lineId)) &&
|
|
619
|
+
text('copy-line-channels-readout').includes('→'),
|
|
620
|
+
`${text('copy-line-channels-readout')} / ${JSON.stringify(channelCopyLinks.map(slot => ({ line: slot?.lineIndex, channel: slot?.channelIndex, group: slot?.groupId })))}`)
|
|
621
|
+
click('copy-revert')
|
|
622
|
+
await until(() => view.getData().points.length === pointsBeforeChannelCopy, 6000)
|
|
623
|
+
click('copy-select-sources')
|
|
624
|
+
await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
625
|
+
click('copy-clipboard')
|
|
626
|
+
const clipboardReady = await until(() => text('clipboard-readout').includes('count=4'), 5000)
|
|
627
|
+
const clipboard = unwrap(setup.clipboardItem)
|
|
628
|
+
check('批量复制与合并:copyPointsToClipboard 留下整组来源快照', Boolean(clipboardReady) && clipboard?.sources?.length === 4 && clipboard?.count === 4, text('clipboard-readout'))
|
|
629
|
+
const pointsBeforePaste = view.getData().points.length
|
|
630
|
+
click('paste-points')
|
|
631
|
+
const pasted = await until(() => view.getData().points.length >= pointsBeforePaste + 4, 6000)
|
|
632
|
+
check('批量复制与合并:pastePoints 按整组锚点落回画布', Boolean(pasted) && text('copy-readout').includes('pastePoints'), `${pointsBeforePaste} → ${view.getData().points.length} / ${text('copy-readout')}`)
|
|
633
|
+
click('copy-revert')
|
|
634
|
+
await until(() => view.getData().points.length === pointsBeforePaste, 6000)
|
|
635
|
+
click('merge-select-sources')
|
|
636
|
+
await wait(120)
|
|
637
|
+
const targetOptionCount = document.querySelectorAll('[data-demo="merge-target"] option').length
|
|
638
|
+
check('批量复制与合并:保留目标下拉只列当前选中节点', targetOptionCount === 4, `${targetOptionCount} 个选项`)
|
|
639
|
+
const targetId = String(setup.mergeTargetId ?? '')
|
|
640
|
+
const pointsBeforeMerge = view.getData().points.length
|
|
641
|
+
click('merge-selected')
|
|
642
|
+
const mergedCount = await until(() => view.getData().points.length === pointsBeforeMerge - 3, 6000)
|
|
643
|
+
const target = view.getData().points.find(point => String(point.id) === targetId)
|
|
644
|
+
check('批量复制与合并:mergeSelectedPoints 指定保留目标并并掉 3 个来源', Boolean(mergedCount) && target?.demo_merged === true && target?.zone_name === '合并后(item 改写)', `${pointsBeforeMerge} → ${view.getData().points.length} / 目标 ${targetId} / ${target?.name ?? '—'}`)
|
|
645
|
+
check('批量复制与合并:合并事件读数带来源、目标与最终节点', text('merge-readout').includes('mergeSelectedPoints') && text('merge-readout').includes('保留') && text('merge-readout').includes('并掉') && text('merge-readout').includes('最终'), text('merge-readout'))
|
|
646
|
+
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'))
|
|
647
|
+
const mergeTransactionReady = await until(() => {
|
|
648
|
+
const preview = text('copy-merge-backend-request')
|
|
649
|
+
return preview.includes('POST /gis/merges') && preview.includes('200 OK')
|
|
650
|
+
}, 8000)
|
|
651
|
+
const mergeRequestPreview = text('copy-merge-backend-request')
|
|
652
|
+
check('批量复制与合并:mergeSelectedPoints 收成一条 /gis/merges 事务请求',
|
|
653
|
+
Boolean(mergeTransactionReady) &&
|
|
654
|
+
mergeRequestPreview.includes('POST /gis/merges') &&
|
|
655
|
+
mergeRequestPreview.includes('"changes"') &&
|
|
656
|
+
mergeRequestPreview.includes('"impact"') &&
|
|
657
|
+
mergeRequestPreview.includes('"baseVersion"') &&
|
|
658
|
+
mergeRequestPreview.includes('"opId"') &&
|
|
659
|
+
mergeRequestPreview.includes('200 OK'),
|
|
660
|
+
mergeRequestPreview.slice(0, 300).replace(/\s+/g, ' '))
|
|
661
|
+
check('批量复制与合并:合并成功响应回标准响应包与事务 id 集合',
|
|
662
|
+
['"code": "OK"', '"operation": "merge.transaction"', '"transactionId"', '"retainedPointIds"', '"removedPointIds"', '"removedLineIds"', '"removedChannelIds"', 'Idempotency-Key:', 'If-Match: "v'].every(key => mergeRequestPreview.includes(key)),
|
|
663
|
+
mergeRequestPreview.slice(-320).replace(/\s+/g, ' '))
|
|
664
|
+
const confirmButton = document.querySelector('[data-demo="merge-confirm"]')
|
|
665
|
+
const cancelButton = document.querySelector('[data-demo="merge-cancel"]')
|
|
666
|
+
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()}`)
|
|
667
|
+
setup.resetScene()
|
|
668
|
+
await wait(300)
|
|
669
|
+
const dragCanvas = document.querySelector('.gis-view__canvas')
|
|
670
|
+
const dragBox = dragCanvas.getBoundingClientRect()
|
|
671
|
+
const dragSend = (type, screen, extra = {}) => {
|
|
672
|
+
dragCanvas.dispatchEvent(new PointerEvent(type, {
|
|
673
|
+
bubbles: true,
|
|
674
|
+
cancelable: true,
|
|
675
|
+
composed: true,
|
|
676
|
+
pointerId: 17,
|
|
677
|
+
pointerType: 'mouse',
|
|
678
|
+
isPrimary: true,
|
|
679
|
+
button: 0,
|
|
680
|
+
buttons: extra.buttons ?? (type === 'pointerup' ? 0 : 1),
|
|
681
|
+
clientX: dragBox.left + screen[0],
|
|
682
|
+
clientY: dragBox.top + screen[1],
|
|
683
|
+
...extra
|
|
684
|
+
}))
|
|
685
|
+
}
|
|
686
|
+
const dragSourcePoint = view.getData().points.find(point => point.id === 'node-0')
|
|
687
|
+
const dragTargetPoint = view.getData().points.find(point => point.id === 'node-7')
|
|
688
|
+
const dragSource = view.project(dragSourcePoint.x, dragSourcePoint.y)
|
|
689
|
+
const dragTarget = view.project(dragTargetPoint.x, dragTargetPoint.y)
|
|
690
|
+
view.selectFeatures(null)
|
|
691
|
+
dragSend('pointermove', dragSource, { buttons: 0 })
|
|
692
|
+
dragSend('pointerdown', dragSource, { buttons: 1 })
|
|
693
|
+
dragSend('pointermove', [(dragSource[0] + dragTarget[0]) / 2, (dragSource[1] + dragTarget[1]) / 2], { buttons: 1 })
|
|
694
|
+
dragSend('pointermove', dragTarget, { buttons: 1 })
|
|
695
|
+
dragSend('pointerup', dragTarget, { buttons: 0 })
|
|
696
|
+
const dragPending = await until(() => Boolean(view.getPointMerge()) && view.getPointMerge().confirmed === false, 6000)
|
|
697
|
+
check('批量复制与合并:真实拖拽相撞进入 pending,数据先不动', Boolean(dragPending) && view.getData().points.length === 14, JSON.stringify({ phase: view.getPointMerge()?.phase, confirmed: view.getPointMerge()?.confirmed, points: view.getData().points.length }))
|
|
698
|
+
const pendingMergePayload = view.getPointMerge()
|
|
699
|
+
const pendingImpact = pendingMergePayload?.impact
|
|
700
|
+
const pendingImpactReadout = text('merge-pending-readout')
|
|
701
|
+
check('批量复制与合并:pending 载荷带 impact 预演(含线路 id、改线 / 删线与通道自环影响)',
|
|
702
|
+
Boolean(pendingImpact) && pendingImpact.lineCount === pendingImpact.lineIds.length && pendingImpact.lineCount >= pendingImpact.removedLines && pendingImpact.linesChanged > 0 && pendingImpact.removedLines > 0 && pendingImpact.removedChannels >= 1 && pendingImpactReadout.includes('影响') && pendingImpactReadout.includes('改线'),
|
|
703
|
+
pendingImpact ? `线路 ${pendingImpact.lineCount}(改 ${pendingImpact.linesChanged} / 删 ${pendingImpact.removedLines})· 通道删 ${pendingImpact.removedChannels} · ${pendingImpact.lineIds.join(', ')}` : '没有 impact')
|
|
704
|
+
click('merge-impact-details')
|
|
705
|
+
await wait(80)
|
|
706
|
+
const pendingImpactDetail = text('merge-impact-detail-readout')
|
|
707
|
+
check('批量复制与合并:按 impact 的 id 调 getLine / getChannel 能列出详情(删掉的通道在确认前仍可读)',
|
|
708
|
+
Boolean(pendingImpact) &&
|
|
709
|
+
pendingImpact.lineIds.every(id => pendingImpactDetail.includes(String(id))) &&
|
|
710
|
+
pendingImpact.channelIds.every(id => pendingImpactDetail.includes(String(id))) &&
|
|
711
|
+
!pendingImpactDetail.includes('已不存在') && pendingImpactDetail.includes('回'),
|
|
712
|
+
pendingImpactDetail.slice(0, 180))
|
|
713
|
+
setup.mergeKeep = 'source'
|
|
714
|
+
setup.mergedName = '拖拽保留来源'
|
|
715
|
+
setup.mergedZone = '拖拽来源最终信息'
|
|
716
|
+
setup.mergedColor = '#00ff88'
|
|
717
|
+
setup.mergedSize = '21'
|
|
718
|
+
setup.mergedSymbol = 'square'
|
|
719
|
+
setup.mergedStrokeColor = '#222222'
|
|
720
|
+
let confirmImpact = null
|
|
721
|
+
let landedMerge = null
|
|
722
|
+
const offMergeProbe = view.on('pointMerge', payload => {
|
|
723
|
+
if (payload.phase === 'confirm') {
|
|
724
|
+
confirmImpact = payload.impact
|
|
725
|
+
}
|
|
726
|
+
if (payload.phase === 'merge' && payload.ok) {
|
|
727
|
+
landedMerge = payload
|
|
728
|
+
}
|
|
729
|
+
})
|
|
730
|
+
click('merge-confirm')
|
|
731
|
+
const sourceMergeDone = await until(() => view.getData().points.length === 13 && view.getData().points.some(point => point.id === 'node-0'), 6000)
|
|
732
|
+
offMergeProbe()
|
|
733
|
+
const mergedSource = view.getData().points.find(point => point.id === 'node-0')
|
|
734
|
+
const removedTarget = view.getData().points.find(point => point.id === 'node-7')
|
|
735
|
+
check('批量复制与合并:confirm 传 keep: source 时保留拖动来源并删碰撞目标', Boolean(sourceMergeDone) && Boolean(mergedSource) && !removedTarget, JSON.stringify({ source: mergedSource?.id, targetGone: !removedTarget, points: view.getData().points.length }))
|
|
736
|
+
check('批量复制与合并:confirm 的 impact 按最终 keep 重算,且与落地后的 linesChanged / removedLines 对账一致',
|
|
737
|
+
Boolean(confirmImpact) && Boolean(landedMerge) && confirmImpact.linesChanged === landedMerge.linesChanged && confirmImpact.removedLines === landedMerge.removedLines && confirmImpact.removedChannels === landedMerge.removedChannels && confirmImpact.mergedChannels === landedMerge.mergedChannels,
|
|
738
|
+
confirmImpact && landedMerge ? `impact 改 ${confirmImpact.linesChanged} / 删 ${confirmImpact.removedLines} / 通道 ${confirmImpact.removedChannels}+${confirmImpact.mergedChannels} · 落地 改 ${landedMerge.linesChanged} / 删 ${landedMerge.removedLines} / 通道 ${landedMerge.removedChannels}+${landedMerge.mergedChannels}` : '没有 confirm / merge 载荷')
|
|
739
|
+
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 }))
|
|
740
|
+
click('copy-select-sources')
|
|
741
|
+
await wait(120)
|
|
742
|
+
const pointsBeforeManual = view.getData().points.length
|
|
743
|
+
click('copy-selected')
|
|
744
|
+
const autoCopied = await until(() => view.getData().points.length === pointsBeforeManual + 4, 6000)
|
|
745
|
+
click('manual-add-point')
|
|
746
|
+
const manualAdded = await until(() => view.getData().points.length === pointsBeforeManual + 4 && text('manual-add-point-readout').includes('已删自动副本 4 个'), 6000)
|
|
747
|
+
const manualPoints = view.getData().points.slice(-4)
|
|
748
|
+
const autoCopies = view.getData().points.filter(point => point.zone_name === '内置复制副本').length
|
|
749
|
+
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')}`)
|
|
750
|
+
await wait(560)
|
|
751
|
+
const copyRequestPreview = text('copy-merge-backend-request')
|
|
752
|
+
check('批量复制与合并:复制假接口请求体带批量字段、版本与幂等键',
|
|
753
|
+
copyRequestPreview.includes('POST /gis/copies/') &&
|
|
754
|
+
['"points"', '"lines"', '"channels"', '"lineChannels"', '"baseVersion"', '"opId"'].every(key => copyRequestPreview.includes(key)) &&
|
|
755
|
+
text('copy-merge-backend-status').includes('420ms'),
|
|
756
|
+
copyRequestPreview.slice(0, 200))
|
|
757
|
+
check('批量复制与合并:复制成功响应回标准响应包、created 与 idMap',
|
|
758
|
+
['"code": "OK"', '"operation": "copy.', '"transactionId"', '"created"', '"counts"', '"idMap"', 'Idempotency-Key:', 'If-Match: "v'].every(key => copyRequestPreview.includes(key)),
|
|
759
|
+
copyRequestPreview.slice(-320).replace(/\s+/g, ' '))
|
|
760
|
+
const copyReplayButton = document.querySelector('[data-demo="copy-backend-replay"]')
|
|
761
|
+
const copyVersionBeforeReplay = setup.backendSnapshot?.version
|
|
762
|
+
const copyAppliedBeforeReplay = setup.backendSnapshot?.applied
|
|
763
|
+
const copyReplayReady = Boolean(copyReplayButton) && !copyReplayButton.disabled
|
|
764
|
+
copyReplayButton?.click()
|
|
765
|
+
const copyReplayHit = await until(() => text('copy-backend-readout').includes('幂等回放') && text('copy-merge-backend-request').includes('"idempotent": true'), 5000)
|
|
766
|
+
check('批量复制与合并:重放原 opId 命中幂等缓存,版本与应用数都不前进',
|
|
767
|
+
copyReplayReady && Boolean(copyReplayHit) && setup.backendSnapshot?.version === copyVersionBeforeReplay && setup.backendSnapshot?.applied === copyAppliedBeforeReplay && text('copy-merge-backend-request').includes('Idempotency-Key 命中'),
|
|
768
|
+
`v${copyVersionBeforeReplay} / applied ${copyAppliedBeforeReplay} → v${setup.backendSnapshot?.version} / applied ${setup.backendSnapshot?.applied} / ${text('copy-backend-readout')}`)
|
|
769
|
+
const backendModeSelect = document.querySelector('[data-demo="copy-merge-backend-mode"]')
|
|
770
|
+
const copyRetryButton = document.querySelector('[data-demo="copy-backend-retry"]')
|
|
771
|
+
const copyRollbackButton = document.querySelector('[data-demo="copy-backend-rollback"]')
|
|
772
|
+
backendModeSelect.value = 'network'
|
|
773
|
+
backendModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
774
|
+
await wait(60)
|
|
775
|
+
click('copy-select-sources')
|
|
776
|
+
await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
777
|
+
const pointsBeforeFailedCopy = view.getData().points.length
|
|
778
|
+
click('copy-selected')
|
|
779
|
+
const copyFailed = await until(() => !copyRetryButton.disabled && !copyRollbackButton.disabled && text('copy-backend-readout').includes('失败'), 8000)
|
|
780
|
+
const failedCopyReadout = text('copy-backend-readout')
|
|
781
|
+
check('批量复制与合并:复制网络失败后保留本地结果,重试与回滚按钮可用', Boolean(copyFailed) && view.getData().points.length === pointsBeforeFailedCopy + 4 && failedCopyReadout.includes('网络请求没有到达服务端'), `${pointsBeforeFailedCopy} → ${view.getData().points.length} / retry=${copyRetryButton.disabled} rollback=${copyRollbackButton.disabled} / ${failedCopyReadout}`)
|
|
782
|
+
copyRetryButton.click()
|
|
783
|
+
const copyRetried = await until(() => text('copy-backend-readout').includes('200 OK'), 5000)
|
|
784
|
+
check('批量复制与合并:复制失败后重试同一请求成功并推进服务端版本', Boolean(copyRetried) && !text('copy-backend-readout').includes('失败'), text('copy-backend-readout'))
|
|
785
|
+
backendModeSelect.value = 'server'
|
|
786
|
+
backendModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
787
|
+
click('copy-select-sources')
|
|
788
|
+
await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
789
|
+
const pointsBeforeRollback = view.getData().points.length
|
|
790
|
+
click('copy-selected')
|
|
791
|
+
const rollbackReady = await until(() => !copyRollbackButton.disabled && text('copy-backend-readout').includes('失败'), 8000)
|
|
792
|
+
copyRollbackButton.click()
|
|
793
|
+
const copyRolledBack = await until(() => view.getData().points.length === pointsBeforeRollback && text('copy-backend-readout').includes('回滚'), 5000)
|
|
794
|
+
check('批量复制与合并:复制失败后可回滚本地整批,服务端版本不前进', Boolean(rollbackReady) && Boolean(copyRolledBack), `${pointsBeforeRollback} → ${view.getData().points.length} / ${text('copy-backend-readout')}`)
|
|
795
|
+
const conflictVersion = setup.backendSnapshot?.version
|
|
796
|
+
const conflictApplied = setup.backendSnapshot?.applied
|
|
797
|
+
backendModeSelect.value = 'conflict'
|
|
798
|
+
backendModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
799
|
+
click('copy-select-sources')
|
|
800
|
+
await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
801
|
+
click('copy-selected')
|
|
802
|
+
const copyConflict = await until(() => !copyRetryButton.disabled && text('copy-backend-readout').includes('409'), 8000)
|
|
803
|
+
const conflictPreview = text('copy-merge-backend-request')
|
|
804
|
+
check('批量复制与合并:版本冲突回 409 响应包与 expectedVersion / currentVersion',
|
|
805
|
+
Boolean(copyConflict) && conflictPreview.includes('409 Conflict') && conflictPreview.includes('"code": "VERSION_CONFLICT"') && conflictPreview.includes(`"expectedVersion": ${conflictVersion}`) && conflictPreview.includes(`"currentVersion": ${conflictVersion + 1}`) && setup.backendSnapshot?.applied === conflictApplied,
|
|
806
|
+
`${conflictPreview.slice(-320).replace(/\s+/g, ' ')} / ${text('copy-backend-readout')}`)
|
|
807
|
+
copyRetryButton.click()
|
|
808
|
+
const copyRebased = await until(() => text('copy-backend-readout').includes('200 OK') && !text('copy-backend-readout').includes('失败'), 5000)
|
|
809
|
+
check('批量复制与合并:409 后 rebase 生成新 opId 并成功落库',
|
|
810
|
+
Boolean(copyRebased) && setup.backendSnapshot?.version === conflictVersion + 2 && setup.backendSnapshot?.applied === conflictApplied + 1,
|
|
811
|
+
`v${conflictVersion} → v${setup.backendSnapshot?.version} / applied ${conflictApplied} → ${setup.backendSnapshot?.applied} / ${text('copy-backend-readout')}`)
|
|
812
|
+
// 拆分:先选一组,再点演示按钮;节点 +1、读数落到面板,撤销一步回到拆分前
|
|
813
|
+
click('copy-select-sources')
|
|
814
|
+
const splitSelectionReady = await until(() => view.getSelectionData().pointCount === 4, 5000)
|
|
815
|
+
const splitSource = view.getSelectionData().points?.[0] ?? view.getData().points[0]
|
|
816
|
+
const pointsBeforeSplit = view.getData().points.length
|
|
817
|
+
click('split-selected')
|
|
818
|
+
const splitLanded = await until(() => view.getData().points.length === pointsBeforeSplit + 1 && text('split-readout').includes('splitPoint'), 6000)
|
|
819
|
+
const splitReadout = text('split-readout')
|
|
820
|
+
check('批量复制与合并:拆分选中节点走 splitPoint,第二份与改线 / 改通道读数落面板',
|
|
821
|
+
Boolean(splitSelectionReady) && Boolean(splitLanded) && text('copy-action').includes('splitPoint') && splitReadout.includes('改线') && splitReadout.includes('改通道'),
|
|
822
|
+
`${pointsBeforeSplit} → ${view.getData().points.length} · ${splitReadout}`)
|
|
823
|
+
view.undo()
|
|
824
|
+
const splitUndone = await until(() => view.getData().points.length === pointsBeforeSplit && view.getData().points.some(point => String(point.id) === String(splitSource?.id)), 5000)
|
|
825
|
+
check('批量复制与合并:拆分是一步历史,撤销后节点与连线恢复', Boolean(splitUndone), `${view.getData().points.length} 点 / ${splitSource?.id ?? '—'}`)
|
|
997
826
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
827
|
+
else {
|
|
828
|
+
check('批量复制与合并:画布与实例都起来了', false, ready ? '没有 setupState.view' : '没有 canvas')
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// 2.0c 扩展能力:五个扩展点各开一次,读数 / 徽标 / 导出结果与 WebGL 层都要跟着变
|
|
833
|
+
if (await clickTab('扩展能力')) {
|
|
834
|
+
const ready = await canvasReady()
|
|
835
|
+
await wait(220)
|
|
836
|
+
const demo = document.querySelector('.dv-demo')
|
|
837
|
+
const setup = demo?.__vueParentComponent?.setupState
|
|
838
|
+
const view = await until(() => setup?.view, 8000)
|
|
839
|
+
const readout = () => document.querySelector('[data-demo=extension-readout]')?.textContent.trim() ?? ''
|
|
840
|
+
const exportText = () => document.querySelector('[data-demo=extension-export-text]')?.textContent.trim() ?? ''
|
|
841
|
+
const click = key => document.querySelector(`[data-demo=${key}]`)?.click()
|
|
842
|
+
const initialReady = await until(() => readout().includes('evenIndex 已注册') && readout().includes('spark 已注册'), 6000)
|
|
843
|
+
check('扩展能力:运算符与图元注册进注册表,初始 18 个节点全部可见', Boolean(ready) && Boolean(initialReady) && readout().includes('可见 18 / 18'), readout())
|
|
844
|
+
click('extension-operator')
|
|
845
|
+
const filtered = await until(() => readout().includes('可见 9 / 18'), 4000)
|
|
846
|
+
check('扩展能力:自定义运算符 evenIndex 筛出 9 个偶数节点', Boolean(filtered) && readout().includes('筛选 开'), readout())
|
|
847
|
+
click('extension-symbol')
|
|
848
|
+
const symbolReady = await until(() => readout().includes('spark 已注册') && readout().includes('/ 使用中'), 4000)
|
|
849
|
+
check('扩展能力:patchPoints 一次换完全部自定义图元', Boolean(symbolReady), readout())
|
|
850
|
+
click('extension-symbol-variant')
|
|
851
|
+
const variantReady = await until(() => readout().includes('菱形'), 4000)
|
|
852
|
+
check('扩展能力:同名重新注册图元当场换画笔(读数从十字切到菱形)', Boolean(variantReady), readout())
|
|
853
|
+
click('extension-plugin')
|
|
854
|
+
const badgeMounted = await until(() => document.querySelector('.demo-extension-badge'), 4000)
|
|
855
|
+
check('扩展能力:view.use 挂载的插件把徽标挂到实例根节点上', Boolean(badgeMounted) && readout().includes('运行时插件 已挂载'), badgeMounted?.textContent ?? '没有徽标')
|
|
856
|
+
click('extension-plugin')
|
|
857
|
+
const badgeRemoved = await until(() => !document.querySelector('.demo-extension-badge'), 4000)
|
|
858
|
+
check('扩展能力:卸载函数执行 destroy,徽标与读数一起撤回', Boolean(badgeRemoved) && readout().includes('运行时插件 未挂载'), readout())
|
|
859
|
+
click('extension-export')
|
|
860
|
+
const exported = await until(() => exportText().includes('gis-view.extension-summary.v1') && exportText().includes('highLoadStations'), 4000)
|
|
861
|
+
let summary = null
|
|
862
|
+
try {
|
|
863
|
+
summary = JSON.parse(exportText())
|
|
1005
864
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
const
|
|
1012
|
-
const
|
|
1013
|
-
const
|
|
1014
|
-
check('
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
check('
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
const
|
|
1038
|
-
check('
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
copyBar?.querySelector('.dv-button--primary')?.click()
|
|
1042
|
-
const revertedCount = await until(() => (eventView.getData().points.length === pointsBeforeCopy ? eventView.getData().points.length : null), 6000)
|
|
1043
|
-
check('事件与方法:收尾条「撤销」= payload.revert(),复制品整份收回去', revertedCount === pointsBeforeCopy && !eventDemo.querySelector('.dv-confirm'), `points=${pointsBeforeCopy} → ${eventView.getData().points.length}`)
|
|
1044
|
-
// 「知道了」只关掉条:复制已经落地,复制品留着
|
|
1045
|
-
eventSend('pointerdown', copyFrom, { altKey: true })
|
|
1046
|
-
eventSend('pointermove', copyTo, { buttons: 0, altKey: true })
|
|
1047
|
-
eventSend('pointerup', copyTo, { buttons: 0, altKey: true })
|
|
1048
|
-
await wait(140)
|
|
1049
|
-
const keptBar = await until(() => eventDemo.querySelector('.dv-confirm'), 6000)
|
|
1050
|
-
keptBar?.querySelector('.dv-button:not(.dv-button--primary)')?.click()
|
|
1051
|
-
await wait(160)
|
|
1052
|
-
check('事件与方法:收尾条「知道了」只关框,复制品留着', !eventDemo.querySelector('.dv-confirm') && eventView.getData().points.length === pointsBeforeCopy + 1, 'points=' + eventView.getData().points.length)
|
|
1053
|
-
}
|
|
1054
|
-
else {
|
|
1055
|
-
check('事件与方法:画布与视图都起来了', false, '没等到 canvas 或 view')
|
|
865
|
+
catch {
|
|
866
|
+
summary = null
|
|
867
|
+
}
|
|
868
|
+
check('扩展能力:defineExportFormat 产出业务 DTO(含高负载场站清单)', Boolean(exported) && summary?.schema === 'gis-view.extension-summary.v1' && summary.highLoadCount > 0 && summary.highLoadStations.length === summary.highLoadCount, summary ? `高负载 ${summary.highLoadCount} 个 / ${summary.highLoadStations.length} 条` : exportText().slice(0, 80))
|
|
869
|
+
click('extension-webgl')
|
|
870
|
+
const layerMounted = await until(() => view?.getWebGLLayers().some(layer => layer.id === 'extension-webgl'), 4000)
|
|
871
|
+
const layerDrawn = await until(() => Number(setup.webglDrawCount) > 0, 4000)
|
|
872
|
+
const mountedLayer = view?.getWebGLLayers().find(layer => layer.id === 'extension-webgl')
|
|
873
|
+
check('扩展能力:WebGL 自定义层挂载到最终画布,view.gl 可直接取、draw 真的执行',
|
|
874
|
+
Boolean(layerMounted) && Boolean(layerDrawn) && Boolean(view.gl) && mountedLayer?.enabled === true && mountedLayer?.order === 20 && readout().includes('WebGL 上下文 可直接取'),
|
|
875
|
+
JSON.stringify({ gl: Boolean(view?.gl), layer: mountedLayer && { enabled: mountedLayer.enabled, order: mountedLayer.order }, draw: setup.webglDrawCount }))
|
|
876
|
+
click('extension-webgl-enabled')
|
|
877
|
+
await wait(180)
|
|
878
|
+
const hiddenLayer = view?.getWebGLLayers().find(layer => layer.id === 'extension-webgl')
|
|
879
|
+
const frozenDrawCount = Number(setup.webglDrawCount)
|
|
880
|
+
await wait(300)
|
|
881
|
+
check('扩展能力:enabled: false 后层仍在注册表里,但 draw 停止执行',
|
|
882
|
+
hiddenLayer?.enabled === false && setup.webglEnabled === false && Number(setup.webglDrawCount) === frozenDrawCount,
|
|
883
|
+
JSON.stringify({ enabled: hiddenLayer?.enabled, draw: [frozenDrawCount, setup.webglDrawCount] }))
|
|
884
|
+
click('extension-webgl-enabled')
|
|
885
|
+
await wait(120)
|
|
886
|
+
click('extension-webgl-order')
|
|
887
|
+
await wait(120)
|
|
888
|
+
check('扩展能力:updateWebGLLayer 原地恢复绘制并改 order',
|
|
889
|
+
view?.getWebGLLayers().find(layer => layer.id === 'extension-webgl')?.order === 40 && setup.webglEnabled === true,
|
|
890
|
+
JSON.stringify(view?.getWebGLLayers().map(layer => ({ id: layer.id, order: layer.order, enabled: layer.enabled }))))
|
|
891
|
+
const drawBeforeRequest = Number(setup.webglDrawCount)
|
|
892
|
+
click('extension-webgl-request')
|
|
893
|
+
const requestedFrame = await until(() => Number(setup.webglDrawCount) > drawBeforeRequest, 3000)
|
|
894
|
+
check('扩展能力:requestRender() 重新排一帧并再次执行自定义 draw', Boolean(requestedFrame) && Number(setup.webglDrawCount) > drawBeforeRequest, `${drawBeforeRequest} -> ${setup.webglDrawCount}`)
|
|
895
|
+
click('extension-webgl')
|
|
896
|
+
const layerRemoved = await until(() => !view?.getWebGLLayers().some(layer => layer.id === 'extension-webgl'), 4000)
|
|
897
|
+
check('扩展能力:卸载后层从 getWebGLLayers 与 getOptions().webglLayers 一起消失',
|
|
898
|
+
Boolean(layerRemoved) && !(view.getOptions().webglLayers ?? []).some(layer => layer.id === 'extension-webgl') && readout().includes('WebGL 层 未挂载'),
|
|
899
|
+
JSON.stringify({ layers: view?.getWebGLLayers().map(layer => layer.id), options: view?.getOptions().webglLayers?.map(layer => layer.id), readout: readout() }))
|
|
1056
900
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
901
|
+
|
|
902
|
+
// 2.1 拖拽落点:看板图标合成一次拖放 —— 拖拽期间不出现浏览器默认的看板拖影,地图上只跟着一个点
|
|
903
|
+
// (组件画的落点预览,与落地后的节点同一套画法);松手先显示在地图上,指定属性之后
|
|
904
|
+
// 「按属性新增」才写进数据、「取消新增」什么都不写;落地后的尺寸与预览里报出的尺寸必须一样
|
|
905
|
+
await clickTab('拖拽落点')
|
|
906
|
+
const dragReady = await canvasReady()
|
|
907
|
+
const paletteItems = [...document.querySelectorAll('.dv-palette__item')]
|
|
908
|
+
let dragResult = null
|
|
909
|
+
let cancelResult = null
|
|
910
|
+
let strayResult = null
|
|
911
|
+
let ghostHidden = false
|
|
912
|
+
let ghostNode = null
|
|
913
|
+
if (dragReady && paletteItems.length === 7) {
|
|
914
|
+
const dragView = document.querySelector('.dv-demo').__vueParentComponent.setupState.view
|
|
915
|
+
const pointsBefore = dragView.getData().points.length
|
|
916
|
+
const canvasNode = document.querySelector('.dv-demo .dv-canvas')
|
|
917
|
+
const canvasBox = canvasNode.getBoundingClientRect()
|
|
918
|
+
const transfer = new DataTransfer()
|
|
919
|
+
// 拖影:看板按钮默认会把整个按钮当拖影(跟着指针的那个「变电站」框)。这里盯着 setDragImage 的入参,
|
|
920
|
+
// 确认换成了 1×1 的透明元素 —— 屏幕上只剩地图上那个点
|
|
921
|
+
let ghostCall = null
|
|
922
|
+
const setDragImage = transfer.setDragImage
|
|
923
|
+
transfer.setDragImage = (node, x, y) => {
|
|
924
|
+
ghostCall = { node, x, y }
|
|
925
|
+
return setDragImage?.call(transfer, node, x, y)
|
|
926
|
+
}
|
|
927
|
+
const dragItem = (index, x, y) => {
|
|
928
|
+
paletteItems[index].dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: transfer }))
|
|
929
|
+
const init = { bubbles: true, cancelable: true, dataTransfer: transfer, clientX: x, clientY: y }
|
|
930
|
+
canvasNode.dispatchEvent(new DragEvent('dragover', init))
|
|
931
|
+
return init
|
|
932
|
+
}
|
|
933
|
+
// 第 1 项是 230kV 档:规则给 9px 的圆
|
|
934
|
+
const dropInit = {
|
|
935
|
+
bubbles: true,
|
|
936
|
+
cancelable: true,
|
|
937
|
+
dataTransfer: transfer,
|
|
938
|
+
clientX: canvasBox.left + canvasBox.width * 0.3,
|
|
939
|
+
clientY: canvasBox.top + canvasBox.height * 0.35
|
|
940
|
+
}
|
|
941
|
+
dragItem(0, dropInit.clientX, dropInit.clientY)
|
|
942
|
+
const previewShown = await until(() => dragView.getDropPreview(), 4000)
|
|
943
|
+
const previewSize = previewShown?.size ?? null
|
|
944
|
+
const previewSymbol = previewShown?.symbol ?? null
|
|
945
|
+
ghostNode = document.querySelector('.dv-drag-ghost')
|
|
946
|
+
ghostHidden = Boolean(ghostCall?.node) && ghostCall.node === ghostNode
|
|
947
|
+
&& ghostCall.node.getBoundingClientRect().width <= 2
|
|
948
|
+
&& getComputedStyle(ghostCall.node).opacity === '0'
|
|
949
|
+
const readoutPreviewed = await until(() => [...document.querySelectorAll('.dv-demo .dv-readout')].some(node => node.textContent.includes('预览图标')), 4000)
|
|
950
|
+
canvasNode.dispatchEvent(new DragEvent('drop', dropInit))
|
|
951
|
+
const draftShown = await until(() => dragView.getDropPreview(), 4000)
|
|
952
|
+
const untouched = dragView.getData().points.length === pointsBefore
|
|
953
|
+
// 真浏览器在 drop 之后还会给源元素补一条 dragend(拖拽演示里就是看板按钮上绑的那一个)。
|
|
954
|
+
// 这一条要是顺手把预览收掉,「松手先显示」就只剩一眨眼 —— 这里照着补发一条,核对点还在落点上
|
|
955
|
+
const draftWorld = draftShown?.world ?? null
|
|
956
|
+
paletteItems[0].dispatchEvent(new DragEvent('dragend', { bubbles: true, cancelable: true, dataTransfer: transfer }))
|
|
957
|
+
await wait(200)
|
|
958
|
+
const afterDragEnd = dragView.getDropPreview()
|
|
959
|
+
const keptAfterDragEnd = Boolean(afterDragEnd) && Boolean(draftWorld)
|
|
960
|
+
&& Math.abs(afterDragEnd.world[0] - draftWorld[0]) < 1e-6 && Math.abs(afterDragEnd.world[1] - draftWorld[1]) < 1e-6
|
|
961
|
+
// 草稿还挂着的时候又拖一项进来(dragover 会先按新的一项画预览),中途松手没落上:
|
|
962
|
+
// 临时预览收掉、点回到草稿自己的落点上,数据依旧一个字节没动
|
|
963
|
+
const strayInit = { ...dropInit, clientX: dropInit.clientX + 140, clientY: dropInit.clientY + 80 }
|
|
964
|
+
dragItem(4, strayInit.clientX, strayInit.clientY)
|
|
965
|
+
// 预览合并到下一帧才算:等它真的挪到新落点上(还留着草稿的那一份就直接算没挪)
|
|
966
|
+
const strayShown = await until(() => {
|
|
967
|
+
const shown = dragView.getDropPreview()
|
|
968
|
+
return shown && draftWorld && (Math.abs(shown.world[0] - draftWorld[0]) > 1e-6 || Math.abs(shown.world[1] - draftWorld[1]) > 1e-6) ? shown : null
|
|
969
|
+
}, 4000)
|
|
970
|
+
const strayMoved = Boolean(strayShown)
|
|
971
|
+
paletteItems[4].dispatchEvent(new DragEvent('dragend', { bubbles: true, cancelable: true, dataTransfer: transfer }))
|
|
972
|
+
await wait(200)
|
|
973
|
+
const restored = dragView.getDropPreview()
|
|
974
|
+
strayResult = {
|
|
975
|
+
moved: strayMoved,
|
|
976
|
+
restored: Boolean(restored) && Boolean(draftWorld)
|
|
977
|
+
&& Math.abs(restored.world[0] - draftWorld[0]) < 1e-6 && Math.abs(restored.world[1] - draftWorld[1]) < 1e-6,
|
|
978
|
+
untouched: dragView.getData().points.length === pointsBefore
|
|
979
|
+
}
|
|
980
|
+
// 面板上指定属性:改成 525kV(规则给 13px),预览与读数必须跟着重画
|
|
981
|
+
const kvSelect = [...document.querySelectorAll('.dv-demo select')].find(node => node.textContent.includes('525 kV'))
|
|
982
|
+
if (kvSelect) {
|
|
983
|
+
kvSelect.value = '525'
|
|
984
|
+
kvSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
985
|
+
}
|
|
986
|
+
const resized = await until(() => (dragView.getDropPreview()?.size ?? 0) === 13, 4000)
|
|
987
|
+
const specifiedSize = dragView.getDropPreview()?.size ?? null
|
|
988
|
+
check('拖拽落点:dragover 期间组件就解析出落点图标(尺寸 / 符号 / 颜色都有读数)', Boolean(previewShown) && Number(previewSize) > 0 && Boolean(previewSymbol), previewShown ? `${previewSize}px / ${previewSymbol} / ${previewShown.color}` : '没有落点预览')
|
|
989
|
+
check('拖拽落点:预览读数写进面板(不是照着样子仿的)', Boolean(readoutPreviewed), readoutPreviewed ? '读数里出现「预览图标」' : '读数里没有预览尺寸')
|
|
990
|
+
document.querySelector('.dv-demo .dv-confirm .dv-button--primary')?.click()
|
|
991
|
+
const landed = await until(() => dragView.getData().points.length === pointsBefore + 1, 4000)
|
|
992
|
+
const landedItem = landed ? dragView.getData().points.at(-1) : null
|
|
993
|
+
// 落地后的那一份:同一条规则链解析出来的尺寸与符号必须与预览一致,属性就是面板上指定的那一个
|
|
994
|
+
const landedStyle = landed ? dragView.getPointStyle(pointsBefore) : null
|
|
995
|
+
const sameAsPreview = Boolean(landedStyle) && landedStyle.size === specifiedSize && previewSymbol === 'circle' && landedItem?.symbol === 'circle'
|
|
996
|
+
const specified = Boolean(kvSelect) && Number(landedItem?.vn_kv) === 525 && Boolean(resized)
|
|
997
|
+
const cleared = landed ? dragView.getDropPreview() === null : false
|
|
998
|
+
dragResult = { before: pointsBefore, draft: Boolean(draftShown), untouched, keptAfterDragEnd, landed: Boolean(landed), sameAsPreview, specified, size: specifiedSize, cleared, after: dragView.getData().points.length }
|
|
999
|
+
// 第二轮:拖一个进去之后点「取消新增」—— 地图上的点收掉,数据一个字节没动
|
|
1000
|
+
const cancelBefore = dragView.getData().points.length
|
|
1001
|
+
const cancelX = dropInit.clientX + 24
|
|
1002
|
+
const cancelY = dropInit.clientY + 24
|
|
1003
|
+
dragItem(1, cancelX, cancelY)
|
|
1004
|
+
await until(() => dragView.getDropPreview(), 4000)
|
|
1005
|
+
canvasNode.dispatchEvent(new DragEvent('drop', { ...dropInit, clientX: cancelX, clientY: cancelY }))
|
|
1006
|
+
// 这一段同样要过一遍 dragend:取消新增之前,点必须一直留在地图上
|
|
1007
|
+
paletteItems[1].dispatchEvent(new DragEvent('dragend', { bubbles: true, cancelable: true, dataTransfer: transfer }))
|
|
1008
|
+
const cancelShown = await until(() => dragView.getDropPreview(), 4000)
|
|
1009
|
+
const cancelButton = [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.trim() === '取消新增')
|
|
1010
|
+
cancelButton?.click()
|
|
1011
|
+
await wait(200)
|
|
1012
|
+
cancelResult = { shown: Boolean(cancelShown), button: Boolean(cancelButton), cleared: dragView.getDropPreview() === null, untouched: dragView.getData().points.length === cancelBefore }
|
|
1013
|
+
}
|
|
1014
|
+
check('拖拽落点:拖拽期间不出现浏览器默认的看板拖影(只剩地图上的点)', Boolean(ghostNode) && ghostHidden, ghostHidden ? 'setDragImage 指向 1×1 透明节点' : '拖影还是看板按钮本身')
|
|
1015
|
+
check('拖拽落点:松手之后预览不收起,先显示在地图上、数据一个字节没动', Boolean(dragResult?.draft) && Boolean(dragResult?.untouched), dragResult ? `点 ${dragResult.before} -> ${dragResult.after}` : `看板 ${paletteItems.length} 项 / 画布 ${Boolean(dragReady)}`)
|
|
1016
|
+
check('拖拽落点:drop 之后那条 dragend 不会把原来拖动的点收掉(点一直留在落点上)', Boolean(dragResult?.keptAfterDragEnd), dragResult?.keptAfterDragEnd ? 'dragend 之后点还在,世界坐标没挪' : 'dragend 之后预览被收掉了')
|
|
1017
|
+
check('拖拽落点:草稿挂着时又拖一项进来、中途松手 —— 点回到落点,数据不动', Boolean(strayResult?.moved) && Boolean(strayResult?.restored) && Boolean(strayResult?.untouched), `临时预览 ${strayResult?.moved ? '挪走了' : '没挪'} / 回到落点 ${strayResult?.restored ? '是' : '否'} / 数据 ${strayResult?.untouched ? '没动' : '动了'}`)
|
|
1018
|
+
check('拖拽落点:面板指定属性(525kV)之后预览跟着重画', Boolean(dragResult?.size) && dragResult.size === 13, dragResult ? `指定之后 ${dragResult.size}px` : '没有拖放结果')
|
|
1019
|
+
check('拖拽落点:点「按属性新增」才写进数据,写进去的就是指定的那一份属性', Boolean(dragResult?.landed) && Boolean(dragResult?.specified), dragResult ? `确认后 ${dragResult.after} 个节点 / vn_kv=525` : '没有拖放结果')
|
|
1020
|
+
check('拖拽落点:落地后的图标尺寸 / 符号与预览完全一致(规则链同一条)', Boolean(dragResult?.sameAsPreview), dragResult ? `预览与落地 ${dragResult.sameAsPreview ? '一致' : '不一致'}` : '没有拖放结果')
|
|
1021
|
+
check('拖拽落点:新增之后落点预览收掉,不会留在画面上', Boolean(dragResult?.cleared), dragResult?.cleared ? 'getDropPreview() 回到 null' : '预览还在')
|
|
1022
|
+
check('拖拽落点:「取消新增」把点从地图上收掉、数据不动', Boolean(cancelResult?.shown) && Boolean(cancelResult?.button) && Boolean(cancelResult?.cleared) && Boolean(cancelResult?.untouched), cancelResult ? `取消按钮 ${cancelResult.button ? '有' : '没有'} / 预览 ${cancelResult.cleared ? '已收' : '还在'}` : '没有第二轮结果')
|
|
1023
|
+
|
|
1024
|
+
// 2.2 定向改法与存档回灌:微调按钮走 updatePoint,重建之后数量与标签状态都还在
|
|
1025
|
+
let tweakResult = null
|
|
1026
|
+
if (dragResult?.landed) {
|
|
1027
|
+
const tweakView = document.querySelector('.dv-demo').__vueParentComponent.setupState.view
|
|
1028
|
+
const panelButton = text => [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.trim() === text)
|
|
1029
|
+
// 落点节点数据里没写 size(尺寸由规则给的 9px),所以「改之前」按样式快照读 —— 与预览读数同一个数
|
|
1030
|
+
const landedIndex = tweakView.getData().points.length - 1
|
|
1031
|
+
const sizeBefore = Number(tweakView.getData().points.at(-1).size) || Number(tweakView.getPointStyle(landedIndex)?.size)
|
|
1032
|
+
panelButton('大一点')?.click()
|
|
1033
|
+
await wait(260)
|
|
1034
|
+
const sizeAfter = Number(tweakView.getData().points.at(-1).size)
|
|
1035
|
+
panelButton('换图标')?.click()
|
|
1036
|
+
await wait(260)
|
|
1037
|
+
const symbol = tweakView.getData().points.at(-1).symbol
|
|
1038
|
+
// 先拖一条标签上去,让 labelStates 有内容 —— 不然「重建之后标签状态还在」是 0 == 0
|
|
1039
|
+
const labelTransfer = new DataTransfer()
|
|
1040
|
+
paletteItems[6].dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: labelTransfer }))
|
|
1041
|
+
// 2.1 里的画布节点只在那一段的作用域里,这里重新量一次
|
|
1042
|
+
const labelCanvas = document.querySelector('.dv-demo .dv-canvas')
|
|
1043
|
+
const labelBox = labelCanvas.getBoundingClientRect()
|
|
1044
|
+
const labelInit = {
|
|
1045
|
+
bubbles: true,
|
|
1046
|
+
cancelable: true,
|
|
1047
|
+
dataTransfer: labelTransfer,
|
|
1048
|
+
clientX: labelBox.left + labelBox.width * 0.3,
|
|
1049
|
+
clientY: labelBox.top + labelBox.height * 0.35
|
|
1050
|
+
}
|
|
1051
|
+
labelCanvas.dispatchEvent(new DragEvent('dragover', labelInit))
|
|
1052
|
+
labelCanvas.dispatchEvent(new DragEvent('drop', labelInit))
|
|
1053
|
+
await wait(260)
|
|
1054
|
+
document.querySelector('.dv-demo .dv-confirm .dv-button--primary')?.click()
|
|
1055
|
+
await wait(320)
|
|
1056
|
+
const countsBefore = tweakView.getData().points.length
|
|
1057
|
+
const labelsBefore = tweakView.getLabelStates().length
|
|
1058
|
+
panelButton('存档 → 重建')?.click()
|
|
1059
|
+
await wait(900)
|
|
1060
|
+
const rebuiltView = document.querySelector('.dv-demo').__vueParentComponent.setupState.view
|
|
1061
|
+
tweakResult = {
|
|
1062
|
+
sizeBefore,
|
|
1063
|
+
sizeAfter,
|
|
1064
|
+
symbol: typeof symbol === 'string' ? symbol : symbol?.type,
|
|
1065
|
+
countsBefore,
|
|
1066
|
+
countsAfter: rebuiltView.getData().points.length,
|
|
1067
|
+
labelStates: rebuiltView.getLabelStates().length,
|
|
1068
|
+
labelsBefore
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
check('拖拽落点:微调按钮走 updatePoint 写回数据', Boolean(tweakResult) && tweakResult.sizeAfter === tweakResult.sizeBefore + 4 && tweakResult.symbol === 'square', tweakResult ? `size ${tweakResult.sizeBefore} -> ${tweakResult.sizeAfter} / symbol ${tweakResult.symbol}` : '没有可微调的目标')
|
|
1072
|
+
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})` : '没有重建读数')
|
|
1073
|
+
|
|
1074
|
+
// 3. 回到标签演示,逐条验这一轮新增的交互
|
|
1075
|
+
await clickTab('标签交互')
|
|
1059
1076
|
await canvasReady()
|
|
1060
|
-
await until(() =>
|
|
1061
|
-
const
|
|
1062
|
-
const
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1077
|
+
await until(() => document.querySelectorAll('.dv-demo .gis-view__label').length > 0, 15000)
|
|
1078
|
+
const labelNodes = () => [...document.querySelectorAll('.dv-demo .gis-view__label')]
|
|
1079
|
+
const perItemLabelNode = id => document.querySelector(`.dv-demo .gis-view__label[data-label-kind="point"][data-label-label="${id}"]`)
|
|
1080
|
+
const labelView = () => document.querySelector('.dv-demo')?.__vueParentComponent?.setupState?.view ?? null
|
|
1081
|
+
check('标签演示:节点标签都画出来了', labelNodes().length > 0, `${labelNodes().length} 个标签节点`)
|
|
1082
|
+
document.querySelector('[data-demo="label-per-item-focus"]')?.click()
|
|
1083
|
+
const perItemReady = await until(() => {
|
|
1084
|
+
const readout = document.querySelector('[data-demo="label-per-item-readout"]')?.textContent.trim() ?? ''
|
|
1085
|
+
return readout.includes('per-item-draggable') && readout.includes('per-item-locked') && perItemLabelNode('per-item-draggable') && perItemLabelNode('per-item-locked')
|
|
1086
|
+
}, 6000)
|
|
1087
|
+
const editablePerItem = perItemLabelNode('per-item-draggable')
|
|
1088
|
+
const lockedPerItem = perItemLabelNode('per-item-locked')
|
|
1089
|
+
check('标签演示:逐条配置 A 可拖可点、B 只读(false 覆盖全局开关)',
|
|
1090
|
+
Boolean(perItemReady) &&
|
|
1091
|
+
editablePerItem?.classList.contains('gis-view__label--draggable') === true &&
|
|
1092
|
+
editablePerItem?.classList.contains('gis-view__label--editable') === true &&
|
|
1093
|
+
lockedPerItem?.classList.contains('gis-view__label--draggable') === false &&
|
|
1094
|
+
lockedPerItem?.classList.contains('gis-view__label--editable') === false &&
|
|
1095
|
+
getComputedStyle(lockedPerItem).pointerEvents === 'none',
|
|
1096
|
+
`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() ?? ''}`)
|
|
1097
|
+
// 3.0 一键换标签内容:setLabelContent 只换全局 label / lineLabel 的 content(节点 + 线路一起)
|
|
1098
|
+
const overlapHideButton = document.querySelector('[data-demo="label-overlap-hide"]')
|
|
1099
|
+
overlapHideButton?.click()
|
|
1100
|
+
await wait(220)
|
|
1101
|
+
const hideOverlapOptions = labelView()?.getOptions()
|
|
1102
|
+
check('标签演示:「碰撞直接隐藏」把节点 / 线路标签一起切到 overlapMode: hide',
|
|
1103
|
+
hideOverlapOptions?.label?.overlapMode === 'hide' && hideOverlapOptions?.lineLabel?.overlapMode === 'hide' &&
|
|
1104
|
+
overlapHideButton?.classList.contains('dv-chip--active') === true,
|
|
1105
|
+
`${hideOverlapOptions?.label?.overlapMode} / ${hideOverlapOptions?.lineLabel?.overlapMode}`)
|
|
1106
|
+
const overlapShiftButton = document.querySelector('[data-demo="label-overlap-shift"]')
|
|
1107
|
+
overlapShiftButton?.click()
|
|
1108
|
+
await wait(220)
|
|
1109
|
+
const shiftOverlapOptions = labelView()?.getOptions()
|
|
1110
|
+
check('标签演示:「主动挪标签」切回 overlapMode: shift(默认避让策略)',
|
|
1111
|
+
shiftOverlapOptions?.label?.overlapMode === 'shift' && shiftOverlapOptions?.lineLabel?.overlapMode === 'shift' &&
|
|
1112
|
+
overlapShiftButton?.classList.contains('dv-chip--active') === true,
|
|
1113
|
+
`${shiftOverlapOptions?.label?.overlapMode} / ${shiftOverlapOptions?.lineLabel?.overlapMode}`)
|
|
1114
|
+
const contentSelect = document.querySelector('[data-demo=label-content-field]')
|
|
1115
|
+
contentSelect.value = '{vn_kv} kV'
|
|
1116
|
+
contentSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1117
|
+
await wait(500)
|
|
1118
|
+
const optionsAfter = labelView()?.getOptions() ?? null
|
|
1119
|
+
const voltageShown = [...document.querySelectorAll('.dv-demo .gis-view__label')].some(node => node.textContent.includes('kV'))
|
|
1120
|
+
const contentReadout = document.querySelector('[data-demo=label-content-text]')?.textContent.trim() ?? ''
|
|
1121
|
+
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`)
|
|
1122
|
+
contentSelect.value = '{name}'
|
|
1123
|
+
contentSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1124
|
+
await wait(300)
|
|
1125
|
+
|
|
1126
|
+
// 3.1 点一下标签 → labelClick
|
|
1127
|
+
const target = labelNodes().find(node => node.classList.contains('gis-view__label--editable')) ?? labelNodes()[0]
|
|
1128
|
+
tap(target)
|
|
1129
|
+
const clickLogged = await until(() => logText().includes('labelClick'), 8000)
|
|
1130
|
+
check('点一下标签发 labelClick,日志记下「哪一条、内容是什么」', Boolean(clickLogged), logText().slice(0, 160))
|
|
1131
|
+
const editReadout = document.querySelector('.dv-demo .dv-input')?.closest('.dv-group')?.querySelector('.dv-readout')?.textContent?.trim() ?? ''
|
|
1132
|
+
check('点完之后编辑面板认到了这一条', !editReadout.includes('点一下标签') && editReadout.length > 0, editReadout)
|
|
1133
|
+
|
|
1134
|
+
// 3.2 右键标签 → 自绘菜单(编辑 / 换位置 / 重置 / 隐藏 / 删除)
|
|
1135
|
+
const rect = target.getBoundingClientRect()
|
|
1136
|
+
const menuEvent = contextMenuOn(target, rect.left + 4, rect.top + 4)
|
|
1137
|
+
check('右键标签的系统菜单被拦下(preventContextMenu 缺省开)', menuEvent.defaultPrevented, `defaultPrevented=${menuEvent.defaultPrevented}`)
|
|
1138
|
+
const menu = await until(() => document.querySelector('.dv-menu'), 6000)
|
|
1139
|
+
check('右键弹出演示自己的菜单', Boolean(menu), menu ? [...menu.querySelectorAll('button')].map(node => node.textContent.trim()).join(' / ') : '没弹出')
|
|
1140
|
+
const items = menu ? [...menu.querySelectorAll('button')].map(node => node.textContent.trim()) : []
|
|
1141
|
+
check('菜单里有改内容 / 换位置 / 重置偏移 / 隐藏 / 删除这五项', ['改内容', '换位置', '重置偏移', '删除这一条'].every(text => items.some(item => item.includes(text))), items.join(' / '))
|
|
1142
|
+
const positionButton = menu ? [...menu.querySelectorAll('button')].find(node => node.textContent.includes('换位置')) : null
|
|
1143
|
+
positionButton?.click()
|
|
1144
|
+
await wait(260)
|
|
1145
|
+
check('菜单「换位置」落到 updateLabel 上(日志看得到)', logText().includes('updateLabel'), logText().slice(0, 160))
|
|
1146
|
+
check('点完菜单项之后菜单收起来了', !document.querySelector('.dv-menu'), document.querySelector('.dv-menu') ? '还开着' : '已收')
|
|
1147
|
+
// 3.3 删除一条:自绘菜单的删除落到 removeLabel / updateLabel(hidden)
|
|
1148
|
+
const beforeDelete = labelNodes()
|
|
1149
|
+
const labelForDelete = beforeDelete[beforeDelete.length - 1]
|
|
1150
|
+
// 按内容认「删的是哪一条」:避让是「没位置就不画」,删掉一条腾出来的小格会让别的候选补位,
|
|
1151
|
+
// 所以这里盯的是「被删的那一条不再画」,不是「总数不涨」(13 → 14 是正常的补位)
|
|
1152
|
+
const deletedLabelText = labelForDelete.textContent.trim()
|
|
1153
|
+
contextMenuOn(labelForDelete, labelForDelete.getBoundingClientRect().left + 4, labelForDelete.getBoundingClientRect().top + 4)
|
|
1154
|
+
const deleteMenu = await until(() => document.querySelector('.dv-menu'), 6000)
|
|
1155
|
+
const deleteButton = deleteMenu ? [...deleteMenu.querySelectorAll('button')].find(node => node.textContent.includes('删除这一条')) : null
|
|
1156
|
+
deleteButton?.click()
|
|
1157
|
+
await wait(400)
|
|
1158
|
+
check('菜单「删除这一条」记到了日志里', logText().includes('removeLabel') || logText().includes('updateLabel'), logText().slice(0, 160))
|
|
1159
|
+
const remainingLabels = labelNodes()
|
|
1160
|
+
check('删完之后被删的那一条不再画(腾出的位置允许别的标签补位)', deletedLabelText !== '' && !remainingLabels.some(node => node.textContent.trim() === deletedLabelText), `${deletedLabelText}|${beforeDelete.length} → ${remainingLabels.length}`)
|
|
1161
|
+
|
|
1162
|
+
// 3.4 批量 API:给全部节点加一条
|
|
1163
|
+
const addAllButton = [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.includes('给全部加一条'))
|
|
1164
|
+
addAllButton?.click()
|
|
1165
|
+
await wait(500)
|
|
1166
|
+
check('「给全部加一条」走 addLabels,日志里有新增计数', logText().includes('addLabels'), logText().slice(0, 160))
|
|
1167
|
+
check('labelChange 也跟着发(addAll)', logText().includes('addAll'), logText().slice(0, 200))
|
|
1168
|
+
|
|
1169
|
+
// 3.5 点选即加:先选中第一个节点,再让「给选中加一条」把它加上
|
|
1170
|
+
const selectButton = [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.includes('选中第一个节点'))
|
|
1171
|
+
selectButton?.click()
|
|
1172
|
+
await wait(400)
|
|
1173
|
+
const selectionReadout = [...document.querySelectorAll('.dv-demo .dv-readout')].map(node => node.textContent.trim()).find(text => text.includes('选中'))
|
|
1174
|
+
check('「选中第一个节点」让面板报出选中集', Boolean(selectionReadout) && !selectionReadout.includes('未选中'), selectionReadout ?? '没有选中读数')
|
|
1175
|
+
const preview = [...document.querySelectorAll('.dv-demo .dv-hint')].map(node => node.textContent.trim()).find(text => text.includes('本次会加到'))
|
|
1176
|
+
check('选中集预览用 resolveLabelTargets 算出「会加到几条」', Boolean(preview), preview ?? '没有预览读数')
|
|
1177
|
+
const addSelectionButton = [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.includes('给选中加一条'))
|
|
1178
|
+
check('「给选中加一条」选中集非空时可用', Boolean(addSelectionButton) && !addSelectionButton.disabled, addSelectionButton ? `disabled=${addSelectionButton.disabled}` : '没找到按钮')
|
|
1179
|
+
addSelectionButton?.click()
|
|
1180
|
+
await wait(500)
|
|
1181
|
+
check('给选中集加标签走 addLabels(..., \'selection\', ...)', logText().includes('选中集:新增') || logText().includes('addLabels'), logText().slice(0, 200))
|
|
1182
|
+
// 3.5.1 连点两次:加出来的标签默认要跟别的标签避让,压在一起就整条不画 ——
|
|
1183
|
+
// 面板上看着像「只有第一次加上了」,这里盯住「每一次都多画一条」
|
|
1184
|
+
const noteCount = () => [...document.querySelectorAll('.dv-demo .gis-view__label')].filter(node => node.textContent.includes('备注#')).length
|
|
1185
|
+
const notesBefore = noteCount()
|
|
1186
|
+
addSelectionButton?.click()
|
|
1187
|
+
await wait(400)
|
|
1188
|
+
addSelectionButton?.click()
|
|
1189
|
+
await wait(400)
|
|
1190
|
+
const notesAfter = noteCount()
|
|
1191
|
+
check('连点「给选中加一条」每次都能多画出一条(不只第一次有效)', notesAfter >= notesBefore + 2, `备注标签 ${notesBefore} → ${notesAfter}`)
|
|
1192
|
+
|
|
1193
|
+
// 3.6 线路标签 + 整条通道:切到线路标签再加
|
|
1194
|
+
const lineTab = [...document.querySelectorAll('.dv-demo .dv-chip')].find(node => node.textContent.includes('线路标签'))
|
|
1195
|
+
lineTab?.click()
|
|
1196
|
+
await wait(200)
|
|
1197
|
+
const channelButton = [...document.querySelectorAll('.dv-demo .dv-button')].find(node => node.textContent.includes('整条通道加'))
|
|
1198
|
+
channelButton?.click()
|
|
1199
|
+
await wait(500)
|
|
1200
|
+
check('「整条通道加」用 { channel } 一次给整条通道的回线加标签', logText().includes('通道'), logText().slice(0, 200))
|
|
1201
|
+
|
|
1202
|
+
// 3.7 双击标签:重置偏移
|
|
1203
|
+
const dblTarget = labelNodes()[0]
|
|
1204
|
+
if (dblTarget) {
|
|
1205
|
+
const dblInit = { bubbles: true, cancelable: true, composed: true, clientX: dblTarget.getBoundingClientRect().left + 2, clientY: dblTarget.getBoundingClientRect().top + 2 }
|
|
1206
|
+
dblTarget.dispatchEvent(new MouseEvent('dblclick', dblInit))
|
|
1207
|
+
await wait(300)
|
|
1208
|
+
check('双击标签发 labelDblClick(这里接成重置偏移)', logText().includes('labelDblClick') || logText().includes('重置偏移'), logText().slice(0, 160))
|
|
1209
|
+
}
|
|
1210
|
+
else {
|
|
1211
|
+
check('双击标签发 labelDblClick(这里接成重置偏移)', false, '没有标签节点可点')
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// 3.8 标签显示:动态切节点 / 线路标签内容(setLabelContent 的独立演示)
|
|
1215
|
+
if (await clickTab('标签显示')) {
|
|
1216
|
+
const ready = await canvasReady()
|
|
1217
|
+
const demo = await until(() => {
|
|
1218
|
+
const node = document.querySelector('.dv-demo')
|
|
1219
|
+
return node?.textContent.includes('改哪一类标签') ? node : null
|
|
1220
|
+
}, 10000)
|
|
1221
|
+
const setup = demo?.__vueParentComponent?.setupState
|
|
1222
|
+
const view = await until(() => setup?.view, 8000)
|
|
1223
|
+
const countLayer = layer => demo?.querySelectorAll(`.gis-view__label-layer--${layer} .gis-view__label`).length ?? 0
|
|
1224
|
+
const labelsShown = await until(() => countLayer('point') > 0 && countLayer('line') > 0, 12000)
|
|
1225
|
+
const click = key => demo?.querySelector(`[data-demo="${key}"]`)?.click()
|
|
1226
|
+
const readout = key => demo?.querySelector(`[data-demo="${key}"]`)?.textContent.trim() ?? ''
|
|
1227
|
+
if (ready && demo && view) {
|
|
1228
|
+
const initial = view.getOptions()
|
|
1229
|
+
const countReady = await until(() => readout('label-display-count').includes('画面上的标签'), 5000)
|
|
1230
|
+
check('标签显示:节点与线路标签都画出来,初始内容由 label / lineLabel 的同一套模板控制',
|
|
1231
|
+
Boolean(labelsShown) && Boolean(countReady) && initial.label?.content === '{name} · {vn_kv} kV' && initial.lineLabel?.content === '{name} · {vn_kv} kV',
|
|
1232
|
+
`节点 ${countLayer('point')} / 线路 ${countLayer('line')} · ${readout('label-display-count')}`)
|
|
1233
|
+
|
|
1234
|
+
click('label-display-target-point')
|
|
1235
|
+
await wait(80)
|
|
1236
|
+
click('label-display-preset-voltage')
|
|
1237
|
+
const pointOnly = await until(() => {
|
|
1238
|
+
const options = view.getOptions()
|
|
1239
|
+
return options.label?.content === '{vn_kv} kV' && options.lineLabel?.content === '{name} · {vn_kv} kV'
|
|
1240
|
+
}, 5000)
|
|
1241
|
+
check('标签显示:只改节点时 setLabelContent 的 kind=point 不影响线路标签', Boolean(pointOnly), readout('label-display-current'))
|
|
1242
|
+
|
|
1243
|
+
click('label-display-target-both')
|
|
1244
|
+
await wait(80)
|
|
1245
|
+
click('label-display-preset-name-voltage')
|
|
1246
|
+
const bothChanged = await until(() => {
|
|
1247
|
+
const options = view.getOptions()
|
|
1248
|
+
return options.label?.content === '{name} · {vn_kv} kV' && options.lineLabel?.content === '{name} · {vn_kv} kV'
|
|
1249
|
+
}, 5000)
|
|
1250
|
+
check('标签显示:切回节点 + 线路后,一份模板同时落到两类标签上', Boolean(bothChanged) && readout('label-display-action').includes('节点 + 线路'), readout('label-display-action'))
|
|
1251
|
+
|
|
1252
|
+
const customInput = demo.querySelector('[data-demo="label-display-custom-input"]')
|
|
1253
|
+
const customText = '自定义 {name} · {vn_kv}'
|
|
1254
|
+
if (customInput) {
|
|
1255
|
+
customInput.value = customText
|
|
1256
|
+
customInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
1257
|
+
}
|
|
1258
|
+
click('label-display-apply-custom')
|
|
1259
|
+
const customApplied = await until(() => {
|
|
1260
|
+
const options = view.getOptions()
|
|
1261
|
+
return options.label?.content === customText && options.lineLabel?.content === customText
|
|
1262
|
+
}, 5000)
|
|
1263
|
+
check('标签显示:自定义模板原样写进节点与线路的 label.content', Boolean(customApplied), readout('label-display-current'))
|
|
1264
|
+
|
|
1265
|
+
click('label-display-auto')
|
|
1266
|
+
const autoStarted = await until(() => readout('label-display-action').includes('自动轮播开始'), 3000)
|
|
1267
|
+
const autoRotated = await until(() => view.getOptions().label?.content !== customText, 4500)
|
|
1268
|
+
click('label-display-auto')
|
|
1269
|
+
const autoStopped = await until(() => readout('label-display-action').includes('已停止'), 3000)
|
|
1270
|
+
check('标签显示:自动轮播按 1.8 秒换模板,按钮再点一次能停', Boolean(autoStarted) && Boolean(autoRotated) && Boolean(autoStopped), `${readout('label-display-action')} / 当前 ${view.getOptions().label?.content}`)
|
|
1271
|
+
}
|
|
1272
|
+
else {
|
|
1273
|
+
check('标签显示:演示页与实例都起来了', false, ready ? '没有 demo / setupState.view' : '没有 canvas')
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// 3.9 悬停与提示:hover 样式、三种 tooltip 内容写法、悬停中刷新内容
|
|
1278
|
+
if (await clickTab('悬停与提示')) {
|
|
1279
|
+
const ready = await canvasReady()
|
|
1280
|
+
const demo = await until(() => {
|
|
1281
|
+
const node = document.querySelector('.dv-demo')
|
|
1282
|
+
return node?.textContent.includes('悬停样式') && node.textContent.includes('提示内容') ? node : null
|
|
1283
|
+
}, 10000)
|
|
1284
|
+
const setup = demo?.__vueParentComponent?.setupState
|
|
1285
|
+
const view = await until(() => setup?.view, 8000)
|
|
1286
|
+
await wait(260)
|
|
1287
|
+
const click = key => demo?.querySelector(`[data-demo="${key}"]`)?.click()
|
|
1288
|
+
const readout = key => demo?.querySelector(`[data-demo="${key}"]`)?.textContent.trim() ?? ''
|
|
1289
|
+
const tooltipNode = () => demo?.querySelector('.gis-view__tooltip') ?? null
|
|
1290
|
+
const tooltipContent = () => {
|
|
1291
|
+
const node = tooltipNode()
|
|
1292
|
+
return node && !node.hidden ? node.textContent.trim() : ''
|
|
1293
|
+
}
|
|
1294
|
+
if (ready && demo && view) {
|
|
1295
|
+
click('hover-first-point')
|
|
1296
|
+
const pointHover = await until(() => (tooltipContent().includes('节点') && readout('hover-readout').includes('point') ? tooltipContent() : null), 6000)
|
|
1297
|
+
check('悬停与提示:hoverAt 命中节点后套用 pointHoverStyle,并弹出节点 tooltip', Boolean(pointHover), `${readout('hover-readout')}|${readout('tooltip-readout')}`)
|
|
1298
|
+
|
|
1299
|
+
const versionBefore = Number(setup.tooltipVersion)
|
|
1300
|
+
click('tooltip-version')
|
|
1301
|
+
const refreshed = await until(() => (tooltipContent().includes(`v${versionBefore + 1}`) ? tooltipContent() : null), 5000)
|
|
1302
|
+
check('悬停与提示:悬停期间 updateOptions({ tooltip }) 立即刷新当前提示(内容版本号变化)', Boolean(refreshed), `${versionBefore} → ${readout('tooltip-readout')}`)
|
|
1303
|
+
|
|
1304
|
+
setup.hoverStyle.pointColor = '#00ff88'
|
|
1305
|
+
setup.hoverStyle.pointSizeScale = 2.4
|
|
1306
|
+
setup.hoverStyle.pointStrokeColor = '#112233'
|
|
1307
|
+
setup.hoverStyle.pointStrokeWidth = 3.5
|
|
1308
|
+
setup.hoverStyle.lineColor = '#00aaff'
|
|
1309
|
+
setup.hoverStyle.lineWidth = 6
|
|
1310
|
+
click('hover-style-apply')
|
|
1311
|
+
await wait(150)
|
|
1312
|
+
const styled = await until(() => {
|
|
1313
|
+
const options = view.getOptions()
|
|
1314
|
+
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
|
|
1315
|
+
}, 5000)
|
|
1316
|
+
check('悬停与提示:节点 / 线路悬停样式都能现场改并写回 options', Boolean(styled) && readout('hover-style-readout').includes('#00ff88') && readout('hover-style-readout').includes('6px'), readout('hover-style-readout'))
|
|
1317
|
+
|
|
1318
|
+
click('hover-first-line')
|
|
1319
|
+
const lineHover = await until(() => (tooltipContent().includes('线路') && readout('hover-readout').includes('line #') ? tooltipContent() : null), 6000)
|
|
1320
|
+
check('悬停与提示:独立线路命中后走 lineHoverStyle / tooltip.line', Boolean(lineHover), `${readout('hover-readout')}|${readout('tooltip-readout')}`)
|
|
1321
|
+
|
|
1322
|
+
click('hover-first-channel')
|
|
1323
|
+
const channelHover = await until(() => (tooltipContent().includes('通道') && readout('hover-readout').includes('通道') ? tooltipContent() : null), 6000)
|
|
1324
|
+
check('悬停与提示:通道回线命中后显示 channel 内容并带 channelScope 读数', Boolean(channelHover), `${readout('hover-readout')}|${readout('tooltip-readout')}`)
|
|
1325
|
+
|
|
1326
|
+
const setSelect = (key, value) => {
|
|
1327
|
+
const node = demo.querySelector(`[data-demo="${key}"]`)
|
|
1328
|
+
if (!node) {
|
|
1329
|
+
return
|
|
1330
|
+
}
|
|
1331
|
+
node.value = value
|
|
1332
|
+
node.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1333
|
+
}
|
|
1334
|
+
setSelect('tooltip-content-mode', 'function')
|
|
1335
|
+
const functionMode = await until(() => (typeof view.getOptions().tooltip?.point === 'function' && tooltipContent().includes('函数') ? tooltipContent() : null), 5000)
|
|
1336
|
+
check('悬停与提示:内容模式切到函数后,tooltip 用命中载荷当场算出文本', Boolean(functionMode), readout('tooltip-config-readout'))
|
|
1337
|
+
|
|
1338
|
+
setSelect('tooltip-content-mode', 'element')
|
|
1339
|
+
const elementMode = await until(() => {
|
|
1340
|
+
const node = tooltipNode()
|
|
1341
|
+
return typeof view.getOptions().tooltip?.point === 'function' && node && !node.hidden && node.querySelector('strong') && node.textContent.includes('DOM 元素')
|
|
1342
|
+
}, 5000)
|
|
1343
|
+
check('悬停与提示:内容模式切到 DOM 元素后,函数返回的节点被直接插进提示框', Boolean(elementMode), readout('tooltip-readout'))
|
|
1344
|
+
|
|
1345
|
+
click('tooltip-html-preset')
|
|
1346
|
+
const htmlMode = await until(() => {
|
|
1347
|
+
const node = tooltipNode()
|
|
1348
|
+
return view.getOptions().tooltip?.html === true && node && !node.hidden && node.innerHTML.includes('<b>')
|
|
1349
|
+
}, 5000)
|
|
1350
|
+
check('悬停与提示:html=true 时模板里的 <b> / <br> 按 HTML 渲染', Boolean(htmlMode), readout('tooltip-config-readout'))
|
|
1351
|
+
|
|
1352
|
+
setSelect('tooltip-channel-scope', 'line')
|
|
1353
|
+
const scopeChanged = await until(() => view.getOptions().tooltip?.channelScope === 'line' && readout('tooltip-config-readout').includes('channelScope=line'), 5000)
|
|
1354
|
+
check('悬停与提示:channelScope 可现场切换(channel / group / line)', Boolean(scopeChanged), readout('tooltip-config-readout'))
|
|
1355
|
+
|
|
1356
|
+
click('hover-clear')
|
|
1357
|
+
const cleared = await until(() => (!tooltipNode() || tooltipNode().hidden) && readout('hover-readout').includes('已离开'), 5000)
|
|
1358
|
+
check('悬停与提示:clearHover 收起提示并撤销悬停高亮', Boolean(cleared), readout('hover-readout'))
|
|
1359
|
+
}
|
|
1360
|
+
else {
|
|
1361
|
+
check('悬停与提示:演示页与实例都起来了', false, ready ? '没有 demo / setupState.view' : '没有 canvas')
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// 4. 事件与方法:拖到一起进「待确认合并」、Alt 点击停在预览态的复制 —— 两个弹框都在演示里裁决
|
|
1366
|
+
await clickTab('事件与方法')
|
|
1367
|
+
await canvasReady()
|
|
1368
|
+
await until(() => [...document.querySelectorAll('.dv-demo .dv-group h3')].some(node => node.textContent.includes('光标读数')), 8000)
|
|
1369
|
+
const eventDemo = document.querySelector('.dv-demo')
|
|
1370
|
+
const eventSetup = eventDemo.__vueParentComponent.setupState
|
|
1371
|
+
const eventView = await until(() => eventSetup.view, 8000)
|
|
1372
|
+
const eventCanvas = eventDemo.querySelector('canvas.gis-view__canvas')
|
|
1373
|
+
if (eventView && eventCanvas) {
|
|
1374
|
+
const eventBox = eventCanvas.getBoundingClientRect()
|
|
1375
|
+
// 合成指针:事件挂在 canvas 上,坐标是画布内的 CSS 像素(与 selftest-features 的 cmSend 同一套)
|
|
1376
|
+
const eventSend = (type, screen, extra = {}) => {
|
|
1377
|
+
eventCanvas.dispatchEvent(new PointerEvent(type, {
|
|
1378
|
+
bubbles: true,
|
|
1379
|
+
cancelable: true,
|
|
1380
|
+
composed: true,
|
|
1381
|
+
pointerId: 9,
|
|
1382
|
+
pointerType: 'mouse',
|
|
1383
|
+
isPrimary: true,
|
|
1384
|
+
button: extra.button ?? 0,
|
|
1385
|
+
buttons: extra.buttons ?? (type === 'pointerup' ? 0 : 1),
|
|
1386
|
+
clientX: eventBox.left + screen[0],
|
|
1387
|
+
clientY: eventBox.top + screen[1],
|
|
1388
|
+
...extra
|
|
1389
|
+
}))
|
|
1390
|
+
}
|
|
1391
|
+
const eventDrag = async (from, to) => {
|
|
1392
|
+
eventSend('pointermove', from, { buttons: 0 })
|
|
1393
|
+
eventSend('pointerdown', from, { buttons: 1 })
|
|
1394
|
+
eventSend('pointermove', [(from[0] + to[0]) / 2, (from[1] + to[1]) / 2], { buttons: 1 })
|
|
1395
|
+
eventSend('pointermove', to, { buttons: 1 })
|
|
1396
|
+
eventSend('pointerup', to, { buttons: 0 })
|
|
1397
|
+
await wait(140)
|
|
1398
|
+
}
|
|
1399
|
+
const eventPoints = eventView.getData().points
|
|
1400
|
+
const eventSource = eventView.project(eventPoints[0].x, eventPoints[0].y)
|
|
1401
|
+
const eventTarget = eventView.project(eventPoints[1].x, eventPoints[1].y)
|
|
1402
|
+
const pointsBeforeMerge = eventPoints.length
|
|
1403
|
+
await eventDrag(eventSource, eventTarget)
|
|
1404
|
+
const mergeBar = await until(() => eventDemo.querySelector('.dv-confirm'), 6000)
|
|
1405
|
+
const mergeText = mergeBar ? mergeBar.textContent.trim() : ''
|
|
1406
|
+
const mergeCursor = eventCanvas.style.cursor
|
|
1407
|
+
check('事件与方法:拖到一起进入「待确认合并」,数据先不动', Boolean(mergeBar) && eventSetup.mergeState?.phase === 'pending' && eventView.getData().points.length === pointsBeforeMerge, mergeBar ? 'phase=' + eventSetup.mergeState?.phase + ' 光标=' + mergeCursor : '没有确认条')
|
|
1408
|
+
check('事件与方法:待确认合并的光标切到 copy 档(cursorStyle.copy)', mergeCursor.includes('copy'), '光标=' + mergeCursor)
|
|
1409
|
+
check('事件与方法:确认条报出「来源 → 目标」', mergeText.includes('合并待确认') && mergeText.includes('→'), mergeText.slice(0, 120))
|
|
1410
|
+
mergeBar?.querySelector('.dv-button--primary')?.click()
|
|
1411
|
+
const mergedCount = await until(() => (eventView.getData().points.length === pointsBeforeMerge - 1 ? eventView.getData().points.length : null), 6000)
|
|
1412
|
+
check('事件与方法:确认条「确认合并」= payload.confirm(),两个节点并成一个', mergedCount === pointsBeforeMerge - 1 && !eventDemo.querySelector('.dv-confirm'), pointsBeforeMerge + ' → ' + eventView.getData().points.length)
|
|
1413
|
+
// 复制:Alt 点击出预览 → 跟着鼠标走 → **松开左键**才结束(中途不弹框)→ 收尾条给一次撤销
|
|
1414
|
+
const copyFrom = eventView.project(eventView.getData().points[0].x, eventView.getData().points[0].y)
|
|
1415
|
+
const copyTo = [copyFrom[0] + 70, copyFrom[1] + 46]
|
|
1416
|
+
const pointsBeforeCopy = eventView.getData().points.length
|
|
1417
|
+
eventSend('pointermove', copyFrom, { buttons: 0 })
|
|
1418
|
+
eventSend('pointerdown', copyFrom, { altKey: true })
|
|
1419
|
+
eventSend('pointermove', copyTo, { buttons: 0, altKey: true })
|
|
1420
|
+
await wait(100)
|
|
1421
|
+
const copyCursor = eventCanvas.style.cursor
|
|
1422
|
+
check('事件与方法:复制中途不弹框(松开左键才算结束)', !eventDemo.querySelector('.dv-confirm') && eventView.getPointCopy()?.phase === 'preview' && eventView.getData().points.length === pointsBeforeCopy, `phase=${eventView.getPointCopy()?.phase} points=${eventView.getData().points.length}`)
|
|
1423
|
+
// 松开 Alt 只是不再算复制修饰键:预览照旧跟着鼠标,抬起左键那一下才落地
|
|
1424
|
+
window.dispatchEvent(new KeyboardEvent('keyup', { key: 'Alt', altKey: false, bubbles: true }))
|
|
1425
|
+
await wait(60)
|
|
1426
|
+
check('事件与方法:松开 Alt 不落地(旧口径就是在这里落地的)', eventView.getPointCopy()?.phase === 'preview' && eventView.getData().points.length === pointsBeforeCopy, `phase=${eventView.getPointCopy()?.phase}`)
|
|
1427
|
+
eventSend('pointerup', copyTo, { buttons: 0, altKey: true })
|
|
1428
|
+
const copiedCount = await until(() => (eventView.getData().points.length === pointsBeforeCopy + 1 ? eventView.getData().points.length : null), 6000)
|
|
1429
|
+
const copyBar = await until(() => eventDemo.querySelector('.dv-confirm'), 6000)
|
|
1430
|
+
const copyText = copyBar ? copyBar.textContent.trim() : ''
|
|
1431
|
+
check('事件与方法:复制预览跟着鼠标走,光标同样是 copy 档', copyCursor.includes('copy'), '光标=' + copyCursor)
|
|
1432
|
+
check('事件与方法:松开左键落地(复制结束),收尾条这时才弹出来', copiedCount === pointsBeforeCopy + 1 && Boolean(copyBar) && copyText.includes('复制结束') && eventView.getPointCopy() === null, `points=${pointsBeforeCopy} → ${eventView.getData().points.length} · ${copyText.slice(0, 90)}`)
|
|
1433
|
+
// 收尾条上的「撤销」= end 阶段载荷的 payload.revert():已经写进去的复制品整份收回去
|
|
1434
|
+
copyBar?.querySelector('.dv-button--primary')?.click()
|
|
1435
|
+
const revertedCount = await until(() => (eventView.getData().points.length === pointsBeforeCopy ? eventView.getData().points.length : null), 6000)
|
|
1436
|
+
check('事件与方法:收尾条「撤销」= payload.revert(),复制品整份收回去', revertedCount === pointsBeforeCopy && !eventDemo.querySelector('.dv-confirm'), `points=${pointsBeforeCopy} → ${eventView.getData().points.length}`)
|
|
1437
|
+
// 「知道了」只关掉条:复制已经落地,复制品留着
|
|
1438
|
+
eventSend('pointerdown', copyFrom, { altKey: true })
|
|
1439
|
+
eventSend('pointermove', copyTo, { buttons: 0, altKey: true })
|
|
1440
|
+
eventSend('pointerup', copyTo, { buttons: 0, altKey: true })
|
|
1441
|
+
await wait(140)
|
|
1442
|
+
const keptBar = await until(() => eventDemo.querySelector('.dv-confirm'), 6000)
|
|
1443
|
+
keptBar?.querySelector('.dv-button:not(.dv-button--primary)')?.click()
|
|
1444
|
+
await wait(160)
|
|
1445
|
+
check('事件与方法:收尾条「知道了」只关框,复制品留着', !eventDemo.querySelector('.dv-confirm') && eventView.getData().points.length === pointsBeforeCopy + 1, 'points=' + eventView.getData().points.length)
|
|
1446
|
+
}
|
|
1447
|
+
else {
|
|
1448
|
+
check('事件与方法:画布与视图都起来了', false, '没等到 canvas 或 view')
|
|
1449
|
+
}
|
|
1450
|
+
// 5. 通道与数据:右键一条通道回线弹出交互框(分组列表 / 线路分配 / runChannelAction 五个动作)
|
|
1451
|
+
await clickTab('通道与数据')
|
|
1452
|
+
await canvasReady()
|
|
1453
|
+
await until(() => [...document.querySelectorAll('.dv-demo .dv-group h3')].some(node => node.textContent.includes('右键交互框')), 8000)
|
|
1454
|
+
const channelDemo = document.querySelector('.dv-demo')
|
|
1455
|
+
const channelSetup = channelDemo.__vueParentComponent.setupState
|
|
1456
|
+
const channelView = await until(() => channelSetup.view, 8000)
|
|
1457
|
+
await until(() => (channelView?.getChannels() ?? []).length > 0, 8000)
|
|
1065
1458
|
const channelField = key => channelDemo.querySelector(`[data-demo="${key}"]`)?.textContent.trim() ?? ''
|
|
1066
1459
|
const channelCanvas = channelDemo.querySelector('canvas.gis-view__canvas')
|
|
1067
1460
|
if (channelView && channelCanvas) {
|
|
1461
|
+
const visibilityChannelId = channelView.getOptions().channels?.[0]?.id
|
|
1462
|
+
const groupsBeforeHide = channelView.getChannelGroups(visibilityChannelId).length
|
|
1463
|
+
const membersBeforeHide = channelView.getChannel(visibilityChannelId)?.lineIndexes?.length ?? 0
|
|
1464
|
+
channelView.clearChangedChannels()
|
|
1465
|
+
channelDemo.querySelector('[data-demo="channel-hide"]')?.click()
|
|
1466
|
+
const channelHidden = await until(() => {
|
|
1467
|
+
const declared = channelView.getOptions().channels?.find(channel => String(channel.id) === String(visibilityChannelId))
|
|
1468
|
+
return declared?.visible === false && channelView.getChannel(visibilityChannelId)?.lineIndexes?.length === 0 && channelField('channel-visibility').includes('隐藏')
|
|
1469
|
+
}, 5000)
|
|
1470
|
+
const hiddenChange = channelView.getChangedChannels().find(channel => String(channel.id) === String(visibilityChannelId))
|
|
1471
|
+
check('通道与数据:隐藏第一条通道只改 visible,成员回线一起退场且最近变化记可见性',
|
|
1472
|
+
Boolean(channelHidden) && hiddenChange?.visibilityChanged === true && hiddenChange?.reasons.includes('visibility') === true && channelField('channel-changed').includes('可见性'),
|
|
1473
|
+
`${channelField('channel-visibility')} / ${channelField('channel-changed')}`)
|
|
1474
|
+
channelDemo.querySelector('[data-demo="channel-show"]')?.click()
|
|
1475
|
+
const channelShown = await until(() => {
|
|
1476
|
+
const declared = channelView.getOptions().channels?.find(channel => String(channel.id) === String(visibilityChannelId))
|
|
1477
|
+
return declared?.visible === true && channelView.getChannel(visibilityChannelId)?.lineIndexes?.length === membersBeforeHide && channelField('channel-visibility').includes('显示')
|
|
1478
|
+
}, 5000)
|
|
1479
|
+
channelDemo.querySelector('[data-demo="channel-clear-changed"]')?.click()
|
|
1480
|
+
const changesCleared = await until(() => channelView.getChangedChannels().length === 0 && channelField('channel-changed').includes('没有'), 5000)
|
|
1481
|
+
check('通道与数据:显示后成员与分组原样恢复,通道对账后最近变化清空',
|
|
1482
|
+
Boolean(channelShown) && membersBeforeHide > 0 && channelView.getChannelGroups(visibilityChannelId).length === groupsBeforeHide && Boolean(changesCleared) && channelField('channel-visibility').includes('新基准'),
|
|
1483
|
+
`${membersBeforeHide} 回 / ${groupsBeforeHide} 组 · ${channelField('channel-visibility')} / ${channelField('channel-changed')}`)
|
|
1484
|
+
const demoPointId = channelView.getOptions().points?.[0]?.id
|
|
1485
|
+
channelDemo.querySelector('[data-demo="point-hide"]')?.click()
|
|
1486
|
+
const demoPointHidden = await until(() => {
|
|
1487
|
+
const point = channelView.getOptions().points?.find(item => String(item.id) === String(demoPointId))
|
|
1488
|
+
return point?.visible === false && channelField('channel-visibility').includes('隐藏节点')
|
|
1489
|
+
}, 5000)
|
|
1490
|
+
channelDemo.querySelector('[data-demo="point-show"]')?.click()
|
|
1491
|
+
const demoPointShown = await until(() => {
|
|
1492
|
+
const point = channelView.getOptions().points?.find(item => String(item.id) === String(demoPointId))
|
|
1493
|
+
return point?.visible === true && channelField('channel-visibility').includes('显示节点')
|
|
1494
|
+
}, 5000)
|
|
1495
|
+
check('通道与数据:demo 的节点隐藏 / 显示按钮只改 visible 并恢复原数据',
|
|
1496
|
+
Boolean(demoPointHidden) && Boolean(demoPointShown) && demoPointId !== undefined,
|
|
1497
|
+
`${String(demoPointId)} · ${channelField('channel-visibility')}`)
|
|
1498
|
+
const demoLineId = channelView.getOptions().lines?.[0]?.id
|
|
1499
|
+
channelDemo.querySelector('[data-demo="line-hide"]')?.click()
|
|
1500
|
+
const demoLineHidden = await until(() => {
|
|
1501
|
+
const line = channelView.getOptions().lines?.find(item => String(item.id) === String(demoLineId))
|
|
1502
|
+
return line?.visible === false && channelField('channel-visibility').includes('隐藏线路')
|
|
1503
|
+
}, 5000)
|
|
1504
|
+
channelDemo.querySelector('[data-demo="line-show"]')?.click()
|
|
1505
|
+
const demoLineShown = await until(() => {
|
|
1506
|
+
const line = channelView.getOptions().lines?.find(item => String(item.id) === String(demoLineId))
|
|
1507
|
+
return line?.visible === true && channelField('channel-visibility').includes('显示线路')
|
|
1508
|
+
}, 5000)
|
|
1509
|
+
check('通道与数据:demo 的线路隐藏 / 显示按钮只改 visible 并恢复原数据',
|
|
1510
|
+
Boolean(demoLineHidden) && Boolean(demoLineShown) && demoLineId !== undefined,
|
|
1511
|
+
`${String(demoLineId)} · ${channelField('channel-visibility')}`)
|
|
1068
1512
|
const channelBox = channelCanvas.getBoundingClientRect()
|
|
1069
|
-
const channelRightClick = (screen) => {
|
|
1070
|
-
channelCanvas.dispatchEvent(new MouseEvent('contextmenu', {
|
|
1071
|
-
bubbles: true,
|
|
1072
|
-
cancelable: true,
|
|
1073
|
-
composed: true,
|
|
1074
|
-
clientX: channelBox.left + screen[0],
|
|
1075
|
-
clientY: channelBox.top + screen[1]
|
|
1076
|
-
}))
|
|
1077
|
-
}
|
|
1078
|
-
// 找一条通道回线:取两端节点投影的中点,再沿法线扫几像素(回线是骨架的平行偏移)
|
|
1079
|
-
const channelLines = channelView.getData().lines
|
|
1080
|
-
const memberIndex = channelLines.findIndex((line, index) => channelView.getLineChannel(index))
|
|
1081
|
-
const memberLine = channelLines[memberIndex]
|
|
1082
|
-
const channelPoints = channelView.getData().points
|
|
1083
|
-
const memberFrom = channelPoints.find(point => point.id === memberLine.from)
|
|
1084
|
-
const memberTo = channelPoints.find(point => point.id === memberLine.to)
|
|
1085
|
-
const fromScreen = channelView.project(memberFrom.x, memberFrom.y)
|
|
1086
|
-
const toScreen = channelView.project(memberTo.x, memberTo.y)
|
|
1087
|
-
const dx = toScreen[0] - fromScreen[0]
|
|
1088
|
-
const dy = toScreen[1] - fromScreen[1]
|
|
1089
|
-
const span = Math.hypot(dx, dy) || 1
|
|
1090
|
-
const normal = [-dy / span, dx / span]
|
|
1091
|
-
const middle = [(fromScreen[0] + toScreen[0]) / 2, (fromScreen[1] + toScreen[1]) / 2]
|
|
1092
|
-
let menuPayload = null
|
|
1093
|
-
let menuScreen = middle
|
|
1094
|
-
for (const offset of [0, 1, 2, 3, 4, 5, 6, -1, -2, -3, -4, -5, -6]) {
|
|
1095
|
-
const candidate = [middle[0] + normal[0] * offset, middle[1] + normal[1] * offset]
|
|
1096
|
-
channelRightClick(candidate)
|
|
1097
|
-
await wait(60)
|
|
1098
|
-
if (channelSetup.channelMenu) {
|
|
1099
|
-
menuPayload = channelSetup.channelMenu
|
|
1100
|
-
menuScreen = candidate
|
|
1101
|
-
break
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
const channelMenuNode = channelDemo.querySelector('.dv-channel-menu')
|
|
1105
|
-
check('通道与数据:右键通道回线弹出交互框(features.channelMenu)', Boolean(menuPayload) && Boolean(channelMenuNode) && channelView.getOptions().features?.channelMenu === true, menuPayload ? '通道 ' + menuPayload.channelId + ' · 命中回线 ' + menuPayload.lineId + ' · 共 ' + menuPayload.groupCount + ' 组' : '没弹出')
|
|
1106
|
-
const groupsBeforeSplit = channelView.getChannelGroups(menuPayload?.channelId).length
|
|
1107
|
-
const channelAction = text => [...channelDemo.querySelectorAll('.dv-channel-menu__actions button')].find(node => node.textContent.trim() === text)
|
|
1108
|
-
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(' / '))
|
|
1109
|
-
// 「取消该分组选中」= deselect({ channelId, groupId }):先选中这一组,点一下按钮之后选中集清空
|
|
1110
|
-
channelView.selectFeatures({ type: 'channel', channelId: menuPayload.channelId, groupId: menuPayload.groupId })
|
|
1111
|
-
const selectedBeforeDeselect = channelView.getSelectionData().count
|
|
1112
|
-
channelAction('取消该分组选中')?.click()
|
|
1113
|
-
await wait(200)
|
|
1114
|
-
check('通道与数据:交互框的「取消该分组选中」走 deselect,选中集清空(deselect-group)', selectedBeforeDeselect > 0 && channelView.getSelectionData().count === 0, `${selectedBeforeDeselect} → ${channelView.getSelectionData().count}`)
|
|
1115
|
-
channelAction('拆成多组')?.click()
|
|
1116
|
-
await wait(300)
|
|
1117
|
-
const groupsAfterSplit = channelView.getChannelGroups(menuPayload?.channelId).length
|
|
1118
|
-
check('通道与数据:交互框的「拆成多组」走 runChannelAction,分组变多', groupsAfterSplit > groupsBeforeSplit, groupsBeforeSplit + ' → ' + groupsAfterSplit + ' 组')
|
|
1119
|
-
// 线路分配:当前组还有多条回线,下拉默认「拆成新分组」,点「分配」走 moveChannelLine
|
|
1120
|
-
channelDemo.querySelector('.dv-channel-menu__lines li button')?.click()
|
|
1121
|
-
await wait(300)
|
|
1122
|
-
const groupsAfterAssign = channelView.getChannelGroups(menuPayload?.channelId).length
|
|
1123
|
-
check('通道与数据:线路分配的「拆成新分组」走 moveChannelLine', groupsAfterAssign === groupsAfterSplit + 1, groupsAfterSplit + ' → ' + groupsAfterAssign + ' 组')
|
|
1124
|
-
await wait(560)
|
|
1125
|
-
const groupRequestPreview = channelField('channel-backend-request')
|
|
1126
|
-
check('通道与数据:分组动作生成整份 groups 覆盖请求(带动作、版本与幂等键)',
|
|
1127
|
-
groupRequestPreview.includes('PATCH /gis/channels/') &&
|
|
1128
|
-
groupRequestPreview.includes('/groups') &&
|
|
1129
|
-
groupRequestPreview.includes('"groups"') &&
|
|
1130
|
-
groupRequestPreview.includes('"lineIds"') &&
|
|
1131
|
-
groupRequestPreview.includes('"baseVersion"') &&
|
|
1132
|
-
groupRequestPreview.includes('"opId"') &&
|
|
1133
|
-
groupRequestPreview.includes('200 OK'),
|
|
1134
|
-
groupRequestPreview.slice(0, 200))
|
|
1135
|
-
check('通道与数据:分组保存成功回标准响应包与最新 groups 快照',
|
|
1136
|
-
['"code": "OK"', '"operation": "channel.groups"', '"transactionId"', '"result"', '"groupCount"', '"lineCount"', 'Idempotency-Key:', 'If-Match: "v'].every(key => groupRequestPreview.includes(key)),
|
|
1137
|
-
groupRequestPreview.slice(-300).replace(/\s+/g, ' '))
|
|
1138
|
-
// 点分组 = setCurrentChannelGroup(顺带选中这一组的回线)
|
|
1139
|
-
const groupsNow = channelView.getChannelGroups(menuPayload?.channelId)
|
|
1140
|
-
const currentId = channelView.getChannelContext(menuPayload?.channelId)?.groupId
|
|
1141
|
-
const targetGroup = groupsNow.find(group => String(group.id) !== String(currentId))
|
|
1142
|
-
const groupItems = [...channelDemo.querySelectorAll('.dv-channel-menu__groups li')]
|
|
1143
|
-
groupItems[groupsNow.findIndex(group => String(group.id) === String(targetGroup?.id))]?.click()
|
|
1144
|
-
await wait(300)
|
|
1145
|
-
const switchedContext = channelView.getChannelContext(menuPayload?.channelId)
|
|
1146
|
-
// getChannelGroups 每一项只给 lineIds(lineCount 是 getChannelContext().group 的字段),回线数按 lineIds.length 算
|
|
1147
|
-
const targetLineCount = targetGroup?.lineIds?.length ?? 0
|
|
1148
|
-
check('通道与数据:点分组 = setCurrentChannelGroup + 选中它的回线', Boolean(targetGroup) && switchedContext?.groupId === targetGroup.id && channelView.getSelections().length === targetLineCount, switchedContext ? '当前「' + switchedContext.groupId + '」/ 选中 ' + channelView.getSelections().length + ' 回线(目标「' + (targetGroup?.id ?? '—') + '」' + targetLineCount + ' 回)' : '没有上下文')
|
|
1149
|
-
// 右键落到节点上:通道回调收到 null,框自己收掉
|
|
1150
|
-
channelRightClick(fromScreen)
|
|
1151
|
-
await wait(200)
|
|
1152
|
-
check('通道与数据:右键落到节点上时回调收到 null,框收掉', !channelDemo.querySelector('.dv-channel-menu') && channelSetup.channelMenu === null, '')
|
|
1153
|
-
// 再右键回回线上、按 Esc 也能收
|
|
1154
|
-
channelRightClick(menuScreen)
|
|
1155
|
-
const reopenedMenu = await until(() => channelDemo.querySelector('.dv-channel-menu'), 5000)
|
|
1156
|
-
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
1157
|
-
await wait(200)
|
|
1158
|
-
check('通道与数据:Esc 收框', Boolean(reopenedMenu) && !channelDemo.querySelector('.dv-channel-menu'), '')
|
|
1159
|
-
|
|
1160
|
-
// 一次拆几条:面板上的勾选块走 addChannelGroup(moveChannelLine 的批量版)
|
|
1161
|
-
const splitPicks = channelDemo.querySelector('[data-demo="add-channel-group-picks"]')
|
|
1162
|
-
const splitBoxes = () => [...(splitPicks?.querySelectorAll('input[type="checkbox"]') ?? [])]
|
|
1163
|
-
const splitButton = channelDemo.querySelector('[data-demo="add-channel-group"]')
|
|
1164
|
-
const splitReadout = channelDemo.querySelector('[data-demo="add-channel-group-text"]')
|
|
1165
|
-
const splitModeSelect = channelDemo.querySelector('[data-demo="add-channel-group-mode"]')
|
|
1166
|
-
const splitIdInput = channelDemo.querySelector('[data-demo="add-channel-group-id"]')
|
|
1167
|
-
const firstChannelId = channelView.getChannels()[0]?.channelId
|
|
1168
|
-
const groupsOfFirst = () => channelView.getChannelGroups(firstChannelId)
|
|
1169
|
-
const membersOfFirst = () => groupsOfFirst().reduce((sum, group) => sum + group.lineIds.length, 0)
|
|
1170
|
-
const idKeys = ids => ids.map(id => String(id)).sort().join(',')
|
|
1171
|
-
// 先并回一组:一条通道一组、三条回线,后面每一步都可预期
|
|
1172
|
-
;[...channelDemo.querySelectorAll('.dv-panel button')].find(node => node.textContent.trim() === '并回一组')?.click()
|
|
1173
|
-
await wait(320)
|
|
1174
|
-
const mergedGroups = groupsOfFirst()
|
|
1175
|
-
check('一次拆几条:勾选块列出第一条通道的全部回线,没勾时按钮禁用着', mergedGroups.length === 1 && splitBoxes().length === membersOfFirst() && splitButton?.disabled === true, `${mergedGroups.length} 组 / ${splitBoxes().length} 个勾选`)
|
|
1176
|
-
// 给这一组加一个拐点,下面才看得出「新分组照抄来源组当前的骨架」
|
|
1177
|
-
channelView.addChannelGroupWaypoint(firstChannelId, mergedGroups[0]?.id)
|
|
1178
|
-
await wait(320)
|
|
1179
|
-
const sourcePath = JSON.stringify(groupsOfFirst()[0]?.path ?? null)
|
|
1180
|
-
// 逐条勾(勾一次等一次重渲染):勾完从面板的状态里读回真正勾上了哪几条
|
|
1181
|
-
const clickBox = async (id) => {
|
|
1182
|
-
const node = splitBoxes().find(entry => String(entry.value) === String(id))
|
|
1183
|
-
if (node && !node.checked) {
|
|
1184
|
-
node.click()
|
|
1185
|
-
}
|
|
1186
|
-
await wait(60)
|
|
1187
|
-
}
|
|
1188
|
-
for (const id of channelSetup.splitCandidates.slice(0, 2).map(row => row.id)) {
|
|
1189
|
-
await clickBox(id)
|
|
1513
|
+
const channelRightClick = (screen) => {
|
|
1514
|
+
channelCanvas.dispatchEvent(new MouseEvent('contextmenu', {
|
|
1515
|
+
bubbles: true,
|
|
1516
|
+
cancelable: true,
|
|
1517
|
+
composed: true,
|
|
1518
|
+
clientX: channelBox.left + screen[0],
|
|
1519
|
+
clientY: channelBox.top + screen[1]
|
|
1520
|
+
}))
|
|
1521
|
+
}
|
|
1522
|
+
// 找一条通道回线:取两端节点投影的中点,再沿法线扫几像素(回线是骨架的平行偏移)
|
|
1523
|
+
const channelLines = channelView.getData().lines
|
|
1524
|
+
const memberIndex = channelLines.findIndex((line, index) => channelView.getLineChannel(index))
|
|
1525
|
+
const memberLine = channelLines[memberIndex]
|
|
1526
|
+
const channelPoints = channelView.getData().points
|
|
1527
|
+
const memberFrom = channelPoints.find(point => point.id === memberLine.from)
|
|
1528
|
+
const memberTo = channelPoints.find(point => point.id === memberLine.to)
|
|
1529
|
+
const fromScreen = channelView.project(memberFrom.x, memberFrom.y)
|
|
1530
|
+
const toScreen = channelView.project(memberTo.x, memberTo.y)
|
|
1531
|
+
const dx = toScreen[0] - fromScreen[0]
|
|
1532
|
+
const dy = toScreen[1] - fromScreen[1]
|
|
1533
|
+
const span = Math.hypot(dx, dy) || 1
|
|
1534
|
+
const normal = [-dy / span, dx / span]
|
|
1535
|
+
const middle = [(fromScreen[0] + toScreen[0]) / 2, (fromScreen[1] + toScreen[1]) / 2]
|
|
1536
|
+
let menuPayload = null
|
|
1537
|
+
let menuScreen = middle
|
|
1538
|
+
for (const offset of [0, 1, 2, 3, 4, 5, 6, -1, -2, -3, -4, -5, -6]) {
|
|
1539
|
+
const candidate = [middle[0] + normal[0] * offset, middle[1] + normal[1] * offset]
|
|
1540
|
+
channelRightClick(candidate)
|
|
1541
|
+
await wait(60)
|
|
1542
|
+
if (channelSetup.channelMenu) {
|
|
1543
|
+
menuPayload = channelSetup.channelMenu
|
|
1544
|
+
menuScreen = candidate
|
|
1545
|
+
break
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
const channelMenuNode = channelDemo.querySelector('.dv-channel-menu')
|
|
1549
|
+
check('通道与数据:右键通道回线弹出交互框(features.channelMenu)', Boolean(menuPayload) && Boolean(channelMenuNode) && channelView.getOptions().features?.channelMenu === true, menuPayload ? '通道 ' + menuPayload.channelId + ' · 命中回线 ' + menuPayload.lineId + ' · 共 ' + menuPayload.groupCount + ' 组' : '没弹出')
|
|
1550
|
+
const groupsBeforeSplit = channelView.getChannelGroups(menuPayload?.channelId).length
|
|
1551
|
+
const channelAction = text => [...channelDemo.querySelectorAll('.dv-channel-menu__actions button')].find(node => node.textContent.trim() === text)
|
|
1552
|
+
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(' / '))
|
|
1553
|
+
// 「取消该分组选中」= deselect({ channelId, groupId }):先选中这一组,点一下按钮之后选中集清空
|
|
1554
|
+
channelView.selectFeatures({ type: 'channel', channelId: menuPayload.channelId, groupId: menuPayload.groupId })
|
|
1555
|
+
const selectedBeforeDeselect = channelView.getSelectionData().count
|
|
1556
|
+
channelAction('取消该分组选中')?.click()
|
|
1557
|
+
await wait(200)
|
|
1558
|
+
check('通道与数据:交互框的「取消该分组选中」走 deselect,选中集清空(deselect-group)', selectedBeforeDeselect > 0 && channelView.getSelectionData().count === 0, `${selectedBeforeDeselect} → ${channelView.getSelectionData().count}`)
|
|
1559
|
+
channelAction('拆成多组')?.click()
|
|
1560
|
+
await wait(300)
|
|
1561
|
+
const groupsAfterSplit = channelView.getChannelGroups(menuPayload?.channelId).length
|
|
1562
|
+
check('通道与数据:交互框的「拆成多组」走 runChannelAction,分组变多', groupsAfterSplit > groupsBeforeSplit, groupsBeforeSplit + ' → ' + groupsAfterSplit + ' 组')
|
|
1563
|
+
// 线路分配:当前组还有多条回线,下拉默认「拆成新分组」,点「分配」走 moveChannelLine
|
|
1564
|
+
channelDemo.querySelector('.dv-channel-menu__lines li button')?.click()
|
|
1565
|
+
await wait(300)
|
|
1566
|
+
const groupsAfterAssign = channelView.getChannelGroups(menuPayload?.channelId).length
|
|
1567
|
+
check('通道与数据:线路分配的「拆成新分组」走 moveChannelLine', groupsAfterAssign === groupsAfterSplit + 1, groupsAfterSplit + ' → ' + groupsAfterAssign + ' 组')
|
|
1568
|
+
await wait(560)
|
|
1569
|
+
const groupRequestPreview = channelField('channel-backend-request')
|
|
1570
|
+
check('通道与数据:分组动作生成整份 groups 覆盖请求(带动作、版本与幂等键)',
|
|
1571
|
+
groupRequestPreview.includes('PATCH /gis/channels/') &&
|
|
1572
|
+
groupRequestPreview.includes('/groups') &&
|
|
1573
|
+
groupRequestPreview.includes('"groups"') &&
|
|
1574
|
+
groupRequestPreview.includes('"lineIds"') &&
|
|
1575
|
+
groupRequestPreview.includes('"baseVersion"') &&
|
|
1576
|
+
groupRequestPreview.includes('"opId"') &&
|
|
1577
|
+
groupRequestPreview.includes('200 OK'),
|
|
1578
|
+
groupRequestPreview.slice(0, 200))
|
|
1579
|
+
check('通道与数据:分组保存成功回标准响应包与最新 groups 快照',
|
|
1580
|
+
['"code": "OK"', '"operation": "channel.groups"', '"transactionId"', '"result"', '"groupCount"', '"lineCount"', 'Idempotency-Key:', 'If-Match: "v'].every(key => groupRequestPreview.includes(key)),
|
|
1581
|
+
groupRequestPreview.slice(-300).replace(/\s+/g, ' '))
|
|
1582
|
+
// 点分组 = setCurrentChannelGroup(顺带选中这一组的回线)
|
|
1583
|
+
const groupsNow = channelView.getChannelGroups(menuPayload?.channelId)
|
|
1584
|
+
const currentId = channelView.getChannelContext(menuPayload?.channelId)?.groupId
|
|
1585
|
+
const targetGroup = groupsNow.find(group => String(group.id) !== String(currentId))
|
|
1586
|
+
const groupItems = [...channelDemo.querySelectorAll('.dv-channel-menu__groups li')]
|
|
1587
|
+
groupItems[groupsNow.findIndex(group => String(group.id) === String(targetGroup?.id))]?.click()
|
|
1588
|
+
await wait(300)
|
|
1589
|
+
const switchedContext = channelView.getChannelContext(menuPayload?.channelId)
|
|
1590
|
+
// getChannelGroups 每一项只给 lineIds(lineCount 是 getChannelContext().group 的字段),回线数按 lineIds.length 算
|
|
1591
|
+
const targetLineCount = targetGroup?.lineIds?.length ?? 0
|
|
1592
|
+
check('通道与数据:点分组 = setCurrentChannelGroup + 选中它的回线', Boolean(targetGroup) && switchedContext?.groupId === targetGroup.id && channelView.getSelections().length === targetLineCount, switchedContext ? '当前「' + switchedContext.groupId + '」/ 选中 ' + channelView.getSelections().length + ' 回线(目标「' + (targetGroup?.id ?? '—') + '」' + targetLineCount + ' 回)' : '没有上下文')
|
|
1593
|
+
// 右键落到节点上:通道回调收到 null,框自己收掉
|
|
1594
|
+
channelRightClick(fromScreen)
|
|
1595
|
+
await wait(200)
|
|
1596
|
+
check('通道与数据:右键落到节点上时回调收到 null,框收掉', !channelDemo.querySelector('.dv-channel-menu') && channelSetup.channelMenu === null, '')
|
|
1597
|
+
// 再右键回回线上、按 Esc 也能收
|
|
1598
|
+
channelRightClick(menuScreen)
|
|
1599
|
+
const reopenedMenu = await until(() => channelDemo.querySelector('.dv-channel-menu'), 5000)
|
|
1600
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
1601
|
+
await wait(200)
|
|
1602
|
+
check('通道与数据:Esc 收框', Boolean(reopenedMenu) && !channelDemo.querySelector('.dv-channel-menu'), '')
|
|
1603
|
+
|
|
1604
|
+
// 一次拆几条:面板上的勾选块走 addChannelGroup(moveChannelLine 的批量版)
|
|
1605
|
+
const splitPicks = channelDemo.querySelector('[data-demo="add-channel-group-picks"]')
|
|
1606
|
+
const splitBoxes = () => [...(splitPicks?.querySelectorAll('input[type="checkbox"]') ?? [])]
|
|
1607
|
+
const splitButton = channelDemo.querySelector('[data-demo="add-channel-group"]')
|
|
1608
|
+
const splitReadout = channelDemo.querySelector('[data-demo="add-channel-group-text"]')
|
|
1609
|
+
const splitModeSelect = channelDemo.querySelector('[data-demo="add-channel-group-mode"]')
|
|
1610
|
+
const splitIdInput = channelDemo.querySelector('[data-demo="add-channel-group-id"]')
|
|
1611
|
+
const firstChannelId = channelView.getChannels()[0]?.channelId
|
|
1612
|
+
const groupsOfFirst = () => channelView.getChannelGroups(firstChannelId)
|
|
1613
|
+
const membersOfFirst = () => groupsOfFirst().reduce((sum, group) => sum + group.lineIds.length, 0)
|
|
1614
|
+
const idKeys = ids => ids.map(id => String(id)).sort().join(',')
|
|
1615
|
+
// 先并回一组:一条通道一组、三条回线,后面每一步都可预期
|
|
1616
|
+
;[...channelDemo.querySelectorAll('.dv-panel button')].find(node => node.textContent.trim() === '并回一组')?.click()
|
|
1617
|
+
await wait(320)
|
|
1618
|
+
const mergedGroups = groupsOfFirst()
|
|
1619
|
+
check('一次拆几条:勾选块列出第一条通道的全部回线,没勾时按钮禁用着', mergedGroups.length === 1 && splitBoxes().length === membersOfFirst() && splitButton?.disabled === true, `${mergedGroups.length} 组 / ${splitBoxes().length} 个勾选`)
|
|
1620
|
+
// 给这一组加一个拐点,下面才看得出「新分组照抄来源组当前的骨架」
|
|
1621
|
+
channelView.addChannelGroupWaypoint(firstChannelId, mergedGroups[0]?.id)
|
|
1622
|
+
await wait(320)
|
|
1623
|
+
const sourcePath = JSON.stringify(groupsOfFirst()[0]?.path ?? null)
|
|
1624
|
+
// 逐条勾(勾一次等一次重渲染):勾完从面板的状态里读回真正勾上了哪几条
|
|
1625
|
+
const clickBox = async (id) => {
|
|
1626
|
+
const node = splitBoxes().find(entry => String(entry.value) === String(id))
|
|
1627
|
+
if (node && !node.checked) {
|
|
1628
|
+
node.click()
|
|
1629
|
+
}
|
|
1630
|
+
await wait(60)
|
|
1631
|
+
}
|
|
1632
|
+
for (const id of channelSetup.splitCandidates.slice(0, 2).map(row => row.id)) {
|
|
1633
|
+
await clickBox(id)
|
|
1634
|
+
}
|
|
1635
|
+
const pickedIds = channelSetup.splitPicked.map(id => String(id))
|
|
1636
|
+
splitIdInput.value = '拆组-甲'
|
|
1637
|
+
splitIdInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
1638
|
+
await wait(80)
|
|
1639
|
+
splitButton.click()
|
|
1640
|
+
await wait(320)
|
|
1641
|
+
const afterSplit = groupsOfFirst()
|
|
1642
|
+
const landedGroup = afterSplit.find(group => String(group.id) === '拆组-甲')
|
|
1643
|
+
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]))}`)
|
|
1644
|
+
check('其余回线一条不丢(三条回线还在,只是分成两组)', membersOfFirst() === mergedGroups[0].lineIds.length && afterSplit.length === 2, `${membersOfFirst()} 回 / ${afterSplit.length} 组`)
|
|
1645
|
+
check('新这一组照抄来源组当前的骨架(拐点没丢、画面不会跳)', JSON.stringify(landedGroup?.path ?? null) === sourcePath && sourcePath !== '[]' && sourcePath !== 'null', sourcePath)
|
|
1646
|
+
check('读数报出新组与组数,日志里留下 addChannelGroup', splitReadout.textContent.includes('拆组-甲') && logText().includes('addChannelGroup'), splitReadout.textContent.trim().slice(0, 90))
|
|
1647
|
+
// 这两条已经单独成一组:不写 path 就什么都没改动,读数报「被拒绝」
|
|
1648
|
+
for (const id of pickedIds) {
|
|
1649
|
+
await clickBox(id)
|
|
1650
|
+
}
|
|
1651
|
+
await wait(80)
|
|
1652
|
+
splitButton.click()
|
|
1653
|
+
await wait(320)
|
|
1654
|
+
check('同两条再拆一次被拒(reason 说「已经是单独一组」)', splitReadout.textContent.includes('被拒绝') && splitReadout.textContent.includes('单独一组'), splitReadout.textContent.trim().slice(0, 90))
|
|
1655
|
+
// 写了 path 就不算「什么都没改」:切「直连」给这一组一份没有拐点的骨架
|
|
1656
|
+
splitModeSelect.value = 'straight'
|
|
1657
|
+
splitModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1658
|
+
splitIdInput.value = ''
|
|
1659
|
+
splitIdInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
1660
|
+
for (const id of pickedIds) {
|
|
1661
|
+
await clickBox(id)
|
|
1662
|
+
}
|
|
1663
|
+
await wait(80)
|
|
1664
|
+
splitButton.click()
|
|
1665
|
+
await wait(320)
|
|
1666
|
+
const straightGroup = groupsOfFirst().find(group => group.lineIds.length === 2)
|
|
1667
|
+
check('切「直连」再拆:options.path 写 [] 就真的一个拐点都不带', Boolean(straightGroup) && straightGroup.path.length === 0, JSON.stringify(groupsOfFirst().map(group => [group.id, group.lineIds, group.path])))
|
|
1668
|
+
// 线路分组弹框:行内确认移入只改草稿,底部确定时一次 splitChannelGroups 提交整份 groups
|
|
1669
|
+
const dialogOpen = channelDemo.querySelector('[data-demo="channel-group-dialog-open"]')
|
|
1670
|
+
const groupsBeforeDialog = JSON.stringify(groupsOfFirst().map(group => ({ id: String(group.id), lineIds: group.lineIds.map(String).sort() })))
|
|
1671
|
+
dialogOpen?.click()
|
|
1672
|
+
const groupDialogNode = await until(() => channelDemo.querySelector('[data-demo="channel-group-dialog"]'), 3000)
|
|
1673
|
+
const dialogCards = [...(groupDialogNode?.querySelectorAll('[data-demo="channel-group-dialog-card"]') ?? [])]
|
|
1674
|
+
const dialogRows = [...(groupDialogNode?.querySelectorAll('[data-demo="channel-group-dialog-line"]') ?? [])]
|
|
1675
|
+
const dialogTitle = groupDialogNode?.querySelector('h3')?.textContent.trim() ?? ''
|
|
1676
|
+
const dialogSubtitle = groupDialogNode?.querySelector('.dv-group-dialog__head p')?.textContent ?? ''
|
|
1677
|
+
check('通道与数据:线路分组弹框按通道列全分组与回线,标题 / 场站对标标识都在',
|
|
1678
|
+
Boolean(groupDialogNode) && dialogTitle === '线路分组' && dialogSubtitle.includes('场站对标标识') && dialogCards.length === groupsOfFirst().length && dialogRows.length === membersOfFirst(),
|
|
1679
|
+
`${dialogCards.length} 组 / ${dialogRows.length} 回 / ${dialogTitle} · ${dialogSubtitle.replace(/\s+/g, ' ').slice(0, 100)}`)
|
|
1680
|
+
const sourceCard = dialogCards.find(card => card.querySelectorAll('[data-demo="channel-group-dialog-line"]').length > 1) ?? dialogCards[0]
|
|
1681
|
+
const sourceRow = sourceCard?.querySelector('[data-demo="channel-group-dialog-line"]')
|
|
1682
|
+
const targetCard = dialogCards.find(card => card !== sourceCard)
|
|
1683
|
+
const draftLineId = sourceRow?.dataset.lineId
|
|
1684
|
+
const draftTargetId = targetCard?.dataset.groupId
|
|
1685
|
+
const draftTargetSelect = sourceRow?.querySelector('[data-demo="channel-group-dialog-target"]')
|
|
1686
|
+
if (draftTargetSelect && draftTargetId) {
|
|
1687
|
+
draftTargetSelect.value = draftTargetId
|
|
1688
|
+
draftTargetSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1190
1689
|
}
|
|
1191
|
-
const pickedIds = channelSetup.splitPicked.map(id => String(id))
|
|
1192
|
-
splitIdInput.value = '拆组-甲'
|
|
1193
|
-
splitIdInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
1194
1690
|
await wait(80)
|
|
1195
|
-
|
|
1196
|
-
await wait(
|
|
1197
|
-
const
|
|
1198
|
-
const
|
|
1199
|
-
check('
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1691
|
+
sourceRow?.querySelector('[data-demo="channel-group-dialog-move"]')?.click()
|
|
1692
|
+
await wait(120)
|
|
1693
|
+
const draftGroups = JSON.stringify(groupsOfFirst().map(group => ({ id: String(group.id), lineIds: group.lineIds.map(String).sort() })))
|
|
1694
|
+
const draftText = groupDialogNode?.querySelector('[data-demo="channel-group-dialog-text"]')?.textContent ?? ''
|
|
1695
|
+
check('通道与数据:弹框里的「确认移入」只记草稿,组件数据不动',
|
|
1696
|
+
Boolean(draftLineId) && Boolean(draftTargetId) && draftGroups === groupsBeforeDialog && draftText.includes('数据还没动'),
|
|
1697
|
+
`${draftLineId} → ${draftTargetId} · ${draftText.replace(/\s+/g, ' ').slice(0, 110)}`)
|
|
1698
|
+
groupDialogNode?.querySelector('[data-demo="channel-group-dialog-cancel"]')?.click()
|
|
1699
|
+
await wait(100)
|
|
1700
|
+
const cancelledGroups = JSON.stringify(groupsOfFirst().map(group => ({ id: String(group.id), lineIds: group.lineIds.map(String).sort() })))
|
|
1701
|
+
check('通道与数据:取消分组弹框不写数据、弹框收起',
|
|
1702
|
+
!channelDemo.querySelector('[data-demo="channel-group-dialog"]') && cancelledGroups === groupsBeforeDialog,
|
|
1703
|
+
cancelledGroups === groupsBeforeDialog ? '数据未变' : '取消后分组被改了')
|
|
1704
|
+
// 重开再做一次同样的移动,这次点「确定分组」:确认只调用一次整组变更方法
|
|
1705
|
+
dialogOpen?.click()
|
|
1706
|
+
const reopenedDialog = await until(() => channelDemo.querySelector('[data-demo="channel-group-dialog"]'), 3000)
|
|
1707
|
+
const reopenedCards = [...(reopenedDialog?.querySelectorAll('[data-demo="channel-group-dialog-card"]') ?? [])]
|
|
1708
|
+
const reopenedSourceCard = reopenedCards.find(card => card.querySelectorAll('[data-demo="channel-group-dialog-line"]').length > 1) ?? reopenedCards[0]
|
|
1709
|
+
const reopenedRow = reopenedSourceCard?.querySelector('[data-demo="channel-group-dialog-line"]')
|
|
1710
|
+
const reopenedTargetCard = reopenedCards.find(card => card !== reopenedSourceCard)
|
|
1711
|
+
const movedLineId = reopenedRow?.dataset.lineId
|
|
1712
|
+
const movedTargetId = reopenedTargetCard?.dataset.groupId
|
|
1713
|
+
const reopenedSelect = reopenedRow?.querySelector('[data-demo="channel-group-dialog-target"]')
|
|
1714
|
+
if (reopenedSelect && movedTargetId) {
|
|
1715
|
+
reopenedSelect.value = movedTargetId
|
|
1716
|
+
reopenedSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1206
1717
|
}
|
|
1207
1718
|
await wait(80)
|
|
1208
|
-
|
|
1209
|
-
await wait(
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
for (const id of pickedIds) {
|
|
1217
|
-
await clickBox(id)
|
|
1719
|
+
reopenedRow?.querySelector('[data-demo="channel-group-dialog-move"]')?.click()
|
|
1720
|
+
await wait(100)
|
|
1721
|
+
const confirmButton = reopenedDialog?.querySelector('[data-demo="channel-group-dialog-confirm"]')
|
|
1722
|
+
let splitGroupCalls = 0
|
|
1723
|
+
const originalSplitChannelGroups = channelView.splitChannelGroups
|
|
1724
|
+
channelView.splitChannelGroups = function (...args) {
|
|
1725
|
+
splitGroupCalls += 1
|
|
1726
|
+
return originalSplitChannelGroups.apply(this, args)
|
|
1218
1727
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
const
|
|
1223
|
-
|
|
1728
|
+
confirmButton?.click()
|
|
1729
|
+
await wait(600)
|
|
1730
|
+
channelView.splitChannelGroups = originalSplitChannelGroups
|
|
1731
|
+
const confirmedGroups = groupsOfFirst()
|
|
1732
|
+
const movedGroup = confirmedGroups.find(group => String(group.id) === String(movedTargetId))
|
|
1733
|
+
check('通道与数据:确定分组只调用一次 splitChannelGroups,整份声明一次落地',
|
|
1734
|
+
Boolean(confirmButton) && splitGroupCalls === 1 && membersOfFirst() === mergedGroups[0].lineIds.length && movedGroup?.lineIds.some(id => String(id) === String(movedLineId)) && logText().includes('splitChannelGroups'),
|
|
1735
|
+
`调用 ${splitGroupCalls} 次 / ${confirmedGroups.length} 组 / ${membersOfFirst()} 回线 / ${movedLineId} → ${movedTargetId}`)
|
|
1224
1736
|
const waypointGroupId = straightGroup?.id ?? groupsOfFirst()[0]?.id
|
|
1225
|
-
const backendModeSelect = channelDemo.querySelector('[data-demo="channel-backend-mode"]')
|
|
1226
|
-
const backendRetryButton = channelDemo.querySelector('[data-demo="channel-backend-retry"]')
|
|
1227
|
-
backendModeSelect.value = 'network'
|
|
1228
|
-
backendModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1229
|
-
channelView.addChannelGroupWaypoint(firstChannelId, waypointGroupId)
|
|
1230
|
-
await wait(560)
|
|
1231
|
-
const waypointFailedPreview = channelField('channel-backend-request')
|
|
1232
|
-
check('通道与数据:拐点 add 生成整份 path 请求,失败保留本地编辑并开放重试',
|
|
1233
|
-
waypointFailedPreview.includes(`PATCH /gis/channels/${firstChannelId}/groups/waypoints`) &&
|
|
1234
|
-
waypointFailedPreview.includes('"path"') &&
|
|
1235
|
-
waypointFailedPreview.includes('"channelId"') &&
|
|
1236
|
-
waypointFailedPreview.includes('"groupId"') &&
|
|
1237
|
-
waypointFailedPreview.includes('"baseVersion"') &&
|
|
1238
|
-
waypointFailedPreview.includes('"opId"') &&
|
|
1239
|
-
channelField('channel-waypoint-status').includes('失败') &&
|
|
1240
|
-
backendRetryButton.disabled === false,
|
|
1241
|
-
waypointFailedPreview.slice(0, 220))
|
|
1242
|
-
backendRetryButton.click()
|
|
1243
|
-
const waypointRetried = await until(() => channelField('channel-waypoint-status').includes('已同步'), 5000)
|
|
1244
|
-
check('通道与数据:拐点请求失败后可原样重试并同步到服务端', Boolean(waypointRetried), channelField('channel-waypoint-status'))
|
|
1245
|
-
const channelReplayButton = channelDemo.querySelector('[data-demo="channel-backend-replay"]')
|
|
1246
|
-
const channelVersionBeforeReplay = channelSetup.backendSnapshot?.version
|
|
1247
|
-
const channelAppliedBeforeReplay = channelSetup.backendSnapshot?.applied
|
|
1248
|
-
const channelReplayReady = Boolean(channelReplayButton) && !channelReplayButton.disabled
|
|
1249
|
-
channelReplayButton?.click()
|
|
1250
|
-
const channelReplayHit = await until(() => channelField('channel-backend-readout').includes('幂等回放') && channelField('channel-backend-request').includes('"idempotent": true'), 5000)
|
|
1251
|
-
check('通道与数据:重放原 opId 命中幂等缓存,版本与已应用数都不前进',
|
|
1252
|
-
channelReplayReady && Boolean(channelReplayHit) && channelSetup.backendSnapshot?.version === channelVersionBeforeReplay && channelSetup.backendSnapshot?.applied === channelAppliedBeforeReplay && channelField('channel-backend-request').includes('Idempotency-Key 命中'),
|
|
1253
|
-
`v${channelVersionBeforeReplay} / applied ${channelAppliedBeforeReplay} → v${channelSetup.backendSnapshot?.version} / applied ${channelSetup.backendSnapshot?.applied} / ${channelField('channel-backend-readout')}`)
|
|
1254
|
-
backendModeSelect.value = 'success'
|
|
1255
|
-
backendModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1256
|
-
channelView.removeChannelGroupWaypoint(firstChannelId, waypointGroupId)
|
|
1257
|
-
await wait(560)
|
|
1258
|
-
const emptyPathPreview = channelField('channel-backend-request')
|
|
1259
|
-
const waypointPath = channelView.getChannelGroupWaypoints(firstChannelId, waypointGroupId)?.path ?? null
|
|
1260
|
-
check('通道与数据:删掉最后一个拐点会提交合法空 path(不是拒绝请求)',
|
|
1261
|
-
Array.isArray(waypointPath) && waypointPath.length === 0 &&
|
|
1262
|
-
emptyPathPreview.includes('"path": []') &&
|
|
1263
|
-
emptyPathPreview.includes('200 OK'),
|
|
1264
|
-
`path=${JSON.stringify(waypointPath)} / ${emptyPathPreview.slice(-120)}`)
|
|
1265
|
-
// 通道选中模式:切到 channel 之后,单击一条回线选中整条通道
|
|
1266
|
-
const channelLineTotal = channelView.getChannelGroups(menuPayload?.channelId).reduce((sum, group) => sum + group.lineIds.length, 0)
|
|
1267
|
-
const modeSelect = [...channelDemo.querySelectorAll('.dv-panel select')].find(node => [...node.options].some(option => option.value === 'channel'))
|
|
1268
|
-
if (modeSelect) {
|
|
1269
|
-
modeSelect.value = 'channel'
|
|
1270
|
-
modeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1271
|
-
}
|
|
1272
|
-
channelView.
|
|
1273
|
-
await wait(200)
|
|
1274
|
-
const channelClick = (screen) => {
|
|
1275
|
-
const base = { bubbles: true, cancelable: true, composed: true, pointerId: 12, pointerType: 'mouse', isPrimary: true, clientX: channelBox.left + screen[0], clientY: channelBox.top + screen[1] }
|
|
1276
|
-
channelCanvas.dispatchEvent(new PointerEvent('pointerdown', { ...base, button: 0, buttons: 1 }))
|
|
1277
|
-
channelCanvas.dispatchEvent(new PointerEvent('pointerup', { ...base, button: 0, buttons: 0 }))
|
|
1278
|
-
}
|
|
1737
|
+
const backendModeSelect = channelDemo.querySelector('[data-demo="channel-backend-mode"]')
|
|
1738
|
+
const backendRetryButton = channelDemo.querySelector('[data-demo="channel-backend-retry"]')
|
|
1739
|
+
backendModeSelect.value = 'network'
|
|
1740
|
+
backendModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1741
|
+
channelView.addChannelGroupWaypoint(firstChannelId, waypointGroupId)
|
|
1742
|
+
await wait(560)
|
|
1743
|
+
const waypointFailedPreview = channelField('channel-backend-request')
|
|
1744
|
+
check('通道与数据:拐点 add 生成整份 path 请求,失败保留本地编辑并开放重试',
|
|
1745
|
+
waypointFailedPreview.includes(`PATCH /gis/channels/${firstChannelId}/groups/waypoints`) &&
|
|
1746
|
+
waypointFailedPreview.includes('"path"') &&
|
|
1747
|
+
waypointFailedPreview.includes('"channelId"') &&
|
|
1748
|
+
waypointFailedPreview.includes('"groupId"') &&
|
|
1749
|
+
waypointFailedPreview.includes('"baseVersion"') &&
|
|
1750
|
+
waypointFailedPreview.includes('"opId"') &&
|
|
1751
|
+
channelField('channel-waypoint-status').includes('失败') &&
|
|
1752
|
+
backendRetryButton.disabled === false,
|
|
1753
|
+
waypointFailedPreview.slice(0, 220))
|
|
1754
|
+
backendRetryButton.click()
|
|
1755
|
+
const waypointRetried = await until(() => channelField('channel-waypoint-status').includes('已同步'), 5000)
|
|
1756
|
+
check('通道与数据:拐点请求失败后可原样重试并同步到服务端', Boolean(waypointRetried), channelField('channel-waypoint-status'))
|
|
1757
|
+
const channelReplayButton = channelDemo.querySelector('[data-demo="channel-backend-replay"]')
|
|
1758
|
+
const channelVersionBeforeReplay = channelSetup.backendSnapshot?.version
|
|
1759
|
+
const channelAppliedBeforeReplay = channelSetup.backendSnapshot?.applied
|
|
1760
|
+
const channelReplayReady = Boolean(channelReplayButton) && !channelReplayButton.disabled
|
|
1761
|
+
channelReplayButton?.click()
|
|
1762
|
+
const channelReplayHit = await until(() => channelField('channel-backend-readout').includes('幂等回放') && channelField('channel-backend-request').includes('"idempotent": true'), 5000)
|
|
1763
|
+
check('通道与数据:重放原 opId 命中幂等缓存,版本与已应用数都不前进',
|
|
1764
|
+
channelReplayReady && Boolean(channelReplayHit) && channelSetup.backendSnapshot?.version === channelVersionBeforeReplay && channelSetup.backendSnapshot?.applied === channelAppliedBeforeReplay && channelField('channel-backend-request').includes('Idempotency-Key 命中'),
|
|
1765
|
+
`v${channelVersionBeforeReplay} / applied ${channelAppliedBeforeReplay} → v${channelSetup.backendSnapshot?.version} / applied ${channelSetup.backendSnapshot?.applied} / ${channelField('channel-backend-readout')}`)
|
|
1766
|
+
backendModeSelect.value = 'success'
|
|
1767
|
+
backendModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1768
|
+
channelView.removeChannelGroupWaypoint(firstChannelId, waypointGroupId)
|
|
1769
|
+
await wait(560)
|
|
1770
|
+
const emptyPathPreview = channelField('channel-backend-request')
|
|
1771
|
+
const waypointPath = channelView.getChannelGroupWaypoints(firstChannelId, waypointGroupId)?.path ?? null
|
|
1772
|
+
check('通道与数据:删掉最后一个拐点会提交合法空 path(不是拒绝请求)',
|
|
1773
|
+
Array.isArray(waypointPath) && waypointPath.length === 0 &&
|
|
1774
|
+
emptyPathPreview.includes('"path": []') &&
|
|
1775
|
+
emptyPathPreview.includes('200 OK'),
|
|
1776
|
+
`path=${JSON.stringify(waypointPath)} / ${emptyPathPreview.slice(-120)}`)
|
|
1777
|
+
// 通道选中模式:切到 channel 之后,单击一条回线选中整条通道
|
|
1778
|
+
const channelLineTotal = channelView.getChannelGroups(menuPayload?.channelId).reduce((sum, group) => sum + group.lineIds.length, 0)
|
|
1779
|
+
const modeSelect = [...channelDemo.querySelectorAll('.dv-panel select')].find(node => [...node.options].some(option => option.value === 'channel'))
|
|
1780
|
+
if (modeSelect) {
|
|
1781
|
+
modeSelect.value = 'channel'
|
|
1782
|
+
modeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1783
|
+
}
|
|
1784
|
+
channelView.selectFeatures(null)
|
|
1785
|
+
await wait(200)
|
|
1786
|
+
const channelClick = (screen) => {
|
|
1787
|
+
const base = { bubbles: true, cancelable: true, composed: true, pointerId: 12, pointerType: 'mouse', isPrimary: true, clientX: channelBox.left + screen[0], clientY: channelBox.top + screen[1] }
|
|
1788
|
+
channelCanvas.dispatchEvent(new PointerEvent('pointerdown', { ...base, button: 0, buttons: 1 }))
|
|
1789
|
+
channelCanvas.dispatchEvent(new PointerEvent('pointerup', { ...base, button: 0, buttons: 0 }))
|
|
1790
|
+
}
|
|
1279
1791
|
channelClick(menuScreen)
|
|
1280
1792
|
await wait(240)
|
|
1281
1793
|
const modeInfo = channelView.getSelectionData()
|
|
1282
1794
|
check('通道与数据:channelSelectMode = channel 时单击一条回线选中整条通道', Boolean(modeSelect) && channelLineTotal > 0 && modeInfo.lineCount === channelLineTotal && modeInfo.channelCount === 1, '选中 ' + modeInfo.lineCount + ' / 通道共 ' + channelLineTotal + ' 回(模式 ' + channelView.getOptions().channelSelectMode + ')')
|
|
1283
|
-
//
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
check('通道与数据:删除选中先出删除载荷,待确认期间数据一个字节不动', Boolean(removeBox) && channelSetup.removeRequest?.phase === 'pending' && channelView.getData().lines.length === linesBeforeRemove, removeBox ? 'lineCount=' + channelView.getData().lines.length + ' / 选中 ' + (channelSetup.removeRequest?.counts?.lines ?? 0) + ' 回' : '确认框没弹出来')
|
|
1289
|
-
const removeActions = [...(removeBox?.querySelectorAll('.dv-channel-menu__actions button') ?? [])].map(node => node.textContent.trim())
|
|
1290
|
-
check('通道与数据:确认框给出「只删线路 / 整个分组 / 整条通道」三个范围', removeActions.some(text => text.includes('删除选中的线路')) && removeActions.some(text => text.includes('删除整个分组')) && removeActions.some(text => text.includes('删除整条通道')), removeActions.join(' / '))
|
|
1291
|
-
;[...(removeBox?.querySelectorAll('.dv-channel-menu__actions button') ?? [])].find(node => node.textContent.includes('删除整条通道'))?.click()
|
|
1292
|
-
const removedLines = await until(() => (channelView.getData().lines.length === linesBeforeRemove - channelLineTotal ? channelView.getData().lines.length : null), 6000)
|
|
1293
|
-
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)
|
|
1294
|
-
}
|
|
1295
|
-
else {
|
|
1296
|
-
check('通道与数据:画布与视图都起来了', false, '没等到 canvas 或 view')
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
// 2.8 编辑与保存(后端接口):分组增删改 / 拐点增删改 / 场站改坐标 —— 每一步生成一条待提交请求
|
|
1301
|
-
if (await clickTab('编辑与保存')) {
|
|
1302
|
-
const ready = await canvasReady()
|
|
1303
|
-
const demo = document.querySelector('.dv-demo')
|
|
1304
|
-
const setup = demo?.__vueParentComponent?.setupState
|
|
1305
|
-
const syncView = () => setup?.view ?? null
|
|
1306
|
-
const text = selector => demo.querySelector(selector)?.textContent.trim() ?? ''
|
|
1307
|
-
const preview = () => text('[data-demo="sync-preview"]')
|
|
1308
|
-
const queue = () => setup?.pending?.length ?? -1
|
|
1309
|
-
const firstChannelId = () => (syncView()?.getOptions().channels ?? [])[0]?.id
|
|
1310
|
-
const firstGroupPath = () => (syncView()?.getChannelGroups(firstChannelId()) ?? [])[0]?.path ?? []
|
|
1311
|
-
const firstChannelGroups = () => syncView()?.getChannelGroups(firstChannelId()) ?? []
|
|
1312
|
-
const spec = text('[data-demo="sync-spec"]')
|
|
1313
|
-
const fragments = ['/gis/points/{id}', '/gis/lines/{id}/path', '/gis/channels/{id}/groups/waypoints', '/gis/channels/{id}/groups', '/gis/documents']
|
|
1314
|
-
check('编辑与保存:接口约定把五条请求摊在面板上(场站 / 线路 / 通道组 / 分组 / 整份)', Boolean(ready) && Boolean(setup) && fragments.every(fragment => spec.includes(fragment)) && queue() === 0, text('[data-demo="sync-queue"]'))
|
|
1315
|
-
const groupReadout = () => setup?.waypointGroup ?? null
|
|
1316
|
-
const perLineWaypoints = () => (groupReadout()?.lines ?? []).map(line => line.waypoints.length)
|
|
1317
|
-
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} 条回线`)
|
|
1318
|
-
check('编辑与保存:整组读数的 scope=group、每条回线 derived=true(公共存储 / 各自派生分得开)', groupReadout()?.scope === 'group' && (groupReadout()?.lines ?? []).every(line => line.derived === true), `${groupReadout()?.scope} / ${(groupReadout()?.lines ?? []).length} 条回线`)
|
|
1319
|
-
// 场站位置:改坐标 → 数据真的动了,并生成一条 PATCH /gis/points/{id}
|
|
1320
|
-
const xBefore = syncView().getData().points[0].x
|
|
1321
|
-
const xInput = demo.querySelector('[data-demo="sync-station-x"]')
|
|
1322
|
-
xInput.value = String(Number(xBefore) + 0.5)
|
|
1323
|
-
xInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
1324
|
-
await wait(80)
|
|
1325
|
-
demo.querySelector('[data-demo="sync-station-move"]').click()
|
|
1326
|
-
await wait(220)
|
|
1327
|
-
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)}`)
|
|
1328
|
-
// 位置对账:getMovedPoints 记下改过位置的场站,clearMovedPoints 对账之后读数归零
|
|
1329
|
-
check('编辑与保存:位置对账读数(getMovedPoints)列出改过位置的场站', text('[data-demo="sync-moved"]').includes('个场站改过位置') && text('[data-demo="sync-moved"]').includes('→'), text('[data-demo="sync-moved"]'))
|
|
1330
|
-
demo.querySelector('[data-demo="sync-moved-clear"]').click()
|
|
1331
|
-
await wait(140)
|
|
1332
|
-
check('编辑与保存:clearMovedPoints 对账之后读数归零(发完接口记成新基准)', text('[data-demo="sync-moved"]').includes('没有场站改过位置') && queue() === 1, text('[data-demo="sync-moved"]'))
|
|
1333
|
-
// 拐点:加一个 → 分组骨架多一个顶点;请求体给的是整份 path
|
|
1334
|
-
const waypointsBefore = firstGroupPath().length
|
|
1335
|
-
demo.querySelector('[data-demo="sync-wp-add"]').click()
|
|
1336
|
-
await wait(240)
|
|
1337
|
-
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}`)
|
|
1338
|
-
check('编辑与保存:同组加拐点后每条回线都同步(读 getChannelGroupWaypoints().lines[].waypoints)', perLineWaypoints().length >= 2 && perLineWaypoints().every(count => count === firstGroupPath().length), `每条 ${perLineWaypoints().join(' / ')} 个拐点`)
|
|
1339
|
-
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))
|
|
1340
|
-
demo.querySelector('[data-demo=sync-pending-0]')?.click()
|
|
1341
|
-
await wait(120)
|
|
1342
|
-
check('编辑与保存:点待提交里的一条可以回看它的 body(预览切回那一条)', preview().includes('point.move') && text('[data-demo=sync-fetch]').includes('/gis/points/'), text('[data-demo=sync-preview]').slice(0, 80))
|
|
1343
|
-
// 改坐标:第 1 个拐点挪到别处(整份覆盖)
|
|
1344
|
-
const pathBefore = JSON.stringify(firstGroupPath())
|
|
1345
|
-
const pick = demo.querySelector('[data-demo="sync-wp-pick"]')
|
|
1346
|
-
pick.value = String(waypointsBefore + 1)
|
|
1347
|
-
pick.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1348
|
-
await wait(80)
|
|
1349
|
-
const wpX = demo.querySelector('[data-demo="sync-wp-x"]')
|
|
1350
|
-
wpX.value = String(Number(wpX.value) + 0.4)
|
|
1351
|
-
wpX.dispatchEvent(new Event('input', { bubbles: true }))
|
|
1352
|
-
await wait(80)
|
|
1353
|
-
demo.querySelector('[data-demo="sync-wp-update"]').click()
|
|
1354
|
-
await wait(240)
|
|
1355
|
-
check('编辑与保存:改拐点坐标走 setChannelPath / setLineWaypoints,整份 path 覆盖', JSON.stringify(firstGroupPath()) !== pathBefore && queue() === 3, `队列 ${queue()}`)
|
|
1356
|
-
// 删拐点:回到加点之前的顶点数
|
|
1357
|
-
demo.querySelector('[data-demo="sync-wp-remove"]').click()
|
|
1358
|
-
await wait(240)
|
|
1359
|
-
check('编辑与保存:删拐点走 removeWaypoint(pathIndex 就是读数里那个)', firstGroupPath().length === waypointsBefore && queue() === 4, `path=${firstGroupPath().length}`)
|
|
1360
|
-
// 分组:勾一条拆成新组 → 整份 groups 声明进请求体;再并回去
|
|
1361
|
-
const picks = [...demo.querySelectorAll('[data-demo="sync-group-picks"] input[type="checkbox"]')]
|
|
1362
|
-
picks[picks.length - 1]?.click()
|
|
1363
|
-
await wait(120)
|
|
1364
|
-
demo.querySelector('[data-demo="sync-group-add"]').click()
|
|
1365
|
-
await wait(260)
|
|
1366
|
-
check('编辑与保存:addChannelGroup 拆出新组 → 请求体是整份 channel.groups(id / lineIds / path)', firstChannelGroups().length === 2 && preview().includes('channel.groups') && preview().includes('"lineIds"') && preview().includes('"path"') && queue() === 5, `${firstChannelGroups().length} 组`)
|
|
1367
|
-
const secondGroup = firstChannelGroups()[1]
|
|
1368
|
-
demo.querySelector(`[data-demo="sync-group-remove-${secondGroup.id}"]`)?.click()
|
|
1369
|
-
await wait(260)
|
|
1370
|
-
check('编辑与保存:删除分组走 mergeChannelGroups(整组并回去,组数回到 1)', firstChannelGroups().length === 1 && preview().includes('channel.groups') && queue() === 6, `${firstChannelGroups().length} 组`)
|
|
1371
|
-
// 整份保存:document = exportData({ format: "full" })
|
|
1372
|
-
demo.querySelector('[data-demo="sync-save-document"]').click()
|
|
1373
|
-
await wait(400)
|
|
1374
|
-
check('编辑与保存:整份保存 PUT /gis/documents(document 是八列标准文档)', preview().includes('document.save') && preview().includes('gis-view') && queue() === 7, text('[data-demo="sync-queue"]'))
|
|
1375
|
-
// 提交:一次带上 baseVersion / opId,服务端版本 +1、队列清零
|
|
1376
|
-
const versionBefore = setup.server.version
|
|
1377
|
-
demo.querySelector('[data-demo="sync-submit"]').click()
|
|
1378
|
-
await wait(240)
|
|
1379
|
-
check('编辑与保存:提交整批 → 服务端版本 +1、队列清零', queue() === 0 && setup.server.version === versionBefore + 1, `v${versionBefore} → v${setup.server.version}`)
|
|
1380
|
-
// 409:勾「撞版本」再提交,队列留着,可以回灌服务端那一份
|
|
1381
|
-
demo.querySelector('[data-demo="sync-conflict"]').click()
|
|
1382
|
-
await wait(60)
|
|
1383
|
-
demo.querySelector('[data-demo="sync-station-move"]').click()
|
|
1384
|
-
await wait(200)
|
|
1385
|
-
demo.querySelector('[data-demo="sync-submit"]').click()
|
|
1386
|
-
await wait(240)
|
|
1387
|
-
check('编辑与保存:撞版本 → 409、队列原样留着', text('[data-demo="sync-conflict-text"]').includes('409') && queue() === 1, text('[data-demo="sync-conflict-text"]').slice(0, 80))
|
|
1388
|
-
demo.querySelector('[data-demo="sync-pull"]').click()
|
|
1389
|
-
await wait(320)
|
|
1390
|
-
check('编辑与保存:回灌服务端那一份走 importData,队列清空', queue() === 0 && text('[data-demo="sync-conflict-text"]').includes('回灌'), text('[data-demo="sync-conflict-text"]').slice(0, 60))
|
|
1391
|
-
// 视口导出:只出屏幕上看得见的那一撮
|
|
1392
|
-
demo.querySelector('[data-demo="sync-export-view"]').click()
|
|
1393
|
-
await wait(200)
|
|
1394
|
-
// 视口导出:只出屏幕上看得见的那一撮
|
|
1395
|
-
demo.querySelector('[data-demo="sync-export-view"]').click()
|
|
1396
|
-
await wait(200)
|
|
1397
|
-
check('编辑与保存:exportViewportData 出视口内的节点 / 线路 / 通道 + 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"]'))
|
|
1398
|
-
// 推送策略:默认攒批;「改完就传」按同一目标合并 + 延迟自动发;画布拖动只在松手落地时进队列
|
|
1399
|
-
const autoToggle = demo.querySelector('[data-demo="sync-auto"]')
|
|
1400
|
-
const pushMode = () => text('[data-demo="sync-push-mode"]')
|
|
1401
|
-
check('编辑与保存:推送策略面板默认攒批(改动只进待提交,点提交一起传)',
|
|
1402
|
-
Boolean(autoToggle) && setup.autoPush === false && pushMode().includes('攒批') && queue() === 0,
|
|
1403
|
-
pushMode())
|
|
1404
|
-
// 拖节点:中间态(start / drag)一条请求都不发,松手落地才进一条 point.move
|
|
1405
|
-
const dragCanvas = demo.querySelector('.dv-canvas canvas') ?? demo.querySelector('canvas')
|
|
1406
|
-
const dragBox = dragCanvas.getBoundingClientRect()
|
|
1407
|
-
const dragPoint = syncView().getData().points[0]
|
|
1408
|
-
const dragAt = syncView().project(dragPoint.x, dragPoint.y)
|
|
1409
|
-
const dragFrom = { clientX: dragBox.left + dragAt[0], clientY: dragBox.top + dragAt[1] }
|
|
1410
|
-
const dragTo = { clientX: dragFrom.clientX + 26, clientY: dragFrom.clientY + 14 }
|
|
1411
|
-
const dragFire = (type, at, buttons) => dragCanvas.dispatchEvent(new PointerEvent(type, {
|
|
1412
|
-
bubbles: true, cancelable: true, composed: true, pointerId: 81, pointerType: 'mouse', isPrimary: true, button: 0, buttons, ...at
|
|
1413
|
-
}))
|
|
1414
|
-
dragFire('pointerdown', dragFrom, 1)
|
|
1415
|
-
dragFire('pointermove', dragTo, 1)
|
|
1416
|
-
await wait(140)
|
|
1417
|
-
const dragMidQueue = queue()
|
|
1418
|
-
const dragMidText = text('[data-demo="sync-drag"]')
|
|
1419
|
-
dragFire('pointerup', dragTo, 0)
|
|
1420
|
-
await wait(220)
|
|
1421
|
-
check('编辑与保存:拖节点中间态一条请求都不发(start / drag 只写读数),松手落地才发一条 point.move',
|
|
1422
|
-
dragMidQueue === 0 && dragMidText.includes('拖动中') && queue() === 1 && text('[data-demo="sync-drag"]').includes('松手落地'),
|
|
1423
|
-
`中间 ${dragMidQueue} 条 → 松手 ${queue()} 条 / ${text('[data-demo="sync-drag"]')}`)
|
|
1424
|
-
demo.querySelector('[data-demo="sync-clear"]').click()
|
|
1425
|
-
await wait(120)
|
|
1426
|
-
// 改完就传:连改两次同一个场站 → 同一目标只留最后一份,延迟一小段自动发出
|
|
1427
|
-
autoToggle.click()
|
|
1428
|
-
await wait(80)
|
|
1429
|
-
check('编辑与保存:勾上「改完就传」→ 策略切到自动推送(同一目标合并成最后一份)',
|
|
1430
|
-
setup.autoPush === true && pushMode().includes('改完就传'),
|
|
1431
|
-
pushMode())
|
|
1432
|
-
const mergedBefore = setup.mergedCount
|
|
1433
|
-
demo.querySelector('[data-demo="sync-merge-demo"]').click()
|
|
1434
|
-
await wait(80)
|
|
1435
|
-
check('编辑与保存:连改两次同一个场站 → 同一目标只留最后一份(队列 1 条、合并省掉 1 条)',
|
|
1436
|
-
setup.mergedCount === mergedBefore + 1 && queue() === 1,
|
|
1437
|
-
`合并 ${setup.mergedCount - mergedBefore} 条 / 队列 ${queue()}`)
|
|
1438
|
-
const pushedBefore = setup.pushedCount
|
|
1439
|
-
const versionBeforeAuto = setup.server.version
|
|
1440
|
-
await wait(420)
|
|
1441
|
-
check('编辑与保存:改完就传延迟一小段自动发出(队列清零、服务端 +1)',
|
|
1442
|
-
queue() === 0 && setup.pushedCount === pushedBefore + 1 && setup.server.version === versionBeforeAuto + 1,
|
|
1443
|
-
`自动发出 ${setup.pushedCount - pushedBefore} 条 / v${versionBeforeAuto} → v${setup.server.version}`)
|
|
1444
|
-
autoToggle.click()
|
|
1445
|
-
await wait(80)
|
|
1446
|
-
check('编辑与保存:关掉「改完就传」→ 回到攒批(改动不再自动发)',
|
|
1447
|
-
setup.autoPush === false && pushMode().includes('攒批'),
|
|
1448
|
-
pushMode())
|
|
1449
|
-
// 通道拐点:声明与导出文档里的分组 path 跟着一起动(不再弹回旧骨架)
|
|
1450
|
-
const wpChannelId = firstChannelId()
|
|
1451
|
-
const livePath = () => (syncView().getChannelGroups(wpChannelId) ?? [])[0]?.path ?? []
|
|
1452
|
-
const declaredPath = () => (syncView().getOptions().channels ?? []).find(channel => String(channel.id) === String(wpChannelId))?.groups?.[0]?.path ?? []
|
|
1453
|
-
const exportedPath = () => (syncView().exportData({ format: 'full' }).channels ?? []).find(channel => String(channel.id) === String(wpChannelId))?.groups?.[0]?.path ?? []
|
|
1454
|
-
demo.querySelector('[data-demo="sync-wp-add"]').click()
|
|
1455
|
-
await wait(240)
|
|
1456
|
-
const liveWp = livePath()[0] ?? null
|
|
1457
|
-
check('编辑与保存:加通道拐点后声明与整份导出文档里的分组 path 都同步(不再弹回旧骨架)',
|
|
1458
|
-
livePath().length > 0 && declaredPath().length === livePath().length && exportedPath().length === livePath().length,
|
|
1459
|
-
`live ${livePath().length} / options ${declaredPath().length} / doc ${exportedPath().length}`)
|
|
1460
|
-
demo.querySelector('[data-demo="sync-wp-remove"]').click()
|
|
1461
|
-
await wait(240)
|
|
1462
|
-
demo.querySelector('[data-demo="sync-clear"]').click()
|
|
1463
|
-
await wait(120)
|
|
1464
|
-
// 组件口径:分过组的通道 setChannelPath 写第一组的声明(顶层 path 会被分组声明盖住)
|
|
1465
|
-
const setPathPoint = liveWp ? [liveWp[0] + 0.5, liveWp[1] + 0.5] : [0, 0]
|
|
1466
|
-
syncView().setChannelPath(wpChannelId, [setPathPoint])
|
|
1467
|
-
await wait(160)
|
|
1468
|
-
check('编辑与保存:setChannelPath 写分过组通道的第一组骨架(不是被分组声明盖住的顶层 path)',
|
|
1469
|
-
livePath().length === 1 && Math.abs(livePath()[0][0] - setPathPoint[0]) < 1e-9 && declaredPath().length === 1,
|
|
1470
|
-
`path ${JSON.stringify(livePath())}`)
|
|
1471
|
-
}
|
|
1472
|
-
else {
|
|
1473
|
-
check('编辑与保存:画布与视图都起来了', false, '没等到 canvas 或 view')
|
|
1474
|
-
}
|
|
1475
|
-
// 2.9 综合示例的「实例方法速查」面板:整份清单、搜索、点一条填表达式、求值、options 清单
|
|
1476
|
-
if (await clickTab('综合')) {
|
|
1477
|
-
const ready = await canvasReady()
|
|
1478
|
-
// 面板在弹层里(v-show):先切到「开关 · 历史」页签,再点出「实例方法速查」那一块
|
|
1479
|
-
;[...document.querySelectorAll('.demo-tabs button')].find(node => node.textContent.includes('开关'))?.click()
|
|
1480
|
-
await wait(80)
|
|
1481
|
-
;[...document.querySelectorAll('.demo-panel-buttons button')].find(node => node.textContent.includes('实例方法速查'))?.click()
|
|
1482
|
-
await wait(150)
|
|
1483
|
-
const apiItems = () => [...document.querySelectorAll('.demo-api-list:not(.demo-api-list--options) .demo-api-item')]
|
|
1484
|
-
const optionItems = () => [...document.querySelectorAll('.demo-api-list--options .demo-api-item')]
|
|
1485
|
-
check('实例方法速查:整份成员清单按分组列全(14 组 / 243 个成员)',
|
|
1486
|
-
Boolean(ready) && document.querySelectorAll('.demo-api-group').length === 14 && apiItems().length === 243,
|
|
1487
|
-
`${document.querySelectorAll('.demo-api-group').length} 组 / ${apiItems().length} 个成员`)
|
|
1488
|
-
const apiNames = apiItems().map(node => node.querySelector('b')?.textContent)
|
|
1489
|
-
const roundNames = ['registerSymbol', 'unregisterSymbol', 'listSymbols']
|
|
1490
|
-
check('实例方法速查:本轮新增的三个方法都在清单里(registerSymbol / unregisterSymbol / listSymbols)',
|
|
1491
|
-
roundNames.every(name => apiNames.includes(name)),
|
|
1492
|
-
roundNames.filter(name => !apiNames.includes(name)).join(' / ') || '三个都在')
|
|
1493
|
-
const roundNamesAuto = ['getZoomBaseScale', 'setZoomBaseScale']
|
|
1494
|
-
check('实例方法速查:自适应比例的读 / 写两个方法在清单里(getZoomBaseScale / setZoomBaseScale)',
|
|
1495
|
-
roundNamesAuto.every(name => apiNames.includes(name)),
|
|
1496
|
-
roundNamesAuto.filter(name => !apiNames.includes(name)).join(' / ') || '两个都在')
|
|
1497
|
-
const roundNames1597 = ['addChannelGroupLines', 'joinChannelGroupLines', 'endAddChannel']
|
|
1498
|
-
check('实例方法速查:这一轮拆出来的方法也在清单里(addChannelGroupLines / joinChannelGroupLines / endAddChannel)',
|
|
1499
|
-
roundNames1597.every(name => apiNames.includes(name)),
|
|
1500
|
-
roundNames1597.filter(name => !apiNames.includes(name)).join(' / ') || '三个都在')
|
|
1501
|
-
check('实例方法速查:options 折叠清单列全 87 个键', optionItems().length === 87, `${optionItems().length} 个键`)
|
|
1502
|
-
const apiSearch = document.querySelector('.demo-api-search input[type="search"]')
|
|
1503
|
-
apiSearch.value = 'focusOn'
|
|
1504
|
-
apiSearch.dispatchEvent(new Event('input', { bubbles: true }))
|
|
1505
|
-
await wait(80)
|
|
1506
|
-
const hitNames = apiItems().map(node => node.querySelector('b')?.textContent)
|
|
1507
|
-
check('实例方法速查:搜索按名字 / 参数 / 返回 / 说明命中', hitNames.includes('focusOn') && hitNames.length < 8, hitNames.join(' / '))
|
|
1508
|
-
apiItems().find(node => node.querySelector('b')?.textContent === 'focusOn')?.click()
|
|
1509
|
-
await wait(80)
|
|
1510
|
-
const apiDetail = document.querySelector('.demo-api-detail')?.textContent ?? ''
|
|
1511
|
-
const evalInput = document.querySelector('.demo-api-eval input[type="text"]')
|
|
1512
|
-
check('实例方法速查:点一条摆出参数与说明,并把 view.名字( 填进求值框',
|
|
1513
|
-
apiDetail.includes('focusOn') && apiDetail.includes('参数') && evalInput?.value === 'view.focusOn(',
|
|
1514
|
-
apiDetail.replace(/\s+/g, ' ').slice(0, 140))
|
|
1515
|
-
;[...document.querySelectorAll('.demo-api-eval button')].find(node => node.textContent.includes('示例'))?.click()
|
|
1516
|
-
await wait(240)
|
|
1517
|
-
const evalText = document.querySelector('.demo-api-eval + .demo-controls .demo-groups')?.textContent ?? ''
|
|
1518
|
-
check('实例方法速查:求值当场把结果打在面板上(示例 = listPoints 读数)', evalText.includes('index'), evalText.replace(/\s+/g, ' ').slice(0, 140))
|
|
1519
|
-
// 2.10 空白处右键新增场站:画布菜单 → demo 自己写的表单 → addPoint
|
|
1520
|
-
const stationCanvas = document.querySelector('canvas.gis-view__canvas')
|
|
1521
|
-
const stationRect = stationCanvas.getBoundingClientRect()
|
|
1522
|
-
// 数据基本铺满画布:候选点按「越可能是空白」排前面(综合示例只有右下角一带是数据外沿)
|
|
1523
|
-
const blankSpots = [
|
|
1524
|
-
[stationRect.right - 8, stationRect.bottom - 8],
|
|
1525
|
-
[stationRect.right - 8, stationRect.bottom - 30],
|
|
1526
|
-
[stationRect.right - 30, stationRect.bottom - 8],
|
|
1527
|
-
[stationRect.right - 8, stationRect.top + 8],
|
|
1528
|
-
[stationRect.left + 8, stationRect.top + 8],
|
|
1529
|
-
[stationRect.left + stationRect.width / 2, stationRect.bottom - 8]
|
|
1530
|
-
]
|
|
1531
|
-
let stationMenu = null
|
|
1532
|
-
for (const [x, y] of blankSpots) {
|
|
1533
|
-
contextMenuOn(stationCanvas, x, y)
|
|
1534
|
-
const opened = await until(() => document.querySelector('.demo-canvas-menu'), 1200)
|
|
1535
|
-
if (opened && opened.textContent.includes('空白处')) {
|
|
1536
|
-
stationMenu = opened
|
|
1537
|
-
break
|
|
1538
|
-
}
|
|
1539
|
-
opened?.querySelector('.demo-channel-menu-head button')?.click()
|
|
1540
|
-
await wait(60)
|
|
1795
|
+
// 分组拐点拖动模式:下拉更新 option,准备按钮拆两组并各加两个拐点
|
|
1796
|
+
const waypointDragModeSelect = channelDemo.querySelector('[data-demo="channel-waypoint-drag-mode"]')
|
|
1797
|
+
if (waypointDragModeSelect) {
|
|
1798
|
+
waypointDragModeSelect.value = 'vertex'
|
|
1799
|
+
waypointDragModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1541
1800
|
}
|
|
1542
|
-
check('画布菜单:空白处右键弹出自绘菜单,并多了一行「新增场站」', Boolean(stationMenu) && stationMenu.textContent.includes('新增场站'), stationMenu ? stationMenu.textContent.replace(/\s+/g, ' ').slice(0, 140) : '候选点都命中了要素')
|
|
1543
|
-
const stationRow = stationMenu?.querySelector('[data-demo="canvas-menu-addStation"]')
|
|
1544
|
-
check('画布菜单:新增场站那一行带 data-demo 钩子、标签点名 addPoint', Boolean(stationRow) && stationRow.textContent.includes('addPoint'), stationRow?.textContent ?? '没有这一行')
|
|
1545
|
-
const pointsBeforeStation = Number((document.querySelector('.demo-overlay')?.textContent ?? '').match(/(\d+)\s*点/)?.[1] ?? 0)
|
|
1546
|
-
stationRow?.click()
|
|
1547
1801
|
await wait(160)
|
|
1548
|
-
const
|
|
1549
|
-
|
|
1550
|
-
const
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
const
|
|
1563
|
-
check('
|
|
1564
|
-
const
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1802
|
+
const waypointDragModeApplied = channelView.getOptions().channelWaypointDragMode === 'vertex'
|
|
1803
|
+
channelDemo.querySelector('[data-demo="channel-waypoint-drag-prepare"]')?.click()
|
|
1804
|
+
const preparedWaypointGroups = await until(() => {
|
|
1805
|
+
const channelId = channelView.getChannels()[0]?.channelId
|
|
1806
|
+
const groups = channelView.getChannelGroups(channelId)
|
|
1807
|
+
return groups.length === 2 && groups.every(group => group.path.length === 2) ? groups : null
|
|
1808
|
+
}, 5000)
|
|
1809
|
+
check('通道与数据:分组拐点拖动可切 vertex,准备按钮拆两组且每组两个拐点',
|
|
1810
|
+
Boolean(waypointDragModeSelect) && Boolean(preparedWaypointGroups) && waypointDragModeApplied && channelField('channel-waypoint-drag-text').includes('已准备 2 组'),
|
|
1811
|
+
`${channelView.getOptions().channelWaypointDragMode} / ${(preparedWaypointGroups ?? []).map(group => group.path.length).join(' / ')} / ${channelField('channel-waypoint-drag-text')}`)
|
|
1812
|
+
// 删除:requestRemove 先出载荷(待确认期间数据不动),确认框由 demo 自己按载荷画
|
|
1813
|
+
const linesBeforeRemove = channelView.getData().lines.length
|
|
1814
|
+
const channelsBeforeRemove = channelView.getOptions().channels.length
|
|
1815
|
+
;[...channelDemo.querySelectorAll('.dv-panel button')].find(node => node.textContent.includes('删除选中'))?.click()
|
|
1816
|
+
const removeBox = await until(() => channelDemo.querySelector('.dv-remove-box'), 5000)
|
|
1817
|
+
check('通道与数据:删除选中先出删除载荷,待确认期间数据一个字节不动', Boolean(removeBox) && channelSetup.removeRequest?.phase === 'pending' && channelView.getData().lines.length === linesBeforeRemove, removeBox ? 'lineCount=' + channelView.getData().lines.length + ' / 选中 ' + (channelSetup.removeRequest?.counts?.lines ?? 0) + ' 回' : '确认框没弹出来')
|
|
1818
|
+
const removeActions = [...(removeBox?.querySelectorAll('.dv-channel-menu__actions button') ?? [])].map(node => node.textContent.trim())
|
|
1819
|
+
check('通道与数据:确认框给出「只删线路 / 整个分组 / 整条通道」三个范围', removeActions.some(text => text.includes('删除选中的线路')) && removeActions.some(text => text.includes('删除整个分组')) && removeActions.some(text => text.includes('删除整条通道')), removeActions.join(' / '))
|
|
1820
|
+
;[...(removeBox?.querySelectorAll('.dv-channel-menu__actions button') ?? [])].find(node => node.textContent.includes('删除整条通道'))?.click()
|
|
1821
|
+
const removedLines = await until(() => (channelView.getData().lines.length === linesBeforeRemove - channelLineTotal ? channelView.getData().lines.length : null), 6000)
|
|
1822
|
+
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)
|
|
1823
|
+
const dtoButton = channelDemo.querySelector('[data-demo="channel-export-dto"]')
|
|
1824
|
+
check('通道与数据:导出区给出自定义格式按钮(defineExportFormat)', Boolean(dtoButton) && dtoButton.textContent.includes('后端 DTO'), dtoButton?.textContent.trim() ?? '没有按钮')
|
|
1825
|
+
dtoButton?.click()
|
|
1826
|
+
await wait(120)
|
|
1827
|
+
const dtoReadout = channelField('channel-export-readout')
|
|
1828
|
+
check('通道与数据:后端 DTO 按钮按注册的标准文档映射出业务列,并列出当前格式表',
|
|
1829
|
+
dtoReadout.includes('channel-backend-dto') && dtoReadout.includes('当前可注册表') && dtoReadout.includes('view / new / test / input / full / channel-backend-dto'),
|
|
1830
|
+
dtoReadout.slice(0, 180))
|
|
1831
|
+
}
|
|
1832
|
+
else {
|
|
1833
|
+
check('通道与数据:画布与视图都起来了', false, '没等到 canvas 或 view')
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
|
|
1837
|
+
// 2.8 编辑与保存(后端接口):分组增删改 / 拐点增删改 / 场站改坐标 —— 每一步生成一条待提交请求
|
|
1838
|
+
if (await clickTab('编辑与保存')) {
|
|
1839
|
+
const ready = await canvasReady()
|
|
1840
|
+
const demo = document.querySelector('.dv-demo')
|
|
1841
|
+
const setup = demo?.__vueParentComponent?.setupState
|
|
1842
|
+
const syncView = () => setup?.view ?? null
|
|
1843
|
+
const text = selector => demo.querySelector(selector)?.textContent.trim() ?? ''
|
|
1844
|
+
const preview = () => text('[data-demo="sync-preview"]')
|
|
1845
|
+
const queue = () => setup?.pending?.length ?? -1
|
|
1846
|
+
const firstChannelId = () => (syncView()?.getOptions().channels ?? [])[0]?.id
|
|
1847
|
+
const firstGroupPath = () => (syncView()?.getChannelGroups(firstChannelId()) ?? [])[0]?.path ?? []
|
|
1848
|
+
const firstChannelGroups = () => syncView()?.getChannelGroups(firstChannelId()) ?? []
|
|
1849
|
+
const spec = text('[data-demo="sync-spec"]')
|
|
1850
|
+
const fragments = ['/gis/points/{id}', '/gis/lines/{id}/path', '/gis/channels/{id}/groups/waypoints', '/gis/channels/{id}/groups', '/gis/documents']
|
|
1851
|
+
check('编辑与保存:接口约定把五条请求摊在面板上(场站 / 线路 / 通道组 / 分组 / 整份)', Boolean(ready) && Boolean(setup) && fragments.every(fragment => spec.includes(fragment)) && queue() === 0, text('[data-demo="sync-queue"]'))
|
|
1852
|
+
const groupReadout = () => setup?.waypointGroup ?? null
|
|
1853
|
+
const perLineWaypoints = () => (groupReadout()?.lines ?? []).map(line => line.waypoints.length)
|
|
1854
|
+
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} 条回线`)
|
|
1855
|
+
check('编辑与保存:整组读数的 scope=group、每条回线 derived=true(公共存储 / 各自派生分得开)', groupReadout()?.scope === 'group' && (groupReadout()?.lines ?? []).every(line => line.derived === true), `${groupReadout()?.scope} / ${(groupReadout()?.lines ?? []).length} 条回线`)
|
|
1856
|
+
// 场站位置:改坐标 → 数据真的动了,并生成一条 PATCH /gis/points/{id}
|
|
1857
|
+
const xBefore = syncView().getData().points[0].x
|
|
1858
|
+
const xInput = demo.querySelector('[data-demo="sync-station-x"]')
|
|
1859
|
+
xInput.value = String(Number(xBefore) + 0.5)
|
|
1860
|
+
xInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
1861
|
+
await wait(80)
|
|
1862
|
+
demo.querySelector('[data-demo="sync-station-move"]').click()
|
|
1863
|
+
await wait(220)
|
|
1864
|
+
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)}`)
|
|
1865
|
+
// 位置对账:getMovedPoints 记下改过位置的场站,clearMovedPoints 对账之后读数归零
|
|
1866
|
+
check('编辑与保存:位置对账读数(getMovedPoints)列出改过位置的场站', text('[data-demo="sync-moved"]').includes('个场站改过位置') && text('[data-demo="sync-moved"]').includes('→'), text('[data-demo="sync-moved"]'))
|
|
1867
|
+
demo.querySelector('[data-demo="sync-moved-clear"]').click()
|
|
1868
|
+
await wait(140)
|
|
1869
|
+
check('编辑与保存:clearMovedPoints 对账之后读数归零(发完接口记成新基准)', text('[data-demo="sync-moved"]').includes('没有场站改过位置') && queue() === 1, text('[data-demo="sync-moved"]'))
|
|
1870
|
+
// 拐点:加一个 → 分组骨架多一个顶点;请求体给的是整份 path
|
|
1871
|
+
const waypointsBefore = firstGroupPath().length
|
|
1872
|
+
demo.querySelector('[data-demo="sync-wp-add"]').click()
|
|
1873
|
+
await wait(240)
|
|
1874
|
+
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}`)
|
|
1875
|
+
check('编辑与保存:同组加拐点后每条回线都同步(读 getChannelGroupWaypoints().lines[].waypoints)', perLineWaypoints().length >= 2 && perLineWaypoints().every(count => count === firstGroupPath().length), `每条 ${perLineWaypoints().join(' / ')} 个拐点`)
|
|
1876
|
+
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))
|
|
1877
|
+
demo.querySelector('[data-demo=sync-pending-0]')?.click()
|
|
1878
|
+
await wait(120)
|
|
1879
|
+
check('编辑与保存:点待提交里的一条可以回看它的 body(预览切回那一条)', preview().includes('point.move') && text('[data-demo=sync-fetch]').includes('/gis/points/'), text('[data-demo=sync-preview]').slice(0, 80))
|
|
1880
|
+
// 改坐标:第 1 个拐点挪到别处(整份覆盖)
|
|
1881
|
+
const pathBefore = JSON.stringify(firstGroupPath())
|
|
1882
|
+
const pick = demo.querySelector('[data-demo="sync-wp-pick"]')
|
|
1883
|
+
pick.value = String(waypointsBefore + 1)
|
|
1884
|
+
pick.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1885
|
+
await wait(80)
|
|
1886
|
+
const wpX = demo.querySelector('[data-demo="sync-wp-x"]')
|
|
1887
|
+
wpX.value = String(Number(wpX.value) + 0.4)
|
|
1888
|
+
wpX.dispatchEvent(new Event('input', { bubbles: true }))
|
|
1889
|
+
await wait(80)
|
|
1890
|
+
demo.querySelector('[data-demo="sync-wp-update"]').click()
|
|
1891
|
+
await wait(240)
|
|
1892
|
+
check('编辑与保存:改拐点坐标走 setChannelPath / setLineWaypoints,整份 path 覆盖', JSON.stringify(firstGroupPath()) !== pathBefore && queue() === 3, `队列 ${queue()}`)
|
|
1893
|
+
// 删拐点:回到加点之前的顶点数
|
|
1894
|
+
demo.querySelector('[data-demo="sync-wp-remove"]').click()
|
|
1895
|
+
await wait(240)
|
|
1896
|
+
check('编辑与保存:删拐点走 removeWaypoint(pathIndex 就是读数里那个)', firstGroupPath().length === waypointsBefore && queue() === 4, `path=${firstGroupPath().length}`)
|
|
1897
|
+
// 分组:勾一条拆成新组 → 整份 groups 声明进请求体;再并回去
|
|
1898
|
+
const picks = [...demo.querySelectorAll('[data-demo="sync-group-picks"] input[type="checkbox"]')]
|
|
1899
|
+
picks[picks.length - 1]?.click()
|
|
1900
|
+
await wait(120)
|
|
1901
|
+
demo.querySelector('[data-demo="sync-group-add"]').click()
|
|
1902
|
+
await wait(260)
|
|
1903
|
+
check('编辑与保存:addChannelGroup 拆出新组 → 请求体是整份 channel.groups(id / lineIds / path)', firstChannelGroups().length === 2 && preview().includes('channel.groups') && preview().includes('"lineIds"') && preview().includes('"path"') && queue() === 5, `${firstChannelGroups().length} 组`)
|
|
1904
|
+
const secondGroup = firstChannelGroups()[1]
|
|
1905
|
+
demo.querySelector(`[data-demo="sync-group-remove-${secondGroup.id}"]`)?.click()
|
|
1906
|
+
await wait(260)
|
|
1907
|
+
check('编辑与保存:删除分组走 mergeChannelGroups(整组并回去,组数回到 1)', firstChannelGroups().length === 1 && preview().includes('channel.groups') && queue() === 6, `${firstChannelGroups().length} 组`)
|
|
1908
|
+
// 整份保存:document = exportData({ format: "full" })
|
|
1909
|
+
demo.querySelector('[data-demo="sync-save-document"]').click()
|
|
1910
|
+
await wait(400)
|
|
1911
|
+
check('编辑与保存:整份保存 PUT /gis/documents(document 是八列标准文档)', preview().includes('document.save') && preview().includes('gis-view') && queue() === 7, text('[data-demo="sync-queue"]'))
|
|
1912
|
+
// 提交:一次带上 baseVersion / opId,服务端版本 +1、队列清零
|
|
1913
|
+
const versionBefore = setup.server.version
|
|
1914
|
+
demo.querySelector('[data-demo="sync-submit"]').click()
|
|
1915
|
+
await wait(240)
|
|
1916
|
+
check('编辑与保存:提交整批 → 服务端版本 +1、队列清零', queue() === 0 && setup.server.version === versionBefore + 1, `v${versionBefore} → v${setup.server.version}`)
|
|
1917
|
+
// 409:勾「撞版本」再提交,队列留着,可以回灌服务端那一份
|
|
1918
|
+
demo.querySelector('[data-demo="sync-conflict"]').click()
|
|
1919
|
+
await wait(60)
|
|
1920
|
+
demo.querySelector('[data-demo="sync-station-move"]').click()
|
|
1921
|
+
await wait(200)
|
|
1922
|
+
demo.querySelector('[data-demo="sync-submit"]').click()
|
|
1923
|
+
await wait(240)
|
|
1924
|
+
check('编辑与保存:撞版本 → 409、队列原样留着', text('[data-demo="sync-conflict-text"]').includes('409') && queue() === 1, text('[data-demo="sync-conflict-text"]').slice(0, 80))
|
|
1925
|
+
demo.querySelector('[data-demo="sync-pull"]').click()
|
|
1926
|
+
await wait(320)
|
|
1927
|
+
check('编辑与保存:回灌服务端那一份走 importData,队列清空', queue() === 0 && text('[data-demo="sync-conflict-text"]').includes('回灌'), text('[data-demo="sync-conflict-text"]').slice(0, 60))
|
|
1928
|
+
// 视口导出:只出屏幕上看得见的那一撮
|
|
1929
|
+
demo.querySelector('[data-demo="sync-export-view"]').click()
|
|
1930
|
+
await wait(200)
|
|
1931
|
+
// 视口导出:只出屏幕上看得见的那一撮
|
|
1932
|
+
demo.querySelector('[data-demo="sync-export-view"]').click()
|
|
1933
|
+
await wait(200)
|
|
1934
|
+
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"]'))
|
|
1935
|
+
// 推送策略:默认攒批;「改完就传」按同一目标合并 + 延迟自动发;画布拖动只在松手落地时进队列
|
|
1936
|
+
const autoToggle = demo.querySelector('[data-demo="sync-auto"]')
|
|
1937
|
+
const pushMode = () => text('[data-demo="sync-push-mode"]')
|
|
1938
|
+
check('编辑与保存:推送策略面板默认攒批(改动只进待提交,点提交一起传)',
|
|
1939
|
+
Boolean(autoToggle) && setup.autoPush === false && pushMode().includes('攒批') && queue() === 0,
|
|
1940
|
+
pushMode())
|
|
1941
|
+
// 拖节点:中间态(start / drag)一条请求都不发,松手落地才进一条 point.move
|
|
1942
|
+
const dragCanvas = demo.querySelector('.dv-canvas canvas') ?? demo.querySelector('canvas')
|
|
1943
|
+
const dragBox = dragCanvas.getBoundingClientRect()
|
|
1944
|
+
const dragPoint = syncView().getData().points[0]
|
|
1945
|
+
const dragAt = syncView().project(dragPoint.x, dragPoint.y)
|
|
1946
|
+
const dragFrom = { clientX: dragBox.left + dragAt[0], clientY: dragBox.top + dragAt[1] }
|
|
1947
|
+
const dragTo = { clientX: dragFrom.clientX + 26, clientY: dragFrom.clientY + 14 }
|
|
1948
|
+
const dragFire = (type, at, buttons) => dragCanvas.dispatchEvent(new PointerEvent(type, {
|
|
1949
|
+
bubbles: true, cancelable: true, composed: true, pointerId: 81, pointerType: 'mouse', isPrimary: true, button: 0, buttons, ...at
|
|
1950
|
+
}))
|
|
1951
|
+
dragFire('pointerdown', dragFrom, 1)
|
|
1952
|
+
dragFire('pointermove', dragTo, 1)
|
|
1953
|
+
await wait(140)
|
|
1954
|
+
const dragMidQueue = queue()
|
|
1955
|
+
const dragMidText = text('[data-demo="sync-drag"]')
|
|
1956
|
+
dragFire('pointerup', dragTo, 0)
|
|
1957
|
+
await wait(220)
|
|
1958
|
+
check('编辑与保存:拖节点中间态一条请求都不发(start / drag 只写读数),松手落地才发一条 point.move',
|
|
1959
|
+
dragMidQueue === 0 && dragMidText.includes('拖动中') && queue() === 1 && text('[data-demo="sync-drag"]').includes('松手落地'),
|
|
1960
|
+
`中间 ${dragMidQueue} 条 → 松手 ${queue()} 条 / ${text('[data-demo="sync-drag"]')}`)
|
|
1961
|
+
demo.querySelector('[data-demo="sync-clear"]').click()
|
|
1962
|
+
await wait(120)
|
|
1963
|
+
// 改完就传:连改两次同一个场站 → 同一目标只留最后一份,延迟一小段自动发出
|
|
1964
|
+
autoToggle.click()
|
|
1965
|
+
await wait(80)
|
|
1966
|
+
check('编辑与保存:勾上「改完就传」→ 策略切到自动推送(同一目标合并成最后一份)',
|
|
1967
|
+
setup.autoPush === true && pushMode().includes('改完就传'),
|
|
1968
|
+
pushMode())
|
|
1969
|
+
const mergedBefore = setup.mergedCount
|
|
1970
|
+
demo.querySelector('[data-demo="sync-merge-demo"]').click()
|
|
1971
|
+
await wait(80)
|
|
1972
|
+
check('编辑与保存:连改两次同一个场站 → 同一目标只留最后一份(队列 1 条、合并省掉 1 条)',
|
|
1973
|
+
setup.mergedCount === mergedBefore + 1 && queue() === 1,
|
|
1974
|
+
`合并 ${setup.mergedCount - mergedBefore} 条 / 队列 ${queue()}`)
|
|
1975
|
+
const pushedBefore = setup.pushedCount
|
|
1976
|
+
const versionBeforeAuto = setup.server.version
|
|
1977
|
+
await wait(420)
|
|
1978
|
+
check('编辑与保存:改完就传延迟一小段自动发出(队列清零、服务端 +1)',
|
|
1979
|
+
queue() === 0 && setup.pushedCount === pushedBefore + 1 && setup.server.version === versionBeforeAuto + 1,
|
|
1980
|
+
`自动发出 ${setup.pushedCount - pushedBefore} 条 / v${versionBeforeAuto} → v${setup.server.version}`)
|
|
1981
|
+
autoToggle.click()
|
|
1982
|
+
await wait(80)
|
|
1983
|
+
check('编辑与保存:关掉「改完就传」→ 回到攒批(改动不再自动发)',
|
|
1984
|
+
setup.autoPush === false && pushMode().includes('攒批'),
|
|
1985
|
+
pushMode())
|
|
1986
|
+
// 通道拐点:声明与导出文档里的分组 path 跟着一起动(不再弹回旧骨架)
|
|
1987
|
+
const wpChannelId = firstChannelId()
|
|
1988
|
+
const livePath = () => (syncView().getChannelGroups(wpChannelId) ?? [])[0]?.path ?? []
|
|
1989
|
+
const declaredPath = () => (syncView().getOptions().channels ?? []).find(channel => String(channel.id) === String(wpChannelId))?.groups?.[0]?.path ?? []
|
|
1990
|
+
const exportedPath = () => (syncView().exportData({ format: 'full' }).channels ?? []).find(channel => String(channel.id) === String(wpChannelId))?.groups?.[0]?.path ?? []
|
|
1991
|
+
demo.querySelector('[data-demo="sync-wp-add"]').click()
|
|
1992
|
+
await wait(240)
|
|
1993
|
+
const liveWp = livePath()[0] ?? null
|
|
1994
|
+
check('编辑与保存:加通道拐点后声明与整份导出文档里的分组 path 都同步(不再弹回旧骨架)',
|
|
1995
|
+
livePath().length > 0 && declaredPath().length === livePath().length && exportedPath().length === livePath().length,
|
|
1996
|
+
`live ${livePath().length} / options ${declaredPath().length} / doc ${exportedPath().length}`)
|
|
1997
|
+
demo.querySelector('[data-demo="sync-wp-remove"]').click()
|
|
1998
|
+
await wait(240)
|
|
1999
|
+
demo.querySelector('[data-demo="sync-clear"]').click()
|
|
2000
|
+
await wait(120)
|
|
2001
|
+
// 组件口径:分过组的通道 setChannelPath 写第一组的声明(顶层 path 会被分组声明盖住)
|
|
2002
|
+
const setPathPoint = liveWp ? [liveWp[0] + 0.5, liveWp[1] + 0.5] : [0, 0]
|
|
2003
|
+
syncView().setChannelPath(wpChannelId, [setPathPoint])
|
|
2004
|
+
await wait(160)
|
|
2005
|
+
check('编辑与保存:setChannelPath 写分过组通道的第一组骨架(不是被分组声明盖住的顶层 path)',
|
|
2006
|
+
livePath().length === 1 && Math.abs(livePath()[0][0] - setPathPoint[0]) < 1e-9 && declaredPath().length === 1,
|
|
2007
|
+
`path ${JSON.stringify(livePath())}`)
|
|
2008
|
+
}
|
|
2009
|
+
else {
|
|
2010
|
+
check('编辑与保存:画布与视图都起来了', false, '没等到 canvas 或 view')
|
|
2011
|
+
}
|
|
2012
|
+
// 2.9 综合示例的「实例方法速查」面板:整份清单、搜索、点一条填表达式、求值、options 清单
|
|
2013
|
+
if (await clickTab('综合')) {
|
|
2014
|
+
const ready = await canvasReady()
|
|
2015
|
+
// 面板在弹层里(v-show):先切到「开关 · 历史」页签,再点出「实例方法速查」那一块
|
|
2016
|
+
;[...document.querySelectorAll('.demo-tabs button')].find(node => node.textContent.includes('开关'))?.click()
|
|
2017
|
+
await wait(80)
|
|
2018
|
+
;[...document.querySelectorAll('.demo-panel-buttons button')].find(node => node.textContent.includes('实例方法速查'))?.click()
|
|
2019
|
+
await wait(150)
|
|
2020
|
+
const apiItems = () => [...document.querySelectorAll('.demo-api-list:not(.demo-api-list--options) .demo-api-item')]
|
|
2021
|
+
const optionItems = () => [...document.querySelectorAll('.demo-api-list--options .demo-api-item')]
|
|
2022
|
+
check('实例方法速查:整份成员清单按分组列全(16 组 / 249 个成员)',
|
|
2023
|
+
Boolean(ready) && document.querySelectorAll('.demo-api-group').length === 16 && apiItems().length === 249,
|
|
2024
|
+
`${document.querySelectorAll('.demo-api-group').length} 组 / ${apiItems().length} 个成员`)
|
|
2025
|
+
const apiNames = apiItems().map(node => node.querySelector('b')?.textContent)
|
|
2026
|
+
const roundNames = ['registerSymbol', 'unregisterSymbol', 'listSymbols']
|
|
2027
|
+
check('实例方法速查:本轮新增的三个方法都在清单里(registerSymbol / unregisterSymbol / listSymbols)',
|
|
2028
|
+
roundNames.every(name => apiNames.includes(name)),
|
|
2029
|
+
roundNames.filter(name => !apiNames.includes(name)).join(' / ') || '三个都在')
|
|
2030
|
+
const roundNamesAuto = ['getZoomBaseScale', 'setZoomBaseScale']
|
|
2031
|
+
check('实例方法速查:自适应比例的读 / 写两个方法在清单里(getZoomBaseScale / setZoomBaseScale)',
|
|
2032
|
+
roundNamesAuto.every(name => apiNames.includes(name)),
|
|
2033
|
+
roundNamesAuto.filter(name => !apiNames.includes(name)).join(' / ') || '两个都在')
|
|
2034
|
+
const roundNames1597 = ['addChannelGroupLines', 'joinChannelGroupLines', 'endAddChannel']
|
|
2035
|
+
check('实例方法速查:这一轮拆出来的方法也在清单里(addChannelGroupLines / joinChannelGroupLines / endAddChannel)',
|
|
2036
|
+
roundNames1597.every(name => apiNames.includes(name)),
|
|
2037
|
+
roundNames1597.filter(name => !apiNames.includes(name)).join(' / ') || '三个都在')
|
|
2038
|
+
check('实例方法速查:options 折叠清单列全 92 个键', optionItems().length === 92, `${optionItems().length} 个键`)
|
|
2039
|
+
const apiSearch = document.querySelector('.demo-api-search input[type="search"]')
|
|
2040
|
+
apiSearch.value = 'focusOn'
|
|
2041
|
+
apiSearch.dispatchEvent(new Event('input', { bubbles: true }))
|
|
2042
|
+
await wait(80)
|
|
2043
|
+
const hitNames = apiItems().map(node => node.querySelector('b')?.textContent)
|
|
2044
|
+
check('实例方法速查:搜索按名字 / 参数 / 返回 / 说明命中', hitNames.includes('focusOn') && hitNames.length < 8, hitNames.join(' / '))
|
|
2045
|
+
apiItems().find(node => node.querySelector('b')?.textContent === 'focusOn')?.click()
|
|
2046
|
+
await wait(80)
|
|
2047
|
+
const apiDetail = document.querySelector('.demo-api-detail')?.textContent ?? ''
|
|
2048
|
+
const evalInput = document.querySelector('.demo-api-eval input[type="text"]')
|
|
2049
|
+
check('实例方法速查:点一条摆出参数与说明,并把 view.名字( 填进求值框',
|
|
2050
|
+
apiDetail.includes('focusOn') && apiDetail.includes('参数') && evalInput?.value === 'view.focusOn(',
|
|
2051
|
+
apiDetail.replace(/\s+/g, ' ').slice(0, 140))
|
|
2052
|
+
;[...document.querySelectorAll('.demo-api-eval button')].find(node => node.textContent.includes('示例'))?.click()
|
|
2053
|
+
await wait(240)
|
|
2054
|
+
const evalText = document.querySelector('.demo-api-eval + .demo-controls .demo-groups')?.textContent ?? ''
|
|
2055
|
+
check('实例方法速查:求值当场把结果打在面板上(示例 = listPoints 读数)', evalText.includes('index'), evalText.replace(/\s+/g, ' ').slice(0, 140))
|
|
2056
|
+
// 2.10 空白处右键新增场站:画布菜单 → demo 自己写的表单 → addPoint
|
|
2057
|
+
const stationCanvas = document.querySelector('canvas.gis-view__canvas')
|
|
2058
|
+
const stationRect = stationCanvas.getBoundingClientRect()
|
|
2059
|
+
// 数据基本铺满画布:候选点按「越可能是空白」排前面(综合示例只有右下角一带是数据外沿)
|
|
2060
|
+
const blankSpots = [
|
|
2061
|
+
[stationRect.right - 8, stationRect.bottom - 8],
|
|
2062
|
+
[stationRect.right - 8, stationRect.bottom - 30],
|
|
2063
|
+
[stationRect.right - 30, stationRect.bottom - 8],
|
|
2064
|
+
[stationRect.right - 8, stationRect.top + 8],
|
|
2065
|
+
[stationRect.left + 8, stationRect.top + 8],
|
|
2066
|
+
[stationRect.left + stationRect.width / 2, stationRect.bottom - 8]
|
|
2067
|
+
]
|
|
2068
|
+
let stationMenu = null
|
|
2069
|
+
for (const [x, y] of blankSpots) {
|
|
2070
|
+
contextMenuOn(stationCanvas, x, y)
|
|
2071
|
+
const opened = await until(() => document.querySelector('.demo-canvas-menu'), 1200)
|
|
2072
|
+
if (opened && opened.textContent.includes('空白处')) {
|
|
2073
|
+
stationMenu = opened
|
|
2074
|
+
break
|
|
2075
|
+
}
|
|
2076
|
+
opened?.querySelector('.demo-channel-menu-head button')?.click()
|
|
2077
|
+
await wait(60)
|
|
2078
|
+
}
|
|
2079
|
+
check('画布菜单:空白处右键弹出自绘菜单,并多了一行「新增场站」', Boolean(stationMenu) && stationMenu.textContent.includes('新增场站'), stationMenu ? stationMenu.textContent.replace(/\s+/g, ' ').slice(0, 140) : '候选点都命中了要素')
|
|
2080
|
+
const stationRow = stationMenu?.querySelector('[data-demo="canvas-menu-addStation"]')
|
|
2081
|
+
check('画布菜单:新增场站那一行带 data-demo 钩子、标签点名 addPoint', Boolean(stationRow) && stationRow.textContent.includes('addPoint'), stationRow?.textContent ?? '没有这一行')
|
|
2082
|
+
const pointsBeforeStation = Number((document.querySelector('.demo-overlay')?.textContent ?? '').match(/(\d+)\s*点/)?.[1] ?? 0)
|
|
2083
|
+
stationRow?.click()
|
|
2084
|
+
await wait(160)
|
|
2085
|
+
const stationConfirm = document.querySelector('[data-demo="add-station-confirm"]')
|
|
2086
|
+
const stationIdInput = document.querySelector('[data-demo="add-station-id"]')
|
|
2087
|
+
const stationNameInput = document.querySelector('[data-demo="add-station-name"]')
|
|
2088
|
+
const stationXInput = document.querySelector('[data-demo="add-station-x"]')
|
|
2089
|
+
check('新增场站:点这一行弹出 demo 自己写的表单(编号 / 名称 / x / y / 电压等级都在,落点已预填)',
|
|
2090
|
+
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)),
|
|
2091
|
+
stationConfirm ? `预填 x=${stationXInput?.value} / y=${document.querySelector('[data-demo="add-station-y"]')?.value} / 编号=${stationIdInput?.value}` : '没弹出表单')
|
|
2092
|
+
// 校验:名称清空不许提交(弹框不关、数据不动)
|
|
2093
|
+
if (stationNameInput) {
|
|
2094
|
+
stationNameInput.value = ''
|
|
2095
|
+
stationNameInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
2096
|
+
}
|
|
2097
|
+
stationConfirm?.click()
|
|
2098
|
+
await wait(140)
|
|
2099
|
+
const emptyNameError = document.querySelector('.demo-station-error')?.textContent ?? ''
|
|
2100
|
+
check('新增场站:名称是空的时候拦在表单里(弹框不关、数据不动)', emptyNameError.includes('名称不能为空') && Boolean(document.querySelector('[data-demo="add-station-confirm"]')), emptyNameError || '没有报错')
|
|
2101
|
+
const newStationId = stationIdInput?.value ?? ''
|
|
2102
|
+
if (stationNameInput) {
|
|
2103
|
+
stationNameInput.value = '自测新场站'
|
|
2104
|
+
stationNameInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
2105
|
+
}
|
|
2106
|
+
document.querySelector('[data-demo="add-station-confirm"]')?.click()
|
|
2107
|
+
await wait(520)
|
|
2108
|
+
const pointsAfterStation = Number((document.querySelector('.demo-overlay')?.textContent ?? '').match(/(\d+)\s*点/)?.[1] ?? 0)
|
|
2109
|
+
const stationEvent = document.querySelector('.gis-demo-event')?.textContent ?? ''
|
|
2110
|
+
check('新增场站:确认走 addPoint —— 表单收起来、要素数 +1、日志点名 addPoint / 落点下标 / 已选中',
|
|
2111
|
+
!document.querySelector('[data-demo="add-station-confirm"]') && pointsAfterStation === pointsBeforeStation + 1 && stationEvent.includes('addPoint') && stationEvent.includes('已选中') && stationEvent.includes(newStationId),
|
|
2112
|
+
`${pointsBeforeStation} → ${pointsAfterStation} · ${stationEvent.slice(0, 150)}`)
|
|
2113
|
+
// 2.11 beginAddChannel 的结束事件(ready):读数摆到面板上,endAddChannel 收掉不新建
|
|
2114
|
+
const draftState = document.querySelector('.demo-view')?.__vueParentComponent?.setupState
|
|
2115
|
+
const draftView = draftState?.viewRef ?? null
|
|
2116
|
+
if (draftView) {
|
|
2117
|
+
// 打开「数据 · 通道 → 通道与拐点」面板,点「新建通道(点两个节点)」
|
|
2118
|
+
;[...document.querySelectorAll('.demo-tabs button')].find(node => node.textContent.includes('数据'))?.click()
|
|
2119
|
+
await wait(80)
|
|
2120
|
+
const openChannelPanel = async () => {
|
|
2121
|
+
;[...document.querySelectorAll('.demo-panel-buttons button')].find(node => node.textContent.trim() === '通道与拐点')?.click()
|
|
2122
|
+
await wait(140)
|
|
2123
|
+
return [...document.querySelectorAll('.demo-popover button')].find(node => node.textContent.trim() === '新建通道(点两个节点)') ?? null
|
|
2124
|
+
}
|
|
2125
|
+
let beginDraftButton = await openChannelPanel()
|
|
2126
|
+
if (!beginDraftButton) {
|
|
2127
|
+
beginDraftButton = await openChannelPanel()
|
|
2128
|
+
}
|
|
2129
|
+
check('新增通道:面板上点得到「新建通道(点两个节点)」', Boolean(beginDraftButton), beginDraftButton ? beginDraftButton.textContent.trim() : '没找到按钮')
|
|
2130
|
+
// 结束事件(ready)与 cancel 的差别就在「不补 cancel」:用事件订阅记一份相位表(不动 demo 自己的 onChannelDraft)
|
|
2131
|
+
const draftPhases = []
|
|
2132
|
+
const offDraftEvent = draftView.on('channelDraft', payload => draftPhases.push(payload.phase))
|
|
2133
|
+
beginDraftButton?.click()
|
|
2134
|
+
await wait(150)
|
|
2135
|
+
// 找一对投影在画布里的节点:先适应视图,再挑屏幕距离最远的一对
|
|
2136
|
+
draftView.fitView()
|
|
2137
|
+
await wait(220)
|
|
2138
|
+
const draftCanvas = document.querySelector('.demo-view canvas.gis-view__canvas')
|
|
2139
|
+
const draftBox = draftCanvas.getBoundingClientRect()
|
|
2140
|
+
const draftOnScreen = draftView.getData().points.filter(point => {
|
|
2141
|
+
const screen = draftView.project(point.x, point.y)
|
|
2142
|
+
return screen[0] > 6 && screen[1] > 6 && screen[0] < draftBox.width - 6 && screen[1] < draftBox.height - 6
|
|
2143
|
+
})
|
|
2144
|
+
// 数据多的时候不做全量两两组合:按等距抽样取最多 60 个候选
|
|
2145
|
+
const draftStride = Math.max(Math.ceil(draftOnScreen.length / 60), 1)
|
|
2146
|
+
const draftPicks = draftOnScreen.filter((_point, index) => index % draftStride === 0)
|
|
2147
|
+
let pickFrom = null
|
|
2148
|
+
let pickTo = null
|
|
2149
|
+
let pickSpan = -1
|
|
2150
|
+
let fallbackFrom = null
|
|
2151
|
+
let fallbackTo = null
|
|
2152
|
+
let fallbackSpan = -1
|
|
2153
|
+
for (const from of draftPicks) {
|
|
2154
|
+
for (const to of draftPicks) {
|
|
2155
|
+
if (String(from.id) === String(to.id)) {
|
|
2156
|
+
continue
|
|
2157
|
+
}
|
|
2158
|
+
const a = draftView.project(from.x, from.y)
|
|
2159
|
+
const b = draftView.project(to.x, to.y)
|
|
2160
|
+
const span = Math.hypot(a[0] - b[0], a[1] - b[1])
|
|
2161
|
+
if (span > fallbackSpan) {
|
|
2162
|
+
fallbackSpan = span
|
|
2163
|
+
fallbackFrom = from
|
|
2164
|
+
fallbackTo = to
|
|
2165
|
+
}
|
|
2166
|
+
// 优先挑一对「还没有通道」的(getChannelBetween 很贵:只在可能刷新记录时才查)
|
|
2167
|
+
if (span > pickSpan && !draftView.getChannelBetween(from.id, to.id)?.hasChannel) {
|
|
2168
|
+
pickSpan = span
|
|
2169
|
+
pickFrom = from
|
|
2170
|
+
pickTo = to
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
if (!pickFrom && fallbackFrom) {
|
|
2175
|
+
pickFrom = fallbackFrom
|
|
2176
|
+
pickTo = fallbackTo
|
|
2177
|
+
}
|
|
2178
|
+
const draftClick = screen => {
|
|
2179
|
+
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] }
|
|
2180
|
+
draftCanvas.dispatchEvent(new PointerEvent('pointermove', { ...base, buttons: 0 }))
|
|
2181
|
+
draftCanvas.dispatchEvent(new PointerEvent('pointerdown', { ...base, buttons: 1 }))
|
|
2182
|
+
draftCanvas.dispatchEvent(new PointerEvent('pointerup', { ...base, buttons: 0 }))
|
|
2183
|
+
}
|
|
2184
|
+
if (pickFrom && pickTo) {
|
|
2185
|
+
draftClick(draftView.project(pickFrom.x, pickFrom.y))
|
|
2186
|
+
await wait(90)
|
|
2187
|
+
draftClick(draftView.project(pickTo.x, pickTo.y))
|
|
2188
|
+
await wait(180)
|
|
2189
|
+
}
|
|
2190
|
+
const readyPayload = draftState?.channelReadyPayload ?? null
|
|
2191
|
+
check('新增通道:结束事件(ready)载荷进了 demo(两个节点 / 没有通道的读数 / 草稿还在)',
|
|
2192
|
+
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',
|
|
2193
|
+
readyPayload ? `from=${readyPayload.from?.id} to=${readyPayload.to?.id} hasChannel=${readyPayload.hasChannel}` : '没有收到 ready 载荷')
|
|
2194
|
+
const readyText = document.querySelector('[data-demo="channel-ready"]')?.textContent ?? ''
|
|
2195
|
+
check('新增通道:面板把 ready 读数摆出来(hasChannel / 分组明细 / 当前分组 / 通道 / existingChannelId)',
|
|
2196
|
+
readyText.includes('hasChannel') && readyText.includes('分组明细') && readyText.includes('当前分组') && readyText.includes('通道') && readyText.includes('existingChannelId'), readyText.replace(/\s+/g, ' ').slice(0, 140))
|
|
2197
|
+
const draftBeforeEnd = draftView.getData()
|
|
2198
|
+
const endDraftButton = document.querySelector('[data-demo="channel-ready-end"]')
|
|
2199
|
+
check('新增通道:结束事件读数旁边给出 endAddChannel 按钮(ready 时可点)', Boolean(endDraftButton) && endDraftButton.disabled === false, endDraftButton ? endDraftButton.textContent.trim() + (endDraftButton.disabled ? '(灰)' : '') : '没有按钮')
|
|
2200
|
+
endDraftButton?.click()
|
|
2201
|
+
await wait(200)
|
|
2202
|
+
check('新增通道:endAddChannel 静默收掉假通道 —— 草稿没了、数据一动不动、不补 cancel',
|
|
2203
|
+
draftPhases.includes('ready') && !draftPhases.includes('cancel') && draftView.getAddChannelDraft() === null && draftView.getData().points.length === draftBeforeEnd.points.length && draftView.getData().lines.length === draftBeforeEnd.lines.length,
|
|
2204
|
+
`相位 ${draftPhases.join(' / ')} · 草稿 ${draftView.getAddChannelDraft() === null ? '已收' : '还在'} / 节点 ${draftBeforeEnd.points.length} → ${draftView.getData().points.length} / 线路 ${draftBeforeEnd.lines.length} → ${draftView.getData().lines.length}`)
|
|
2205
|
+
offDraftEvent()
|
|
2206
|
+
}
|
|
2207
|
+
else {
|
|
2208
|
+
check('新增通道:综合示例的实例拿得到(setupState.viewRef)', false, '没有 setupState.viewRef')
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
else {
|
|
2212
|
+
check('实例方法速查:综合示例的页签在', false, '没找到「综合」页签')
|
|
2213
|
+
}
|
|
2214
|
+
// 2.10 假后端契约:头 / body 必须一致,同 opId 换 body 必须报幂等冲突
|
|
2215
|
+
{
|
|
2216
|
+
const { createFakeBackend } = await import('../demos/shared/fakeBackend.js')
|
|
2217
|
+
const backend = createFakeBackend({ initialVersion: 12, latency: 0, prefix: 'contract' })
|
|
2218
|
+
const request = backend.makeRequest({
|
|
2219
|
+
url: '/gis/copies/batch',
|
|
2220
|
+
body: {
|
|
2221
|
+
op: 'copy.batch',
|
|
2222
|
+
points: [{ id: 'contract-p1', name: '契约点 1' }]
|
|
2223
|
+
}
|
|
2224
|
+
})
|
|
2225
|
+
const success = await backend.submit(request, { latencyMs: 0 })
|
|
2226
|
+
check('假后端契约:合法请求头 / body 一致时 200 并推进版本',
|
|
2227
|
+
success.status === 200 && success.data?.idempotent === false && success.data?.version === 13 && success.data?.result?.counts?.points === 1,
|
|
2228
|
+
`HTTP ${success.status} · v${backend.snapshot().version} · ${success.code}`)
|
|
2229
|
+
const versionAfterSuccess = backend.snapshot().version
|
|
2230
|
+
const replay = await backend.submit(request, { latencyMs: 0 })
|
|
2231
|
+
const replaySnapshot = backend.snapshot()
|
|
2232
|
+
check('假后端契约:同 opId 同 body 原样重放 → 200 / idempotent / 版本与资源不重复',
|
|
2233
|
+
replay.status === 200 && replay.data?.idempotent === true && replay.data?.version === versionAfterSuccess && replaySnapshot.version === versionAfterSuccess && replay.data?.result?.counts?.points === 1,
|
|
2234
|
+
`HTTP ${replay.status} · idempotent=${replay.data?.idempotent} · v${replaySnapshot.version}`)
|
|
2235
|
+
const differentBody = {
|
|
2236
|
+
...request,
|
|
2237
|
+
body: {
|
|
2238
|
+
...request.body,
|
|
2239
|
+
points: [{ id: 'contract-p2', name: '契约点 2' }]
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
const conflict = await backend.submit(differentBody, { latencyMs: 0 })
|
|
2243
|
+
check('假后端契约:同 opId 换 body → 409 IDEMPOTENCY_CONFLICT,版本不前进',
|
|
2244
|
+
conflict.status === 409 && conflict.code === 'IDEMPOTENCY_CONFLICT' && conflict.body?.data?.conflict?.type === 'idempotency-key' && backend.snapshot().version === versionAfterSuccess,
|
|
2245
|
+
`HTTP ${conflict.status} · ${conflict.code} · v${backend.snapshot().version}`)
|
|
2246
|
+
const mismatchHeader = backend.makeRequest({ url: '/gis/copies/batch', body: { op: 'copy.batch', points: [] } })
|
|
2247
|
+
mismatchHeader.headers['Idempotency-Key'] = 'contract-wrong'
|
|
2248
|
+
const headerMismatch = await backend.submit(mismatchHeader, { latencyMs: 0 })
|
|
2249
|
+
check('假后端契约:Idempotency-Key 与 body.opId 不一致 → 400 BAD_REQUEST',
|
|
2250
|
+
headerMismatch.status === 400 && headerMismatch.code === 'BAD_REQUEST' && /Idempotency-Key/.test(headerMismatch.reason ?? ''),
|
|
2251
|
+
`HTTP ${headerMismatch.status} · ${headerMismatch.code}`)
|
|
2252
|
+
const mismatchIfMatch = backend.makeRequest({ url: '/gis/copies/batch', body: { op: 'copy.batch', points: [] } })
|
|
2253
|
+
mismatchIfMatch.headers['If-Match'] = '"v999"'
|
|
2254
|
+
const ifMatchMismatch = await backend.submit(mismatchIfMatch, { latencyMs: 0 })
|
|
2255
|
+
check('假后端契约:If-Match 与 body.baseVersion 不一致 → 400 BAD_REQUEST',
|
|
2256
|
+
ifMatchMismatch.status === 400 && ifMatchMismatch.code === 'BAD_REQUEST' && /If-Match/.test(ifMatchMismatch.reason ?? ''),
|
|
2257
|
+
`HTTP ${ifMatchMismatch.status} · ${ifMatchMismatch.code}`)
|
|
2258
|
+
const versionBeforeInvalid = backend.snapshot().version
|
|
2259
|
+
const invalidVersion = backend.makeRequest({ url: '/gis/copies/batch', body: { op: 'copy.batch', points: [] } })
|
|
2260
|
+
invalidVersion.body.baseVersion = 'not-a-number'
|
|
2261
|
+
const badVersion = await backend.submit(invalidVersion, { latencyMs: 0 })
|
|
2262
|
+
check('假后端契约:baseVersion 不是有限数字 → 400,版本不动',
|
|
2263
|
+
badVersion.status === 400 && badVersion.code === 'BAD_REQUEST' && backend.snapshot().version === versionBeforeInvalid,
|
|
2264
|
+
`HTTP ${badVersion.status} · ${badVersion.code} · v${backend.snapshot().version}`)
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
catch (error) {
|
|
2268
|
+
check('自测过程没有抛错', false, error instanceof Error ? `${error.message}\n${error.stack ?? ''}` : String(error))
|
|
2269
|
+
}
|
|
2270
|
+
const failures = results.filter(line => line.startsWith('FAIL'))
|
|
2271
|
+
document.getElementById('out').textContent = `${document.title} ${failures.length ? 'FAIL' : 'PASS'}\n${results.join('\n')}`
|
|
2272
|
+
document.title = `${document.title} ${failures.length ? 'FAIL' : 'PASS'}`
|
|
2273
|
+
</script>
|
|
2274
|
+
</body>
|
|
2275
|
+
</html>
|