@mrrisega/dsh-remote 0.6.14 → 0.6.16

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.
Files changed (35) hide show
  1. package/clients/dsh-remote/dsh-bridge.mjs +64 -13
  2. package/clients/dsh-remote/dsh-events.mjs +22 -2
  3. package/clients/dsh-remote/e2ee-client.mjs +97 -13
  4. package/clients/dsh-remote/e2ee-shim-script.js +31 -3
  5. package/clients/dsh-remote/mobile-adapter.mjs +190 -108
  6. package/clients/dsh-remote/test/dsh-events.test.mjs +89 -0
  7. package/clients/dsh-remote/test/e2ee-bridge.test.mjs +73 -3
  8. package/clients/dsh-remote/test/e2ee-client.test.mjs +42 -1
  9. package/clients/dsh-remote/test/e2ee-shim.test.mjs +10 -1
  10. package/clients/dsh-remote/test/mobile-adapter-guards.test.mjs +45 -1
  11. package/clients/dsh-remote/test/mobile-adapter-image.test.mjs +325 -0
  12. package/clients/dsh-remote/test/mobile-adapter-runtime.test.mjs +3 -1
  13. package/clients/dsh-remote/test/wechat-runtime.test.mjs +268 -1
  14. package/clients/dsh-remote/upstream-discovery.mjs +186 -30
  15. package/clients/dsh-remote/wechat-channel.mjs +86 -3
  16. package/clients/dsh-remote/wechat-runtime.mjs +253 -10
  17. package/dsh-setup.mjs +161 -7
  18. package/package.json +1 -1
  19. package/packages/dsh-remote-web/lib/client.js +31 -1
  20. package/packages/dsh-remote-web/lib/index.js +392 -49
  21. package/packages/dsh-remote-web/package.json +1 -1
  22. package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +64 -13
  23. package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-events.mjs +22 -2
  24. package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-client.mjs +97 -13
  25. package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-shim-script.js +31 -3
  26. package/packages/dsh-remote-web/runtime/clients/dsh-remote/mobile-adapter.mjs +190 -108
  27. package/packages/dsh-remote-web/runtime/clients/dsh-remote/upstream-discovery.mjs +186 -30
  28. package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-channel.mjs +86 -3
  29. package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-runtime.mjs +253 -10
  30. package/packages/dsh-remote-web/runtime/dsh-setup.mjs +161 -7
  31. package/packages/dsh-remote-web/test/doctor-cli.test.mjs +117 -0
  32. package/packages/dsh-remote-web/test/linux-bridge.test.mjs +346 -0
  33. package/packages/dsh-remote-web/test/picker-pin.test.mjs +80 -27
  34. package/packages/dsh-remote-web/test/self-manage.test.mjs +12 -0
  35. package/clients/dsh-remote/test/boot-tip.test.mjs +0 -158
