dsh-workspace-mover 2.2.0 → 2.4.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/CHANGELOG.md +311 -0
- package/README.md +53 -12
- package/README_EN.md +353 -0
- package/client/client.js +447 -43
- package/docs/ARCHITECTURE.md +224 -206
- package/docs/compatibility/dsh-0.2.0-rc.2-2026-10-04.md +39 -0
- package/docs/compatibility/dsh-0.2.0-rc.2-2026-10-13.md +45 -0
- package/docs/compatibility/dsh-0.2.1-rc.2-2026-10-18.md +37 -0
- package/docs/compatibility/issue-5-desktop-acceptance-2026-10-22.md +106 -0
- package/docs/compatibility/pr-review-2026-10-22.md +56 -0
- package/lib/index.js +4507 -2939
- package/package.json +103 -99
- package/test/client-dom.test.mjs +208 -0
- package/test/e2e-sandbox.test.mjs +2724 -2200
- package/test/host-services.test.mjs +303 -0
- package/test/installed-host.test.mjs +119 -0
- package/test/workspace-tools.test.mjs +977 -0
package/package.json
CHANGED
|
@@ -1,99 +1,103 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-workspace-mover",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "在 DSH 侧边栏安全迁移会话:真迁移原始档案,支持失联会话恢复、移动历史、一键撤回和 agent 工具。",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "lib/index.js",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": "./lib/index.js",
|
|
9
|
-
"./client": "./client/client.js",
|
|
10
|
-
"./package.json": "./package.json"
|
|
11
|
-
},
|
|
12
|
-
"files": [
|
|
13
|
-
"lib",
|
|
14
|
-
"client",
|
|
15
|
-
"test",
|
|
16
|
-
"cordis.patch.yml",
|
|
17
|
-
"README.md",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"@deepseek-ai/
|
|
48
|
-
"@deepseek-ai/dsh-
|
|
49
|
-
"@deepseek-ai/dsh-
|
|
50
|
-
"@deepseek-ai/dsh-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"@deepseek-ai/
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
"
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-workspace-mover",
|
|
3
|
+
"version": "2.4.0",
|
|
4
|
+
"description": "在 DSH 侧边栏安全迁移会话:真迁移原始档案,支持失联会话恢复、移动历史、一键撤回和 agent 工具。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./client": "./client/client.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"lib",
|
|
14
|
+
"client",
|
|
15
|
+
"test",
|
|
16
|
+
"cordis.patch.yml",
|
|
17
|
+
"README.md",
|
|
18
|
+
"README_EN.md",
|
|
19
|
+
"CHANGELOG.md",
|
|
20
|
+
"docs/ARCHITECTURE.md",
|
|
21
|
+
"docs/compatibility",
|
|
22
|
+
"docs/media",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
], "scripts": {
|
|
25
|
+
"test": "node --test test/core.test.mjs test/e2e-sandbox.test.mjs test/policy.test.mjs test/client-dom.test.mjs test/traffic-badge.test.mjs test/release-downloads.test.mjs test/workspace-tools.test.mjs test/host-services.test.mjs",
|
|
26
|
+
"test:installed-host": "node --test test/installed-host.test.mjs",
|
|
27
|
+
"check:syntax": "node --check lib/index.js && node --check client/client.js",
|
|
28
|
+
"check:pack": "npm pack --dry-run",
|
|
29
|
+
"check": "npm run check:syntax && npm test && npm run check:pack"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"deepseek-harness",
|
|
33
|
+
"dsh",
|
|
34
|
+
"dsh-plugin",
|
|
35
|
+
"workspace",
|
|
36
|
+
"session",
|
|
37
|
+
"session-migration",
|
|
38
|
+
"drag-and-drop",
|
|
39
|
+
"agent-tools"
|
|
40
|
+
],
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=22",
|
|
43
|
+
"dsh": ">=0.1.5-rc.1"
|
|
44
|
+
},
|
|
45
|
+
"//peerDependencies": "Deliberately `*`. These are OPTIONAL peers: the plugin declares them so storefronts can see the host packages it integrates with, but it fail-softs at runtime (mover.status capabilities + mover_doctor). npm's prerelease rule evaluates each `||` alternative as a set and only admits a prerelease when a comparator in that set shares the version's exact major.minor.patch tuple AND carries a prerelease tag -- so an upper bound like `<0.2.0-0` excludes EVERY 0.2.0 prerelease, which made the host report 2.2.0 as incompatible with 0.2.0-rc.2. Enumerating release lines would keep producing false incompatibilities on each new line; the real floor is engines.dsh, and capability flags report any actual runtime mismatch. Do NOT narrow these to version ranges.",
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@deepseek-ai/cordis": "*",
|
|
48
|
+
"@deepseek-ai/dsh-session": "*",
|
|
49
|
+
"@deepseek-ai/dsh-tools": "*",
|
|
50
|
+
"@deepseek-ai/dsh-workspace": "*",
|
|
51
|
+
"@deepseek-ai/schemastery": "*",
|
|
52
|
+
"@deepseek-ai/dsh-client-connection": "*",
|
|
53
|
+
"@deepseek-ai/dsh-client-locale": "*",
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-slots": "*"
|
|
55
|
+
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"@deepseek-ai/cordis": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"@deepseek-ai/dsh-session": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"@deepseek-ai/dsh-tools": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"@deepseek-ai/dsh-workspace": {
|
|
67
|
+
"optional": true
|
|
68
|
+
},
|
|
69
|
+
"@deepseek-ai/schemastery": {
|
|
70
|
+
"optional": true
|
|
71
|
+
},
|
|
72
|
+
"@deepseek-ai/dsh-client-connection": {
|
|
73
|
+
"optional": true
|
|
74
|
+
},
|
|
75
|
+
"@deepseek-ai/dsh-client-locale": {
|
|
76
|
+
"optional": true
|
|
77
|
+
},
|
|
78
|
+
"@deepseek-ai/dsh-client-ui-slots": {
|
|
79
|
+
"optional": true
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"license": "MIT",
|
|
83
|
+
"repository": {
|
|
84
|
+
"type": "git",
|
|
85
|
+
"url": "git+https://github.com/PianoPrince/dsh-workspace-mover.git"
|
|
86
|
+
},
|
|
87
|
+
"bugs": {
|
|
88
|
+
"url": "https://github.com/PianoPrince/dsh-workspace-mover/issues"
|
|
89
|
+
},
|
|
90
|
+
"homepage": "https://github.com/PianoPrince/dsh-workspace-mover#readme",
|
|
91
|
+
"dsh": {
|
|
92
|
+
"bundle": {
|
|
93
|
+
"patch": "./cordis.patch.yml"
|
|
94
|
+
},
|
|
95
|
+
"client": {
|
|
96
|
+
"inject": [
|
|
97
|
+
"@deepseek-ai/dsh-client-connection",
|
|
98
|
+
"@deepseek-ai/dsh-client-ui-slots"
|
|
99
|
+
],
|
|
100
|
+
"platform": "web"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
package/test/client-dom.test.mjs
CHANGED
|
@@ -100,6 +100,7 @@ function createElement(tag) {
|
|
|
100
100
|
tagName: String(tag).toUpperCase(),
|
|
101
101
|
childNodes: [],
|
|
102
102
|
parentNode: null,
|
|
103
|
+
get parentElement() { return this.parentNode?.nodeType === 1 ? this.parentNode : null; },
|
|
103
104
|
className: '',
|
|
104
105
|
id: '',
|
|
105
106
|
style: {},
|
|
@@ -297,12 +298,26 @@ const reactShim = {
|
|
|
297
298
|
|
|
298
299
|
let mod = null;
|
|
299
300
|
const rpcCalls = [];
|
|
301
|
+
const animationFrames = new Map();
|
|
302
|
+
let animationFrameId = 0;
|
|
303
|
+
function frame() {
|
|
304
|
+
const pending = [...animationFrames.values()];
|
|
305
|
+
animationFrames.clear();
|
|
306
|
+
for (const callback of pending) callback();
|
|
307
|
+
}
|
|
300
308
|
let rpcResponder = () => ({ ok: true, value: {} });
|
|
301
309
|
|
|
302
310
|
function setResponder(fn) { rpcResponder = fn; }
|
|
303
311
|
|
|
304
312
|
before(async () => {
|
|
305
313
|
globalThis.document = doc;
|
|
314
|
+
globalThis.getComputedStyle = (el) => ({ overflowY: el.style.overflowY ?? 'visible' });
|
|
315
|
+
globalThis.requestAnimationFrame = (callback) => {
|
|
316
|
+
const id = ++animationFrameId;
|
|
317
|
+
animationFrames.set(id, callback);
|
|
318
|
+
return id;
|
|
319
|
+
};
|
|
320
|
+
globalThis.cancelAnimationFrame = (id) => animationFrames.delete(id);
|
|
306
321
|
globalThis.MutationObserver = MutationObserverStub;
|
|
307
322
|
globalThis.Node = { ELEMENT_NODE: 1 };
|
|
308
323
|
globalThis.Element = function Element() {};
|
|
@@ -371,6 +386,7 @@ function unmount() {
|
|
|
371
386
|
|
|
372
387
|
function resetDom() {
|
|
373
388
|
unmount();
|
|
389
|
+
animationFrames.clear();
|
|
374
390
|
doc._body.childNodes = [];
|
|
375
391
|
doc._listeners = [];
|
|
376
392
|
rpcCalls.length = 0;
|
|
@@ -498,6 +514,44 @@ test('未卸载就重复装载时不得叠加第二份监听器(防重复实
|
|
|
498
514
|
`重复 apply 不得叠加:${afterFirst} -> ${doc._listeners.length}`);
|
|
499
515
|
});
|
|
500
516
|
|
|
517
|
+
test('drag autoscroll stops outside sidebar, at bounds, on dragleave/drop/dragend and unload', () => {
|
|
518
|
+
resetDom();
|
|
519
|
+
mount();
|
|
520
|
+
const row = sessionRow('Scroll test', 'session-aaa');
|
|
521
|
+
const scroller = createElement('div');
|
|
522
|
+
scroller.style.overflowY = 'auto';
|
|
523
|
+
Object.assign(scroller, { scrollHeight: 1000, clientHeight: 300, scrollTop: 100,
|
|
524
|
+
getBoundingClientRect: () => ({ left: 0, right: 200, top: 0, bottom: 300 }) });
|
|
525
|
+
scroller.appendChild(row);
|
|
526
|
+
attach(scroller);
|
|
527
|
+
const start = (x = 100, y = 290) => {
|
|
528
|
+
dispatch({ type: 'dragstart', target: row });
|
|
529
|
+
dispatch({ type: 'dragover', target: row, clientX: x, clientY: y });
|
|
530
|
+
};
|
|
531
|
+
start(); frame();
|
|
532
|
+
assert.equal(scroller.scrollTop, 112);
|
|
533
|
+
dispatch({ type: 'dragover', target: row, clientX: 400, clientY: 290 });
|
|
534
|
+
frame();
|
|
535
|
+
assert.equal(scroller.scrollTop, 112);
|
|
536
|
+
assert.equal(animationFrames.size, 0);
|
|
537
|
+
start(100, 150); frame();
|
|
538
|
+
assert.equal(animationFrames.size, 0, 'center does not scroll');
|
|
539
|
+
scroller.scrollTop = 700;
|
|
540
|
+
start(); frame();
|
|
541
|
+
assert.equal(animationFrames.size, 0, 'bottom boundary stops frames');
|
|
542
|
+
scroller.scrollTop = 0;
|
|
543
|
+
start(100, 5); frame();
|
|
544
|
+
assert.equal(animationFrames.size, 0, 'top boundary stops frames');
|
|
545
|
+
scroller.scrollTop = 100;
|
|
546
|
+
for (const type of ['dragleave', 'drop', 'dragend']) {
|
|
547
|
+
start();
|
|
548
|
+
dispatch({ type, target: doc._body, relatedTarget: null });
|
|
549
|
+
assert.equal(animationFrames.size, 0, `${type} cancels pending frames`);
|
|
550
|
+
}
|
|
551
|
+
start(); unmount();
|
|
552
|
+
assert.equal(animationFrames.size, 0, 'hot unload cancels frames');
|
|
553
|
+
});
|
|
554
|
+
|
|
501
555
|
test('拖拽跨组移动:经权威 fiber 通道解析出正确 sessionId', async () => {
|
|
502
556
|
resetDom();
|
|
503
557
|
mount();
|
|
@@ -512,6 +566,31 @@ test('拖拽跨组移动:经权威 fiber 通道解析出正确 sessionId', asy
|
|
|
512
566
|
assert.equal(calls[0].payload.targetWorkspaceId, 'wid-b');
|
|
513
567
|
});
|
|
514
568
|
|
|
569
|
+
test('迁移标题使用权威预览,不包含侧边栏时间;驻留队列提示可能需重启', async () => {
|
|
570
|
+
resetDom();
|
|
571
|
+
mount();
|
|
572
|
+
const row = sessionRow('测试刚刚', 'session-aaa');
|
|
573
|
+
const beta = header('proj-beta');
|
|
574
|
+
fixture({ rows: [row] });
|
|
575
|
+
const prior = rpcResponder;
|
|
576
|
+
setResponder((endpoint, payload) => endpoint === 'mover.preview' ? {
|
|
577
|
+
ok: true, value: {
|
|
578
|
+
title: '测试', status: 'queued', source: { cwd: 'C:/proj-alpha' },
|
|
579
|
+
target: { path: 'C:/proj-beta' }, estimatedBackupBytes: 0,
|
|
580
|
+
warnings: ['resident writer; restart required']
|
|
581
|
+
}
|
|
582
|
+
} : prior(endpoint, payload));
|
|
583
|
+
attach(beta);
|
|
584
|
+
await dragDrop(row, beta, 'session-aaa', { confirm: false });
|
|
585
|
+
const overlay = doc._body.querySelector('.wsm-overlay');
|
|
586
|
+
assert.equal(overlay.querySelector('.wsm-session').textContent, '测试');
|
|
587
|
+
assert.match(overlay.querySelector('.wsm-preview').textContent, /可能需重启/);
|
|
588
|
+
assert.doesNotMatch(overlay.querySelector('.wsm-preview').textContent, /进入空闲队列/);
|
|
589
|
+
dispatch({ type: 'click', target: overlay.querySelector('.wsm-ok') });
|
|
590
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
591
|
+
assert.equal(moveCalls()[0].payload.sessionTitle, '测试');
|
|
592
|
+
});
|
|
593
|
+
|
|
515
594
|
test('兜底映射错配时不得把错配 id 当成真实会话移动(P2 回归)', async () => {
|
|
516
595
|
resetDom();
|
|
517
596
|
mount();
|
|
@@ -562,6 +641,135 @@ test('工作区解析必须自证:标题与路径都不匹配时不得按 DOM
|
|
|
562
641
|
assert.equal(calls.length, 0, '标题与路径都不匹配时必须中止,不得按序号回退');
|
|
563
642
|
});
|
|
564
643
|
|
|
644
|
+
test('工作区身份优先于标题:移入后仍可拖回原工作区', async () => {
|
|
645
|
+
resetDom();
|
|
646
|
+
mount();
|
|
647
|
+
const row = sessionRow('Alpha discussion', 'session-aaa');
|
|
648
|
+
const beta = header('proj-beta');
|
|
649
|
+
const alpha = header('proj-beta'); // Simulate stale visible text after a sidebar update.
|
|
650
|
+
Object.defineProperty(alpha, '__reactFiber$t', { value: {
|
|
651
|
+
memoizedProps: { group: { workspaceId: 'wid-a' } }, return: null
|
|
652
|
+
} });
|
|
653
|
+
fixture({ rows: [row] });
|
|
654
|
+
attach(beta, alpha);
|
|
655
|
+
await dragDrop(row, beta, 'session-aaa');
|
|
656
|
+
await dragDrop(row, alpha, 'session-aaa');
|
|
657
|
+
assert.deepEqual(moveCalls().map((call) => call.payload.targetWorkspaceId), ['wid-b', 'wid-a']);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test('工作区 data-row-key 优先于包含其他工作区名称的文字', async () => {
|
|
661
|
+
resetDom();
|
|
662
|
+
mount();
|
|
663
|
+
const row = sessionRow('Alpha discussion', 'session-aaa');
|
|
664
|
+
const beta = header('proj-alpha proj-beta');
|
|
665
|
+
beta.setAttribute('data-row-key', 'workspace:wid-b');
|
|
666
|
+
Object.defineProperty(beta, '__reactFiber$t', { value: {
|
|
667
|
+
memoizedProps: { group: { workspaceId: 'wid-a' } }, return: null
|
|
668
|
+
} }); // Rendered DOM identity must win over stale React props.
|
|
669
|
+
fixture({ rows: [row] });
|
|
670
|
+
attach(beta);
|
|
671
|
+
await dragDrop(row, beta, 'session-aaa');
|
|
672
|
+
assert.equal(moveCalls()[0]?.payload.targetWorkspaceId, 'wid-b');
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
test('嵌套会话标题不会被用作目标工作区名称', async () => {
|
|
676
|
+
resetDom();
|
|
677
|
+
mount();
|
|
678
|
+
const row = sessionRow('Alpha discussion', 'session-aaa');
|
|
679
|
+
const beta = header('proj-beta');
|
|
680
|
+
beta.appendChild(sessionRow('proj-alpha', 'session-other'));
|
|
681
|
+
fixture({ rows: [row] });
|
|
682
|
+
attach(beta);
|
|
683
|
+
await dragDrop(row, beta, 'session-aaa');
|
|
684
|
+
assert.equal(moveCalls()[0]?.payload.targetWorkspaceId, 'wid-b');
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
test('工作区文字匹配有歧义时停止迁移', async () => {
|
|
688
|
+
resetDom();
|
|
689
|
+
mount();
|
|
690
|
+
const row = sessionRow('Alpha discussion', 'session-aaa');
|
|
691
|
+
const ambiguous = header('proj-alpha proj-beta');
|
|
692
|
+
fixture({ rows: [row] });
|
|
693
|
+
attach(ambiguous);
|
|
694
|
+
await dragDrop(row, ambiguous, 'session-aaa');
|
|
695
|
+
assert.equal(moveCalls().length, 0);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
test('落在标题外时不沿用上次悬停的工作区', async () => {
|
|
699
|
+
resetDom();
|
|
700
|
+
mount();
|
|
701
|
+
const row = sessionRow('Alpha discussion', 'session-aaa');
|
|
702
|
+
const beta = header('proj-beta');
|
|
703
|
+
fixture({ rows: [row] });
|
|
704
|
+
attach(beta);
|
|
705
|
+
const dt = { getData: () => 'session-aaa' };
|
|
706
|
+
dispatch({ type: 'dragstart', target: row, dataTransfer: dt });
|
|
707
|
+
dispatch({ type: 'dragover', target: beta, preventDefault() {} });
|
|
708
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
709
|
+
dispatch({ type: 'drop', target: doc._body, dataTransfer: dt, preventDefault() {}, stopImmediatePropagation() {} });
|
|
710
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
711
|
+
assert.equal(doc._body.querySelector('.wsm-overlay'), null);
|
|
712
|
+
assert.equal(moveCalls().length, 0);
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
test('同工作区预览阻断后可显式选择原工作区并重新预览再移回', async () => {
|
|
716
|
+
resetDom();
|
|
717
|
+
mount();
|
|
718
|
+
const row = sessionRow('Alpha discussion', 'session-aaa');
|
|
719
|
+
const beta = header('proj-beta');
|
|
720
|
+
const { ws } = fixture({ rows: [row] });
|
|
721
|
+
attach(beta);
|
|
722
|
+
setResponder((endpoint, payload) => {
|
|
723
|
+
if (endpoint === 'mover.workspaces') return { ok: true, value: { items: ws } };
|
|
724
|
+
if (endpoint === 'mover.preview') return { ok: true, value: {
|
|
725
|
+
status: payload.targetWorkspaceId === 'wid-b' ? 'blocked' : 'safe',
|
|
726
|
+
source: { cwd: 'C:/proj-beta' },
|
|
727
|
+
target: { path: ws.find((item) => item.workspaceId === payload.targetWorkspaceId).path },
|
|
728
|
+
warnings: [], estimatedBackupBytes: 123
|
|
729
|
+
} };
|
|
730
|
+
return { ok: true, value: {} };
|
|
731
|
+
});
|
|
732
|
+
await dragDrop(row, beta, 'session-aaa', { confirm: false });
|
|
733
|
+
const select = doc._body.querySelector('.wsm-target-select');
|
|
734
|
+
const ok = doc._body.querySelector('.wsm-ok');
|
|
735
|
+
assert.equal(ok.disabled, true);
|
|
736
|
+
select.value = 'wid-a';
|
|
737
|
+
dispatch({ type: 'change', target: select });
|
|
738
|
+
assert.equal(ok.disabled, true, 'Must wait for the new preview');
|
|
739
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
740
|
+
assert.equal(ok.disabled, false);
|
|
741
|
+
assert.match(doc._body.querySelector('.wsm-preview').textContent, /目标路径: C:\/proj-alpha/);
|
|
742
|
+
dispatch({ type: 'click', target: ok });
|
|
743
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
744
|
+
assert.equal(moveCalls()[0]?.payload.targetWorkspaceId, 'wid-a');
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
test('切换目标后预览失败时禁止按旧预览执行', async () => {
|
|
748
|
+
resetDom();
|
|
749
|
+
mount();
|
|
750
|
+
const row = sessionRow('Alpha discussion', 'session-aaa');
|
|
751
|
+
const beta = header('proj-beta');
|
|
752
|
+
const { ws } = fixture({ rows: [row] });
|
|
753
|
+
attach(beta);
|
|
754
|
+
setResponder((endpoint, payload) => {
|
|
755
|
+
if (endpoint === 'mover.workspaces') return { ok: true, value: { items: ws } };
|
|
756
|
+
if (endpoint === 'mover.preview') return payload.targetWorkspaceId === 'wid-a'
|
|
757
|
+
? { ok: false, error: { message: 'preview failed' } }
|
|
758
|
+
: { ok: true, value: { status: 'safe', source: { cwd: 'C:/proj-alpha' } } };
|
|
759
|
+
return { ok: true, value: {} };
|
|
760
|
+
});
|
|
761
|
+
await dragDrop(row, beta, 'session-aaa', { confirm: false });
|
|
762
|
+
const select = doc._body.querySelector('.wsm-target-select');
|
|
763
|
+
select.value = 'wid-a';
|
|
764
|
+
dispatch({ type: 'change', target: select });
|
|
765
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
766
|
+
const ok = doc._body.querySelector('.wsm-ok');
|
|
767
|
+
assert.equal(ok.disabled, true);
|
|
768
|
+
dispatch({ type: 'click', target: ok });
|
|
769
|
+
assert.equal(moveCalls().length, 0);
|
|
770
|
+
dispatch({ type: 'click', target: doc._body.querySelector('.wsm-cancel') });
|
|
771
|
+
});
|
|
772
|
+
|
|
565
773
|
test('弹窗 Escape 关闭并释放 keydown 监听器', async () => {
|
|
566
774
|
resetDom();
|
|
567
775
|
mount();
|