@mzzsfy/dsh-usage-dash 0.3.0 → 0.5.0
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 +55 -14
- package/package.json +2 -2
- package/src/archive-reader.js +138 -0
- package/src/client.js +539 -128
- package/src/collector.js +175 -29
- package/src/direct-log-reader.js +126 -0
- package/src/pricing.js +5 -6
- package/src/query.js +38 -14
- package/src/routes.js +14 -1
- package/src/store.js +86 -4
- package/test/archive-reader.test.mjs +365 -0
- package/test/client.test.mjs +210 -12
- package/test/collector.test.mjs +458 -19
- package/test/direct-log-reader.test.mjs +104 -0
- package/test/pricing-parity.test.mjs +38 -4
- package/test/pricing.test.mjs +50 -18
- package/test/query.test.mjs +50 -1
- package/test/routes.test.mjs +9 -2
- package/test/stats-line.test.mjs +6 -6
- package/test/store.test.mjs +116 -1
- package/test/stream-parity.test.mjs +75 -0
- package/test/turn-tail.test.mjs +161 -41
package/test/client.test.mjs
CHANGED
|
@@ -61,6 +61,8 @@ const {
|
|
|
61
61
|
hourTickLabel,
|
|
62
62
|
indexOfDay,
|
|
63
63
|
isEmptyRange,
|
|
64
|
+
groupRulesOf,
|
|
65
|
+
insertRuleAt,
|
|
64
66
|
logTimeOf,
|
|
65
67
|
matchPrice,
|
|
66
68
|
maxSlotsFor,
|
|
@@ -68,11 +70,15 @@ const {
|
|
|
68
70
|
modelSegmentLabel,
|
|
69
71
|
modelSpeedText,
|
|
70
72
|
modelNameOf,
|
|
73
|
+
moveRuleTo,
|
|
71
74
|
niceTicks,
|
|
72
75
|
otherDetailItems,
|
|
73
76
|
parseEnvelope,
|
|
77
|
+
partitionGroupOf,
|
|
74
78
|
providerOf,
|
|
75
79
|
rateAxisTicks,
|
|
80
|
+
removeRulesAt,
|
|
81
|
+
renameRulesAt,
|
|
76
82
|
resolveDayRange,
|
|
77
83
|
resolveHourRange,
|
|
78
84
|
resolveMinuteRange,
|
|
@@ -84,6 +90,10 @@ const {
|
|
|
84
90
|
trendLayout,
|
|
85
91
|
trendRatePoints,
|
|
86
92
|
trendSpeedPoints,
|
|
93
|
+
trendTtftPoints,
|
|
94
|
+
ttftScaleMax,
|
|
95
|
+
ttftTipText,
|
|
96
|
+
modelTtftText,
|
|
87
97
|
speedTipText,
|
|
88
98
|
speedScaleMax,
|
|
89
99
|
legendToggle,
|
|
@@ -470,8 +480,24 @@ const TIP_ANCHOR = { left: 100, top: 100, right: 140, bottom: 114 }
|
|
|
470
480
|
const TIP_SIZE = { width: 80, height: 40 }
|
|
471
481
|
const TIP_BOUNDS = { left: 0, top: 0, right: 800, bottom: 600 }
|
|
472
482
|
|
|
473
|
-
test('tipPlace
|
|
474
|
-
assert.deepEqual(tipPlace(TIP_ANCHOR, TIP_SIZE, TIP_BOUNDS), { left: 80, top:
|
|
483
|
+
test('tipPlace 小锚点默认翻上方水平居中', () => {
|
|
484
|
+
assert.deepEqual(tipPlace(TIP_ANCHOR, TIP_SIZE, TIP_BOUNDS), { left: 80, top: 52 })
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
test('tipPlace 锚定区装得下提示框贴内顶', () => {
|
|
488
|
+
const anchor = { left: 100, top: 100, right: 240, bottom: 460 }
|
|
489
|
+
assert.deepEqual(tipPlace(anchor, TIP_SIZE, TIP_BOUNDS), { left: 130, top: 108 })
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
test('tipPlace 上方不足翻下方', () => {
|
|
493
|
+
const anchor = { left: 100, top: 20, right: 140, bottom: 60 }
|
|
494
|
+
assert.equal(tipPlace(anchor, TIP_SIZE, TIP_BOUNDS).top, 68)
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
test('tipPlace 候选边界等号归属', () => {
|
|
498
|
+
assert.deepEqual(tipPlace({ left: 100, top: 100, right: 240, bottom: 148 }, TIP_SIZE, TIP_BOUNDS), { left: 130, top: 108 })
|
|
499
|
+
assert.equal(tipPlace({ left: 100, top: 56, right: 140, bottom: 96 }, TIP_SIZE, TIP_BOUNDS).top, 8)
|
|
500
|
+
assert.equal(tipPlace({ left: 100, top: 8, right: 140, bottom: 48 }, TIP_SIZE, { left: 0, top: 0, right: 800, bottom: 104 }).top, 56)
|
|
475
501
|
})
|
|
476
502
|
|
|
477
503
|
test('tipPlace 下方放不下翻转上方', () => {
|
|
@@ -575,6 +601,44 @@ test('speedScaleMax 全零或空槽钳底防除零,混合取最大速度', () =>
|
|
|
575
601
|
assert.equal(speedScaleMax([{ day: 'd0', speed: 30 }, { day: 'd1', speed: 12.5 }]), 30)
|
|
576
602
|
})
|
|
577
603
|
|
|
604
|
+
test('trendTtftPoints 点映射列中心与刻度上限比例高度且无 ttft 槽跳过', () => {
|
|
605
|
+
const slots = [
|
|
606
|
+
{ day: 'd0', ttft: 2000 },
|
|
607
|
+
{ day: 'd1', total: 10 },
|
|
608
|
+
{ day: 'd2', ttft: 1000 },
|
|
609
|
+
]
|
|
610
|
+
const bars = [{ x: 10 }, { x: 20 }, { x: 30 }]
|
|
611
|
+
const plotHeight = CHART_HEIGHT - CHART_PAD.top - CHART_PAD.bottom
|
|
612
|
+
const points = trendTtftPoints(slots, bars, plotHeight, 2200)
|
|
613
|
+
assert.equal(points.length, 2)
|
|
614
|
+
assert.equal(points[0].day, 'd0')
|
|
615
|
+
assert.equal(points[0].x, 10)
|
|
616
|
+
approx(points[0].y, CHART_PAD.top + plotHeight - (2000 / 2200) * plotHeight)
|
|
617
|
+
assert.equal(points[1].day, 'd2')
|
|
618
|
+
assert.equal(points[1].x, 30)
|
|
619
|
+
approx(points[1].y, CHART_PAD.top + plotHeight - (1000 / 2200) * plotHeight)
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
test('ttftScaleMax 全零或空槽钳底防除零,混合取最大延迟', () => {
|
|
623
|
+
assert.equal(ttftScaleMax([]), 1)
|
|
624
|
+
assert.equal(ttftScaleMax([{ day: 'd0' }, { day: 'd1', ttft: 0 }]), 1)
|
|
625
|
+
assert.equal(ttftScaleMax([{ day: 'd0', ttft: 3000 }, { day: 'd1', ttft: 1250 }]), 3000)
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
test('ttftTipText 无 ttft 为占位符有 ttft 为官方时长口径', () => {
|
|
629
|
+
const zhT = createTranslator(MESSAGES_ZH)
|
|
630
|
+
const enT = createTranslator(MESSAGES_EN)
|
|
631
|
+
assert.equal(ttftTipText(undefined, zhT), '—')
|
|
632
|
+
assert.equal(ttftTipText(200, zhT), '0.2秒')
|
|
633
|
+
assert.equal(ttftTipText(200, enT), '0.2s')
|
|
634
|
+
assert.equal(ttftTipText(9500, zhT), '9.5秒')
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
test('ttftLegend 中英文案注册', () => {
|
|
638
|
+
assert.equal(MESSAGES_ZH.ttftLegend, '首 token 延迟')
|
|
639
|
+
assert.equal(MESSAGES_EN.ttftLegend, 'First-token latency')
|
|
640
|
+
})
|
|
641
|
+
|
|
578
642
|
test('smoothPath 空点集为空串单点为移动命令', () => {
|
|
579
643
|
assert.equal(smoothPath([]), '')
|
|
580
644
|
assert.equal(smoothPath([{ x: 1, y: 2 }]), 'M 1 2')
|
|
@@ -590,6 +654,14 @@ test('smoothPath 三点后段取真实邻点', () => {
|
|
|
590
654
|
assert.equal(d, 'M 0 0 C 10 5, 40 30, 60 30 C 80 30, 110 5, 120 0')
|
|
591
655
|
})
|
|
592
656
|
|
|
657
|
+
test('smoothPath 相邻点间距超 maxGap 折线断开成新段', () => {
|
|
658
|
+
const points = [{ x: 0, y: 0 }, { x: 60, y: 0 }, { x: 300, y: 0 }, { x: 360, y: 0 }]
|
|
659
|
+
const d = smoothPath(points, 90)
|
|
660
|
+
assert.equal(d, 'M 0 0 C 10 0, 10 0, 60 0 M 300 0 C 350 0, 350 0, 360 0')
|
|
661
|
+
// 无 maxGap 时不分段,行为与旧签名一致
|
|
662
|
+
assert.equal(smoothPath(points).includes('M 300'), false)
|
|
663
|
+
})
|
|
664
|
+
|
|
593
665
|
// —— S8 模型 donut 与列表 ——
|
|
594
666
|
|
|
595
667
|
test('donut 常量锁定视口与环几何', () => {
|
|
@@ -665,6 +737,13 @@ test('模型速度文本:官方吞吐口径格式化,无速度为空串', () =>
|
|
|
665
737
|
assert.equal(modelSpeedText(undefined), '')
|
|
666
738
|
})
|
|
667
739
|
|
|
740
|
+
test('模型首字文本:语言中立短时长,无 ttft 为空串', () => {
|
|
741
|
+
assert.equal(modelTtftText(200), 'TTFT 0.2s')
|
|
742
|
+
assert.equal(modelTtftText(162000), 'TTFT 2m42s')
|
|
743
|
+
assert.equal(modelTtftText(0), 'TTFT 0s')
|
|
744
|
+
assert.equal(modelTtftText(undefined), '')
|
|
745
|
+
})
|
|
746
|
+
|
|
668
747
|
// —— S14 费用格式化与展示辅助(镜像函数核心语义见 pricing-parity.test.mjs) ——
|
|
669
748
|
|
|
670
749
|
test('formatCost 千分位与两位小数', () => {
|
|
@@ -749,51 +828,70 @@ test('costTitleText 未计价计数后缀', () => {
|
|
|
749
828
|
})
|
|
750
829
|
|
|
751
830
|
test('defaultCondition 各类型默认值即填即用', () => {
|
|
752
|
-
// Given 四种条件类型 When 取默认 Then
|
|
753
|
-
assert.deepEqual(defaultCondition('dailyWindow'), { kind: 'dailyWindow', from: '00:00', to: '
|
|
831
|
+
// Given 四种条件类型 When 取默认 Then 时段全天(00:00~23:59)/周几空/号段全月(1~31)/日期段当天单日
|
|
832
|
+
assert.deepEqual(defaultCondition('dailyWindow'), { kind: 'dailyWindow', from: '00:00', to: '23:59' })
|
|
754
833
|
assert.deepEqual(defaultCondition('weekdays'), { kind: 'weekdays', days: [] })
|
|
755
834
|
assert.deepEqual(defaultCondition('monthDays'), { kind: 'monthDays', from: 1, to: 31 })
|
|
756
835
|
assert.deepEqual(defaultCondition('dateRange', new Date(2026, 2, 15)), { kind: 'dateRange', from: '2026-03-15', to: '2026-03-15' })
|
|
757
836
|
})
|
|
758
837
|
|
|
759
838
|
test('validatePricingRules 拒绝非法时刻与时刻字段缺失', () => {
|
|
760
|
-
// Given dailyWindow 时刻超界或缺失 When 校验 Then 标 condTime/required
|
|
839
|
+
// Given dailyWindow 时刻超界或缺失 When 校验 Then 标 condTime/required;双闭域 00:00~23:59,24:00 超界
|
|
761
840
|
const rules = [
|
|
762
841
|
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'dailyWindow', from: '99:99', to: '08:00' }] },
|
|
763
842
|
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'dailyWindow', from: '', to: '08:00' }] },
|
|
764
|
-
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'dailyWindow', from: '
|
|
843
|
+
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'dailyWindow', from: '24:00', to: '24:00' }] },
|
|
844
|
+
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'dailyWindow', from: '08:00', to: '24:30' }] },
|
|
845
|
+
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'dailyWindow', from: '00:00', to: '23:59' }] },
|
|
765
846
|
]
|
|
766
847
|
const errors = validatePricingRules(rules)
|
|
767
848
|
assert.equal(errors.get('0.conditions.0.from'), 'condTime')
|
|
768
849
|
assert.equal(errors.get('1.conditions.0.from'), 'required')
|
|
769
|
-
assert.equal(errors.get('2.conditions.0.
|
|
850
|
+
assert.equal(errors.get('2.conditions.0.from'), 'condTime')
|
|
851
|
+
assert.equal(errors.get('3.conditions.0.to'), 'condTime')
|
|
852
|
+
assert.equal(errors.has('4.conditions.0'), false)
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
test('validatePricingRules 接受 from===to 单点与单日', () => {
|
|
856
|
+
// Given 双闭下时段/号段/日期段 from===to When 校验 Then 全部合法
|
|
857
|
+
const rules = [
|
|
858
|
+
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'dailyWindow', from: '10:00', to: '10:00' }] },
|
|
859
|
+
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'monthDays', from: 5, to: 5 }] },
|
|
860
|
+
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'dateRange', from: '2026-03-05', to: '2026-03-05' }] },
|
|
861
|
+
]
|
|
862
|
+
const errors = validatePricingRules(rules)
|
|
863
|
+
assert.equal(errors.size, 0)
|
|
770
864
|
})
|
|
771
865
|
|
|
772
866
|
test('validatePricingRules 校验周几与月号段边界', () => {
|
|
773
|
-
// Given weekdays 含 7、monthDays 含 0
|
|
867
|
+
// Given weekdays 含 7、monthDays 含 0/32/33 When 校验 Then 标 condWeekday/condMonthDay;31 为合法上界
|
|
774
868
|
const rules = [
|
|
775
869
|
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'weekdays', days: [0, 7] }] },
|
|
776
870
|
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'monthDays', from: 0, to: 31 }] },
|
|
777
|
-
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'monthDays', from:
|
|
871
|
+
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'monthDays', from: 32, to: 32 }] },
|
|
872
|
+
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'monthDays', from: 1, to: 33 }] },
|
|
778
873
|
]
|
|
779
874
|
const errors = validatePricingRules(rules)
|
|
780
875
|
assert.equal(errors.get('0.conditions.0.days'), 'condWeekday')
|
|
781
876
|
assert.equal(errors.get('1.conditions.0.from'), 'condMonthDay')
|
|
782
|
-
assert.equal(errors.get('2.conditions.0.
|
|
877
|
+
assert.equal(errors.get('2.conditions.0.from'), 'condMonthDay')
|
|
878
|
+
assert.equal(errors.get('3.conditions.0.to'), 'condMonthDay')
|
|
783
879
|
})
|
|
784
880
|
|
|
785
881
|
test('validatePricingRules 校验日期段格式与倒序', () => {
|
|
786
|
-
// Given dateRange 非规范日期或倒序
|
|
882
|
+
// Given dateRange 非规范日期或倒序(双闭下同日合法)When 校验 Then 标 condDate/condRange
|
|
787
883
|
const rules = [
|
|
788
884
|
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'dateRange', from: '2026-3-5', to: '2026-03-08' }] },
|
|
789
885
|
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'dateRange', from: '2026-03-08', to: '2026-03-05' }] },
|
|
886
|
+
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'dateRange', from: '2026-03-05', to: '2026-03-05' }] },
|
|
790
887
|
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'dateRange', from: '2026-03-05', to: '2026-03-08' }] },
|
|
791
888
|
]
|
|
792
889
|
const errors = validatePricingRules(rules)
|
|
793
890
|
assert.equal(errors.get('0.conditions.0.from'), 'condDate')
|
|
794
891
|
assert.equal(errors.get('1.conditions.0'), 'condRange')
|
|
795
892
|
assert.equal(errors.has('2.conditions.0'), false)
|
|
796
|
-
assert.equal(errors.has('
|
|
893
|
+
assert.equal(errors.has('3.conditions.0'), false)
|
|
894
|
+
assert.equal(errors.has('3.conditions.0.from'), false)
|
|
797
895
|
})
|
|
798
896
|
|
|
799
897
|
test('coerceConditions 规整数字字段且不改其余字段', () => {
|
|
@@ -822,3 +920,103 @@ test('coercePricingRules 连带规整条件', () => {
|
|
|
822
920
|
assert.deepEqual(coerced[0].conditions, [{ kind: 'weekdays', days: [1] }, { kind: 'monthDays', from: 1, to: 31 }])
|
|
823
921
|
assert.deepEqual(coerced[0].price, { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 })
|
|
824
922
|
})
|
|
923
|
+
|
|
924
|
+
// —— 定价编辑器分组视图:模型组 = 默认价(组末无条件规则投影,禁条件) + 附加计费规则(带条件) ——
|
|
925
|
+
|
|
926
|
+
test('groupRulesOf 按模型聚合,组序为首次出现序,槽位保序带平面索引', () => {
|
|
927
|
+
// Given 平面规则 [a1, b1, a2] When 分组 Then a 组槽位 [0,2]、b 组槽位 [1],组顺序按首次出现
|
|
928
|
+
const rules = [{ model: 'a/1' }, { model: 'b/1' }, { model: 'a/1' }]
|
|
929
|
+
const groups = groupRulesOf(rules)
|
|
930
|
+
assert.deepEqual(groups.map((group) => group.model), ['a/1', 'b/1'])
|
|
931
|
+
assert.deepEqual(groups[0].slots.map((slot) => slot.index), [0, 2])
|
|
932
|
+
assert.deepEqual(groups[1].slots.map((slot) => slot.index), [1])
|
|
933
|
+
groups[0].slots.forEach((slot, i) => assert.equal(slot.rule, rules[[0, 2][i]]))
|
|
934
|
+
})
|
|
935
|
+
|
|
936
|
+
test('groupRulesOf 空表返回空组列表', () => {
|
|
937
|
+
assert.deepEqual(groupRulesOf([]), [])
|
|
938
|
+
})
|
|
939
|
+
|
|
940
|
+
test('partitionGroupOf 组末无条件规则为默认价,其余为附加规则且保序', () => {
|
|
941
|
+
// Given 组槽位 [条件0, 条件1, 无条件2] When 划分 Then 默认价=索引2,附加=[0,1]
|
|
942
|
+
const cond = { kind: 'dailyWindow', from: '00:00', to: '08:00' }
|
|
943
|
+
const group = { model: 'a/1', slots: [
|
|
944
|
+
{ rule: { model: 'a/1', conditions: [cond] }, index: 0 },
|
|
945
|
+
{ rule: { model: 'a/1', conditions: [{ kind: 'weekdays', days: [1] }] }, index: 1 },
|
|
946
|
+
{ rule: { model: 'a/1', conditions: [] }, index: 2 },
|
|
947
|
+
] }
|
|
948
|
+
const { defaultSlot, extras } = partitionGroupOf(group)
|
|
949
|
+
assert.equal(defaultSlot.index, 2)
|
|
950
|
+
assert.deepEqual(extras.map((slot) => slot.index), [0, 1])
|
|
951
|
+
})
|
|
952
|
+
|
|
953
|
+
test('partitionGroupOf 全条件规则组无默认价', () => {
|
|
954
|
+
// Given 组内全部带条件 When 划分 Then defaultSlot 为 null,附加=全部槽位
|
|
955
|
+
const group = { model: 'a/1', slots: [
|
|
956
|
+
{ rule: { model: 'a/1', conditions: [{ kind: 'weekdays', days: [1] }] }, index: 0 },
|
|
957
|
+
{ rule: { model: 'a/1', conditions: [{ kind: 'weekdays', days: [2] }] }, index: 1 },
|
|
958
|
+
] }
|
|
959
|
+
const { defaultSlot, extras } = partitionGroupOf(group)
|
|
960
|
+
assert.equal(defaultSlot, null)
|
|
961
|
+
assert.deepEqual(extras.map((slot) => slot.index), [0, 1])
|
|
962
|
+
})
|
|
963
|
+
|
|
964
|
+
test('partitionGroupOf 多条无条件规则时末条为默认价,前条按附加规则显示', () => {
|
|
965
|
+
// Given 组槽位 [无条件0, 无条件1] When 划分 Then 默认价=索引1,附加=[0](存量数据自然收敛)
|
|
966
|
+
const group = { model: 'a/1', slots: [
|
|
967
|
+
{ rule: { model: 'a/1', conditions: [] }, index: 0 },
|
|
968
|
+
{ rule: { model: 'a/1', conditions: [] }, index: 1 },
|
|
969
|
+
] }
|
|
970
|
+
const { defaultSlot, extras } = partitionGroupOf(group)
|
|
971
|
+
assert.equal(defaultSlot.index, 1)
|
|
972
|
+
assert.deepEqual(extras.map((slot) => slot.index), [0])
|
|
973
|
+
})
|
|
974
|
+
|
|
975
|
+
test('moveRuleTo 跨位移动返回新数组且不动原数组', () => {
|
|
976
|
+
// Given [A,B,C] When 索引 2 移到索引 0 Then [C,A,B],原数组不变
|
|
977
|
+
const rules = [{ model: 'a/1' }, { model: 'b/1' }, { model: 'a/2' }]
|
|
978
|
+
const moved = moveRuleTo(rules, 2, 0)
|
|
979
|
+
assert.deepEqual(moved.map((rule) => rule.model), ['a/2', 'a/1', 'b/1'])
|
|
980
|
+
assert.deepEqual(rules.map((rule) => rule.model), ['a/1', 'b/1', 'a/2'])
|
|
981
|
+
assert.notEqual(moved, rules)
|
|
982
|
+
})
|
|
983
|
+
|
|
984
|
+
test('moveRuleTo 组内上移等价于移到组内前一槽位平面位', () => {
|
|
985
|
+
// Given [a1,b1,a2] When a2 移到 a1 平面位 0 Then [a2,a1,b1],a 组内序为 a2,a1
|
|
986
|
+
const rules = [{ model: 'a/1' }, { model: 'b/1' }, { model: 'a/1' }]
|
|
987
|
+
assert.deepEqual(moveRuleTo(rules, 2, 0).map((rule) => rule.model), ['a/1', 'a/1', 'b/1'])
|
|
988
|
+
})
|
|
989
|
+
|
|
990
|
+
test('moveRuleTo 同位或越界返回原数组引用', () => {
|
|
991
|
+
// Given 同源同目标/越界 When 移动 Then 恒返回原引用,UI 无操作
|
|
992
|
+
const rules = [{ model: 'a/1' }, { model: 'a/2' }]
|
|
993
|
+
assert.equal(moveRuleTo(rules, 0, 0), rules)
|
|
994
|
+
assert.equal(moveRuleTo(rules, -1, 1), rules)
|
|
995
|
+
assert.equal(moveRuleTo(rules, 0, rules.length), rules)
|
|
996
|
+
assert.equal(moveRuleTo(rules, rules.length, 0), rules)
|
|
997
|
+
})
|
|
998
|
+
|
|
999
|
+
test('renameRulesAt 批量改组内模型键,其余规则不动', () => {
|
|
1000
|
+
// Given 索引 [0,2] 改名 x/y When 应用 Then 仅这两条 model 变 x/y
|
|
1001
|
+
const rules = [{ model: 'a/1' }, { model: 'b/1' }, { model: 'a/1' }]
|
|
1002
|
+
const renamed = renameRulesAt(rules, [0, 2], 'x/y')
|
|
1003
|
+
assert.deepEqual(renamed.map((rule) => rule.model), ['x/y', 'b/1', 'x/y'])
|
|
1004
|
+
assert.notEqual(renamed[0], rules[0])
|
|
1005
|
+
assert.equal(renamed[1], rules[1])
|
|
1006
|
+
assert.notEqual(renamed[2], rules[2])
|
|
1007
|
+
})
|
|
1008
|
+
test('insertRuleAt 插入到目标位之后', () => {
|
|
1009
|
+
// Given 组内末槽位平面索引 2 When 插入新槽位 Then 落在索引 3,后续元素后移
|
|
1010
|
+
const rules = [{ model: 'a/1' }, { model: 'b/1' }, { model: 'a/1' }]
|
|
1011
|
+
const next = insertRuleAt(rules, 2, { model: 'a/1' })
|
|
1012
|
+
assert.equal(next.length, 4)
|
|
1013
|
+
assert.equal(next[3].model, 'a/1')
|
|
1014
|
+
assert.equal(next[1].model, 'b/1')
|
|
1015
|
+
})
|
|
1016
|
+
|
|
1017
|
+
test('removeRulesAt 批量移除组内槽位', () => {
|
|
1018
|
+
// Given 索引 [0,2] When 删除 Then 仅剩 b 组
|
|
1019
|
+
const rules = [{ model: 'a/1' }, { model: 'b/1' }, { model: 'a/1' }]
|
|
1020
|
+
const rest = removeRulesAt(rules, [0, 2])
|
|
1021
|
+
assert.deepEqual(rest.map((rule) => rule.model), ['b/1'])
|
|
1022
|
+
})
|