dsh-workspace-mover 2.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-workspace-mover",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "在 DSH 侧边栏安全迁移会话:真迁移原始档案,支持失联会话恢复、移动历史、一键撤回和 agent 工具。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -22,7 +22,8 @@
22
22
  "docs/media",
23
23
  "LICENSE"
24
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",
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",
26
27
  "check:syntax": "node --check lib/index.js && node --check client/client.js",
27
28
  "check:pack": "npm pack --dry-run",
28
29
  "check": "npm run check:syntax && npm test && npm run check:pack"
@@ -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();