@@ -1,158 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * 镜像页首屏加载提示的契约与行为测试(0.6.14)。
4
- *
5
- * 用户反馈:「普通用户加载进入镜像页面时等待时间较长,目前等待过程中一直在转圈。
6
- * 建议给普通用户增加一个友好的提示(例如提示"正在排队中")。」
7
- *
8
- * 首屏为什么慢:镜像页要拉 dsh web 的全部客户端插件(实测 ~14 MB 的一个**不可拆分**聚合请求),
9
- * 免费档限速下几十秒起步。官方那个转圈不告诉用户任何事,用户只能反复刷新 —— 反而更慢。
10
- *
11
- * 这里守四件事:
12
- * ① 提示必须在**页面解析的第一时间**出现(早于所有主机判定 —— 首屏慢的正是判定等不到的时候);
13
- * ② 分阶段补充说明(8s / 30s)与秒表,让"到底等了多久"可见;
14
- * ③ 官方 UI 就绪后自动撤下;且有 4 分钟兜底 + pagehide/unload 清理,绝不留悬挂定时器;
15
- * ④ 只做提示、不做拦截(pointer-events:none),提示失效也绝不挡住用户操作。
16
- *
17
- * 用法: node --test test/boot-tip.test.mjs
18
- */
19
- import assert from "node:assert/strict";
20
- import { readFileSync } from "node:fs";
21
- import path from "node:path";
22
- import test from "node:test";
23
- import vm from "node:vm";
24
- import { fileURLToPath } from "node:url";
25
-
26
- const HERE = path.dirname(fileURLToPath(import.meta.url));
27
- const SRC = readFileSync(path.join(HERE, "..", "mobile-adapter.mjs"), "utf8");
28
- const STYLE = SRC.slice(SRC.indexOf("const STYLE = `") + "const STYLE = `".length, SRC.indexOf("`;\n\n/**", SRC.indexOf("const STYLE = `")));
29
- const SCRIPT_START = SRC.indexOf("const SCRIPT = `(() => {") + "const SCRIPT = `".length;
30
- const SCRIPT = SRC.slice(SCRIPT_START, SRC.indexOf("`;\n\n/**", SCRIPT_START));
31
- /**
32
- * 只取「首屏提示」这一段来跑行为。
33
- * 为什么不像别的用例那样跑整个适配层:整段要一整套 DOM 桩(抽屉/遮罩/设置面板…),
34
- * 而这里要验的是**提示的生命周期**,隔离出来既稳又准(改动范围也正好是这一段)。
35
- */
36
- const BOOT = SCRIPT.slice(SCRIPT.indexOf("var maBootTip"), SCRIPT.indexOf("/* —— 主机门解除"));
37
-
38
- /** 极简 DOM 桩:只支撑"提示出现/撤下"这条路径。 */
39
- function makeDom({ body = true, ready = false, frame = false } = {}) {
40
- const nodes = [];
41
- const el = (tag) => {
42
- const n = {
43
- tagName: String(tag).toUpperCase(), className: "", _text: "", children: [], attrs: {}, parentNode: null,
44
- set innerHTML(v) { this._html = v; }, get innerHTML() { return this._html || ""; },
45
- set textContent(v) { this._text = String(v); }, get textContent() { return this._text; },
46
- setAttribute(k, v) { this.attrs[k] = String(v); },
47
- appendChild(c) { c.parentNode = this; this.children.push(c); return c; },
48
- removeChild(c) { this.children = this.children.filter((x) => x !== c); c.parentNode = null; return c; },
49
- querySelector(sel) {
50
- const cls = String(sel).replace(/^\./, "");
51
- const hit = (x) => (x.className || "").split(/\s+/).includes(cls);
52
- const walk = (x) => { for (const c of x.children) { if (hit(c)) return c; const d = walk(c); if (d) return d; } return null; };
53
- return walk(this);
54
- }
55
- };
56
- return n;
57
- };
58
- const doc = {
59
- readyState: ready ? "complete" : "loading",
60
- listeners: {},
61
- createElement: el,
62
- querySelector(sel) {
63
- if (sel === ".dsh-ma-boot") return nodes.find((n) => (n.className || "").includes("dsh-ma-boot")) || null;
64
- if (frame && /frame|textarea|contenteditable|textbox|sidebar-collapsed/.test(sel)) return el("div");
65
- return null;
66
- },
67
- addEventListener(type, fn) { (this.listeners[type] = this.listeners[type] || []).push(fn); },
68
- dispatch(type) { (this.listeners[type] || []).forEach((fn) => fn({})); }
69
- };
70
- if (body) { doc.body = el("body"); doc.body.className = ""; }
71
- else { doc.body = null; doc.bodyHolder = el("body"); }
72
- return { doc, nodes, el, attachBody() { doc.body = doc.bodyHolder; } };
73
- }
74
-
75
- function run(env) {
76
- const timers = [];
77
- const intervals = [];
78
- const win = {
79
- listeners: {},
80
- addEventListener(type, fn) { (this.listeners[type] = this.listeners[type] || []).push(fn); },
81
- dispatch(type) { (this.listeners[type] || []).forEach((fn) => fn({})); },
82
- innerWidth: 390, innerHeight: 844,
83
- location: { href: "https://example.test/remote/dev-x/", origin: "https://example.test" },
84
- MutationObserver: class { observe() {} disconnect() {} },
85
- requestAnimationFrame: (fn) => { timers.push({ fn, ms: 0 }); return timers.length; },
86
- cancelAnimationFrame() {}, matchMedia: () => ({ matches: false, addEventListener() {}, removeEventListener() {} })
87
- };
88
- const ctx = {
89
- window: win,
90
- document: env.doc,
91
- location: win.location,
92
- navigator: { userAgent: "node-test", language: "zh-CN" },
93
- console: { warn() {}, log() {}, error() {} },
94
- setTimeout: (fn, ms) => { timers.push({ fn, ms }); return timers.length; },
95
- clearTimeout: () => {},
96
- setInterval: (fn, ms) => { intervals.push({ fn, ms }); return intervals.length; },
97
- clearInterval: () => {},
98
- Date, Math, JSON, Object, Array, String, Number, Boolean, Error, RegExp, Promise, Map, Set
99
- };
100
- vm.createContext(ctx);
101
- vm.runInContext(BOOT, ctx, { timeout: 5000 });
102
- return { win, timers, intervals };
103
- }
104
-
105
- test("样式:提示层不挡点击(pointer-events:none)、覆盖所有宽度、有转圈与秒表样式", () => {
106
- assert.match(STYLE, /\.dsh-ma-boot\s*\{[\s\S]*?pointer-events:\s*none/, "提示层绝不能拦截点击");
107
- assert.ok(!/@media \(max-width: 820px\) \{\s*\.dsh-ma-boot/.test(STYLE), "提示不受窄屏限定(Windows/桌面浏览器同样要等)");
108
- assert.match(STYLE, /\.dsh-ma-boot-spin/, "要有加载动画");
109
- assert.match(STYLE, /\.dsh-ma-boot-elapsed/, "要有秒表");
110
- });
111
-
112
- test("★ 行为:页面还在解析(readyState=loading)也要立刻挂出提示", () => {
113
- const env = makeDom({ ready: false, frame: false });
114
- const { win, timers } = run(env);
115
- // 提示挂载挂在 DOMContentLoaded 上(此时 body 可能还没出来)
116
- assert.ok((env.doc.listeners.DOMContentLoaded || []).length > 0, "必须监听 DOMContentLoaded 再挂提示");
117
- env.doc.dispatch("DOMContentLoaded");
118
- const tip = env.doc.body.children.find((c) => (c.className || "").includes("dsh-ma-boot"));
119
- assert.ok(tip, "解析阶段就要出现提示(这正是首屏最慢的时候)");
120
- assert.match(tip.innerHTML, /正在加载远程桌面/);
121
- assert.match(tip.innerHTML, /首次打开需要下载/, "要说明为什么慢");
122
- assert.ok(timers.some((t) => t.ms === 8000) && timers.some((t) => t.ms === 30000), "要有 8s/30s 两段补充说明");
123
- assert.ok(timers.some((t) => t.ms === 240000), "要有 4 分钟兜底撤下");
124
- assert.ok((win.listeners.pagehide || []).length > 0, "pagehide 要清理(不留悬挂定时器)");
125
- });
126
-
127
- test("★ 行为:官方 UI 就绪后自动撤下提示", () => {
128
- const env = makeDom({ ready: true, frame: true }); // ready=complete → 立即挂;frame=true → 已就绪
129
- const { intervals } = run(env);
130
- const tip = env.doc.body.children.find((c) => (c.className || "").includes("dsh-ma-boot"));
131
- assert.ok(tip, "先挂上");
132
- assert.ok(intervals.length >= 2, "要有秒表 + 就绪轮询两个定时器");
133
- // 两个 interval 都是 1000ms(秒表 / 就绪轮询),顺序不保证 —— 全部推一次,模拟"1 秒过去"
134
- intervals.forEach((t) => t.fn());
135
- assert.equal(tip.attrs["data-fading"], "1", "官方 UI 就绪后应进入淡出");
136
- });
137
-
138
- test("行为:脚本在 <head> 里先跑(readyState=loading,body 还没出来)也绝不能抛,body 就位后补挂", () => {
139
- // 真实时序就是这条:适配层被内联在 <head>,执行时 body 可能还不存在
140
- const env = makeDom({ body: false, ready: false });
141
- assert.doesNotThrow(() => run(env), "body 缺失时绝不能抛(适配层其他逻辑还要跑)");
142
- env.attachBody();
143
- env.doc.dispatch("DOMContentLoaded");
144
- const tip = env.doc.body.children.find((c) => (c.className || "").includes("dsh-ma-boot"));
145
- assert.ok(tip, "body 就位后补挂提示");
146
- assert.match(tip.innerHTML, /正在加载远程桌面/);
147
- });
148
-
149
- test("源码契约:提示必须早于主机判定(HOSTISH/NARROW),否则首屏最慢那段没有提示", () => {
150
- // ⚠️ 基准要用 **HOSTISH 的定义**(const HOSTISH =),不能用 indexOf("HOSTISH"):
151
- // 提示那段自己的注释里就提到了 HOSTISH,拿注释当基准会把顺序判反(第一版就这么红的)。
152
- const iTip = SCRIPT.indexOf("var maBootTip");
153
- const iHost = SCRIPT.indexOf("const HOSTISH");
154
- assert.ok(iTip > 0 && iHost > 0, "找不到提示段或主机判定");
155
- assert.ok(iTip < iHost, "提示段必须在主机判定之前");
156
- assert.ok(BOOT.includes("pointer-events: none") || STYLE.includes("pointer-events: none"), "必须写明不拦截点击");
157
- assert.ok(!/dsh-ma-boot[\s\S]{0,400}?pointer-events:\s*auto/.test(STYLE), "提示层不得改成可点击拦截");
158
- });