android-stub.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. /**
  2. * android-stub.js — 最小 Android / AutoX 运行环境模拟
  3. *
  4. * 为什么需要它:AutoX 设备端没有 linter、没有构建、没有测试框架
  5. * (见 autox-coding-standards 的「设备端没有门禁」)。
  6. * 产物一旦发到手机上,只能靠肉眼看界面判断对错。
  7. *
  8. * 这个桩把 android.* / ui / storages / files / dialogs 等替换成可观测的假实现,
  9. * 让 tools/smoke.js 能在 PC 上把整个产物真实跑起来:
  10. * · 模块能否加载(作用域与依赖顺序是否正确)
  11. * · 界面树能否构建完整(每个 View 的构造是否踩到不存在的 API)
  12. * · 状态读写是否正确(点击后配置有没有真的变)
  13. * 桩只覆盖产物真正用到的 API,不追求完整。
  14. */
  15. 'use strict';
  16. /* ------------------------------------------------------------ 视图基类 */
  17. class MockLayoutParams {
  18. constructor(w, h) {
  19. this.width = w;
  20. this.height = h;
  21. this.weight = 0;
  22. this.margins = { l: 0, t: 0, r: 0, b: 0 };
  23. }
  24. setMargins(l, t, r, b) {
  25. this.margins = { l, t, r, b };
  26. return this;
  27. }
  28. }
  29. class MockView {
  30. constructor() {
  31. this.children = [];
  32. this.parent = null;
  33. this.layoutParams = null;
  34. this.listeners = {};
  35. this.padding = { l: 0, t: 0, r: 0, b: 0 };
  36. this.background = null;
  37. this.minWidth = 0;
  38. this.minHeight = 0;
  39. this.visibility = 0;
  40. this.tag = null;
  41. }
  42. setLayoutParams(p) { this.layoutParams = p; return this; }
  43. getLayoutParams() { return this.layoutParams; }
  44. addView(v, lp) {
  45. if (lp) { v.layoutParams = lp; }
  46. v.parent = this;
  47. this.children.push(v);
  48. return this;
  49. }
  50. removeAllViews() {
  51. this.children.forEach((c) => { c.parent = null; });
  52. this.children = [];
  53. return this;
  54. }
  55. removeView(v) {
  56. this.children = this.children.filter((c) => c !== v);
  57. v.parent = null;
  58. }
  59. getChildCount() { return this.children.length; }
  60. getChildAt(index) { return this.children[index] || null; }
  61. getParent() { return this.parent; }
  62. setOnClickListener(fn) { this.listeners.click = fn; }
  63. setOnKeyListener(l) { this.listeners.key = l; }
  64. setOnTouchListener(l) { this.listeners.touch = l; }
  65. setBackground(d) { this.background = d; }
  66. setPadding(l, t, r, b) { this.padding = { l, t, r, b }; }
  67. setMinimumHeight(h) { this.minHeight = h; }
  68. setMinimumWidth(w) { this.minWidth = w; }
  69. setMinWidth(w) { this.minWidth = w; }
  70. setClickable(v) { this.clickable = v; }
  71. setFocusable(v) { this.focusable = v; }
  72. setFocusableInTouchMode(v) { this.focusableInTouchMode = v; }
  73. setGravity(g) { this.gravity = g; }
  74. setVisibility(v) { this.visibility = v; }
  75. getVisibility() { return this.visibility === undefined ? 0 : this.visibility; }
  76. setAlpha(a) { this.alpha = a; }
  77. getAlpha() { return this.alpha === undefined ? 1 : this.alpha; }
  78. setClipToOutline(v) { this.clipToOutline = v; }
  79. /* 悬浮球的布局轨迹靠它们:真机读的是「布局结果」,桩里就用 LayoutParams 里写的值顶上 */
  80. addOnLayoutChangeListener(l) {
  81. if (!this.layoutListeners) { this.layoutListeners = []; }
  82. this.layoutListeners.push(l);
  83. }
  84. getLocationOnScreen(out) {
  85. if (out) { out[0] = this.getLeft(); out[1] = this.getTop(); }
  86. return out;
  87. }
  88. /* 悬浮球的状态切换靠"等这一帧画完"(onPreDraw)。桩里不做绘制,
  89. * 所以监听只是登记下来,真正的兜底由 setTimeout(桩里立即执行)走完 —— 结论一样。 */
  90. getViewTreeObserver() {
  91. return {
  92. addOnPreDrawListener: (l) => { if (!this.preDrawListeners) { this.preDrawListeners = []; } this.preDrawListeners.push(l); },
  93. removeOnPreDrawListener: () => { }
  94. };
  95. }
  96. setTag(t) { this.tag = t; }
  97. getTag() { return this.tag; }
  98. requestFocus() { this.focused = true; }
  99. /* 几何:桩不做真正的测量,直接用 LayoutParams 里写的值当"量完之后"的结果。
  100. * 悬浮球的 dumpLayout() 会打印这些数字,有了它们,离线也能核验"球/按钮被放到哪",
  101. * 而不是只能看到一片 0。 */
  102. getWidth() { return numOr(this.layoutParams && this.layoutParams.width, 0); }
  103. getHeight() { return numOr(this.layoutParams && this.layoutParams.height, 0); }
  104. getLeft() { return numOr(this.layoutParams && this.layoutParams.leftMargin, 0); }
  105. getTop() { return numOr(this.layoutParams && this.layoutParams.topMargin, 0); }
  106. getX() { return this.getLeft(); }
  107. getY() { return this.getTop(); }
  108. /* 测试辅助:模拟点击 */
  109. performClick() { if (this.listeners.click) { this.listeners.click(this); } }
  110. /** 测试辅助:模拟一次触摸(走与真机同一条通路的 OnTouchListener) */
  111. dispatchTouch(event) {
  112. if (this.listeners.touch) { return this.listeners.touch.onTouch(this, event); }
  113. return false;
  114. }
  115. }
  116. /** 负值(MATCH_PARENT / WRAP_CONTENT)不该被当成真实尺寸上报 */
  117. function numOr(v, fallback) {
  118. return (typeof v === 'number' && v > 0) ? v : fallback;
  119. }
  120. /* 可见性常量:用**真机的数字**(0/4/8)。桩里不给的话就是 undefined,
  121. * 而 `setVisibility(undefined)` 不报错 —— 于是"某个视图到底是不是 GONE"永远断言不出来。 */
  122. MockView.VISIBLE = 0;
  123. MockView.INVISIBLE = 4;
  124. MockView.GONE = 8;
  125. /**
  126. * TextView 桩。
  127. *
  128. * ★ `setText` 必须**同步触发** TextWatcher —— 真机就是这个顺序
  129. * (已在设备上实测:watcher 回调在 setText 返回前就跑完了)。
  130. * 如果桩里不触发,那些「程序化回写会自触发」的坑就测不出来,
  131. * 界面会在真机上陷入「回写 → 触发监听 → 再回写」的循环。
  132. */
  133. class MockTextView extends MockView {
  134. constructor() {
  135. super();
  136. this.text = '';
  137. this.textSize = 0;
  138. this.textColor = null;
  139. this.typeface = null;
  140. this.singleLine = false;
  141. this.ellipsize = null;
  142. this.hint = '';
  143. this.hintColor = null;
  144. this.maxWidth = 0;
  145. this.watchers = [];
  146. this.selection = 0;
  147. }
  148. setText(t) {
  149. const prev = this.text;
  150. this.text = String(t === null || t === undefined ? '' : t);
  151. this.notifyText(prev, this.text);
  152. }
  153. getText() { return this.text; }
  154. addTextChangedListener(w) { this.watchers.push(w); }
  155. removeTextChangedListener(w) { this.watchers = this.watchers.filter((x) => x !== w); }
  156. notifyText(prev, next) {
  157. this.watchers.forEach((w) => {
  158. if (w.beforeTextChanged) { w.beforeTextChanged(prev, 0, prev.length, next.length); }
  159. if (w.onTextChanged) { w.onTextChanged(next, 0, prev.length, next.length); }
  160. if (w.afterTextChanged) { w.afterTextChanged(next); }
  161. });
  162. }
  163. /** 测试辅助:模拟用户逐字输入(与 IME 走同一条通路) */
  164. type(str) {
  165. for (let i = 0; i < String(str).length; i++) {
  166. const prev = this.text;
  167. this.text = prev + String(str).charAt(i);
  168. this.notifyText(prev, this.text);
  169. }
  170. }
  171. /** 测试辅助:模拟连续退格清空 */
  172. eraseAll() {
  173. while (this.text.length > 0) {
  174. const prev = this.text;
  175. this.text = prev.substring(0, prev.length - 1);
  176. this.notifyText(prev, this.text);
  177. }
  178. }
  179. /** 测试辅助:退一格(从尾部删一个字符) */
  180. backspace() {
  181. if (this.text.length === 0) { return; }
  182. const prev = this.text;
  183. this.text = prev.substring(0, prev.length - 1);
  184. this.notifyText(prev, this.text);
  185. }
  186. /* 监听器在桩里存的是**回调表**(真机上也是个实现了接口的对象),
  187. * 所以统一通过方法名派发,别把回调表当函数直接调 */
  188. setOnFocusChangeListener(l) { this.focusListener = l; }
  189. notifyFocus(hasFocus) {
  190. const l = this.focusListener;
  191. if (!l) { return; }
  192. if (typeof l === 'function') { l(this, hasFocus); return; }
  193. if (typeof l.onFocusChange === 'function') { l.onFocusChange(this, hasFocus); }
  194. }
  195. gainFocus() { this.focused = true; this.notifyFocus(true); }
  196. loseFocus() { this.focused = false; this.notifyFocus(false); }
  197. setTextSize(s) { this.textSize = s; }
  198. setTextColor(c) { this.textColor = c; }
  199. setTypeface(t) { this.typeface = t; }
  200. setSingleLine(v) { this.singleLine = v; }
  201. setEllipsize(e) { this.ellipsize = e; }
  202. setHint(h) { this.hint = h; }
  203. setHintTextColor(c) { this.hintColor = c; }
  204. setMaxWidth(w) { this.maxWidth = w; }
  205. setMaxLines() { }
  206. setLineSpacing(add, mult) { this.lineSpacing = { add, mult }; }
  207. setSelection(i) { this.selection = i; }
  208. setSelectAllOnFocus(v) { this.selectAllOnFocus = v; }
  209. setImeOptions(o) { this.imeOptions = o; }
  210. }
  211. class MockEditText extends MockTextView {
  212. setInputType(t) { this.inputType = t; }
  213. }
  214. class MockScrollView extends MockView {
  215. setVerticalScrollBarEnabled() { }
  216. setOverScrollMode() { }
  217. }
  218. class MockLinearLayout extends MockView {
  219. setOrientation(o) { this.orientation = o; }
  220. }
  221. class MockFrameLayout extends MockView { }
  222. /** WindowManager.LayoutParams 桩:悬浮球用的是裸 WindowManager 这条路 */
  223. class MockWMLayoutParams {
  224. constructor() {
  225. this.type = 0;
  226. this.format = 0;
  227. this.flags = 0;
  228. this.gravity = 0;
  229. this.x = 0;
  230. this.y = 0;
  231. this.width = 0;
  232. this.height = 0;
  233. }
  234. }
  235. class MockImageView extends MockView {
  236. setImageBitmap(b) { this.bitmap = b; }
  237. setImageDrawable(d) { this.drawable = d; }
  238. setScaleType(s) { this.scaleType = s; }
  239. }
  240. MockImageView.ScaleType = { FIT_XY: 'FIT_XY', CENTER_CROP: 'CENTER_CROP', FIT_CENTER: 'FIT_CENTER' };
  241. /**
  242. * MotionEvent 桩。
  243. *
  244. * 悬浮球靠 getAction()/getRawX()/getRawY() 判断「单击 vs 拖动 vs 取消」,
  245. * 而这三者的区分是本模块最容易出错的地方(ACTION_CANCEL 曾经被当成 UP 处理,
  246. * 导致"每次展开立刻又收起")。所以它必须能被离线构造出来,否则那段逻辑永远测不到。
  247. */
  248. class MockMotionEvent {
  249. constructor(action, rawX, rawY) {
  250. this.action = action;
  251. this.rawX = rawX;
  252. this.rawY = rawY;
  253. }
  254. getAction() { return this.action; }
  255. getRawX() { return this.rawX; }
  256. getRawY() { return this.rawY; }
  257. getX() { return this.rawX; }
  258. getY() { return this.rawY; }
  259. }
  260. MockMotionEvent.ACTION_DOWN = 0;
  261. MockMotionEvent.ACTION_UP = 1;
  262. MockMotionEvent.ACTION_MOVE = 2;
  263. MockMotionEvent.ACTION_CANCEL = 3;
  264. /**
  265. * floaty 窗口桩。
  266. *
  267. * 真机上 `floaty.window(xml)` 返回一个已经挂在 WindowManager 上的窗口对象,
  268. * `findView(id)` 从它膨胀出来的视图树里取视图。桩只需要两件事:
  269. * · `findView()` 给回一个可用的 FrameLayout(产物只在里面 add/remove 子视图)
  270. * · 记录 setSize/setPosition 的入参 —— 悬浮球的坐标公式算错过两次(dp/px 混用、
  271. * 漏算根视图内边距),有了这份记录,离线就能断言"球应该落在哪"。
  272. */
  273. class MockFloatyWindow {
  274. constructor() {
  275. this.root = new MockFrameLayout();
  276. this.width = 0;
  277. this.height = 0;
  278. this.x = 0;
  279. this.y = 0;
  280. this.closed = false;
  281. this.focusDisabled = false;
  282. this.adjustEnabled = true;
  283. }
  284. findView() { return this.root; }
  285. setSize(w, h) { this.width = w; this.height = h; return this; }
  286. setPosition(x, y) { this.x = x; this.y = y; return this; }
  287. setAdjustEnabled(v) { this.adjustEnabled = !!v; return this; }
  288. disableFocus() { this.focusDisabled = true; return this; }
  289. close() { this.closed = true; return this; }
  290. getWidth() { return this.width; }
  291. getHeight() { return this.height; }
  292. getX() { return this.x; }
  293. getY() { return this.y; }
  294. }
  295. MockLinearLayout.LayoutParams = MockLayoutParams;
  296. MockFrameLayout.LayoutParams = MockLayoutParams;
  297. class MockGradientDrawable {
  298. constructor() {
  299. this.colors = null;
  300. this.color = null;
  301. this.radius = 0;
  302. this.stroke = null;
  303. }
  304. setShape(s) { this.shape = s; }
  305. setColor(c) { this.color = c; }
  306. setCornerRadius(r) { this.radius = r; }
  307. setStroke(w, c) { this.stroke = { width: w, color: c }; }
  308. setOrientation(o) { this.orientation = o; }
  309. setColors(list) { this.colors = list; }
  310. setGradientType(t) { this.gradientType = t; }
  311. setGradientRadius(r) { this.gradientRadius = r; }
  312. /* 登录按钮的「校验中」态靠整体降透明度表达 */
  313. setAlpha(a) { this.alpha = a; }
  314. }
  315. /* GradientDrawable 的静态常量:产物在代码里设置形状与渐变方向时会读它们 */
  316. MockGradientDrawable.RECTANGLE = 0;
  317. MockGradientDrawable.OVAL = 1;
  318. MockGradientDrawable.LINE = 2;
  319. MockGradientDrawable.LINEAR_GRADIENT = 0;
  320. MockGradientDrawable.RADIAL_GRADIENT = 1;
  321. MockGradientDrawable.SWEEP_GRADIENT = 2;
  322. MockGradientDrawable.Orientation = {
  323. LEFT_RIGHT: 0, RIGHT_LEFT: 1, TOP_BOTTOM: 2, BOTTOM_TOP: 3, TL_BR: 4, BL_TR: 5
  324. };
  325. class MockIntent {
  326. constructor(action, uri) { this.action = action; this.uri = uri || null; this.extras = {}; }
  327. setData(uri) { this.uri = uri; return this; }
  328. putExtra(k, v) { this.extras[k] = v; return this; }
  329. addFlags() { return this; }
  330. }
  331. /* 真机上的值,桩里保持同一个数字:断言能对得上,
  332. * 也不会因为桩漏了常量而静默变成 undefined(那会让"拉回前台"悄悄失效)。 */
  333. MockIntent.FLAG_ACTIVITY_REORDER_TO_FRONT = 0x20000000;
  334. class MockUri {
  335. static parse(s) { return { raw: s }; }
  336. }
  337. class MockAdapterListener {
  338. constructor(obj) { Object.assign(this, obj || {}); }
  339. }
  340. /**
  341. * Java 接口桩:真机上 `new Interface({...})` 走的是 Rhino 的 JavaAdapter,
  342. * 拿到的是一个实现了该接口的对象。桩里直接把回调表原样返回 ——
  343. * 只要「回调能被存下来并在对的时机被调用」,被测代码的行为就与真机一致。
  344. */
  345. MockView.OnFocusChangeListener = function (handlers) { return handlers; };
  346. MockView.OnClickListener = function (handlers) { return handlers; };
  347. MockView.OnKeyListener = function (handlers) { return handlers; };
  348. /* 悬浮球的球体靠它收手势(单击 / 拖动 / 取消),必须能构造 */
  349. MockView.OnTouchListener = function (handlers) { return handlers; };
  350. MockView.OnLayoutChangeListener = function (handlers) { return handlers; };
  351. MockView.OnPreDrawListener = function (handlers) { return handlers; };
  352. /* ------------------------------------------------------------ 状态容器 */
  353. function install() {
  354. const android = {
  355. graphics: {
  356. drawable: { GradientDrawable: MockGradientDrawable },
  357. Typeface: { DEFAULT_BOLD: 'BOLD', MONOSPACE: 'MONO', DEFAULT: 'DEFAULT' },
  358. PixelFormat: { OPAQUE: -1 }
  359. },
  360. widget: {
  361. TextView: MockTextView,
  362. EditText: MockEditText,
  363. ScrollView: MockScrollView,
  364. LinearLayout: MockLinearLayout,
  365. FrameLayout: MockFrameLayout,
  366. ImageView: MockImageView,
  367. AdapterView: { OnItemSelectedListener: MockAdapterListener }
  368. },
  369. view: {
  370. View: MockView,
  371. Gravity: { CENTER: 17, CENTER_HORIZONTAL: 1, CENTER_VERTICAL: 16, TOP: 48, LEFT: 3, RIGHT: 5, END: 8388613 },
  372. KeyEvent: { KEYCODE_BACK: 4 },
  373. MotionEvent: MockMotionEvent,
  374. /* 注意必须是 function 而不是箭头函数:产物里是 `new android.view.ViewTreeObserver.OnPreDrawListener({...})` */
  375. ViewTreeObserver: { OnPreDrawListener: function (h) { return h; } },
  376. WindowManager: { LayoutParams: MockWMLayoutParams },
  377. inputmethod: { EditorInfo: { IME_ACTION_DONE: 6, IME_ACTION_NEXT: 5 } }
  378. },
  379. text: {
  380. TextUtils: { TruncateAt: { START: 'START', END: 'END', MIDDLE: 'MIDDLE' } },
  381. InputType: { TYPE_CLASS_TEXT: 1, TYPE_CLASS_NUMBER: 2, TYPE_NUMBER_FLAG_DECIMAL: 8192 },
  382. TextWatcher: function (handlers) { return handlers; }
  383. },
  384. content: {
  385. Intent: MockIntent,
  386. Context: { MODE_PRIVATE: 0 },
  387. pm: { PackageManager: { PERMISSION_GRANTED: 0, PERMISSION_DENIED: -1 } }
  388. },
  389. net: { Uri: MockUri },
  390. os: { Build: { MODEL: 'SmokeDevice', VERSION: { RELEASE: '14', SDK: '34' } } }
  391. };
  392. const memoryStorage = {};
  393. const memoryFiles = {};
  394. const mounted = { root: null };
  395. const logLines = [];
  396. /* 计时器:
  397. * · setTimeout 立即执行 —— 登录流程用它留一帧给「登录中…」,
  398. * 自检里没有理由真的等 450ms;
  399. * · setInterval **不自动跑**,只登记句柄(自动跑就是死循环)。
  400. * 需要验证计时逻辑时由测试调用 flushIntervals() 手动推进。 */
  401. const intervals = new Map();
  402. let intervalSeq = 0;
  403. const wmWindows = [];
  404. const wmStub = {
  405. addView: (view, lp) => { view.setLayoutParams(lp); wmWindows.push({ view: view, lp: lp }); },
  406. updateViewLayout: (view, lp) => { view.setLayoutParams(lp); },
  407. removeView: (view) => {
  408. const i = wmWindows.findIndex((w) => w.view === view);
  409. if (i >= 0) { wmWindows.splice(i, 1); }
  410. }
  411. };
  412. const context = {
  413. getResources: () => ({
  414. getDisplayMetrics: () => ({ density: 2.75 }),
  415. getIdentifier: () => 0,
  416. getDimensionPixelSize: () => 66
  417. }),
  418. getPackageName: () => 'org.autojs.autoxjs.v7',
  419. getPackageManager: () => ({
  420. getPackageInfo: (name) => {
  421. /* 让 TikTok 主包与 AutoX 自身"存在",
  422. * 既验证 device.js 的多包探测与回退,也覆盖 appVersionName 的成功路径 */
  423. if (name === 'com.zhiliaoapp.musically') { return { versionName: '39.5.4' }; }
  424. if (name === 'org.autojs.autoxjs.v7') { return { versionName: '9.3.11' }; }
  425. throw new Error('NameNotFoundException: ' + name);
  426. }
  427. }),
  428. /* 悬浮球走裸 WindowManager(不用 floaty),桩里给它一个可观测的实现:
  429. * 记录加过哪些窗口、它们的 LayoutParams,方便断言「两个窗口、尺寸恒定、触摸开关」。 */
  430. getSystemService: (name) => (name === 'window' ? wmStub : null),
  431. /* 通知权限桩成「未授予」:Android 13+ 才走这条分支,
  432. * 而这正是「通知标可选」那段逻辑唯一的触发条件 */
  433. checkSelfPermission: () => -1,
  434. startActivity: () => { }
  435. };
  436. const g = globalThis;
  437. g.android = android;
  438. g.context = context;
  439. g.device = { width: 1080, height: 1920 };
  440. g.auto = { service: null };
  441. /* 悬浮球的三件套:floaty(开窗)/ images(品牌图)/ MotionEvent(手势)。
  442. * 缺任意一个,悬浮球在离线自检里都会"静默不建"(产物把创建包在 try 里),
  443. * 于是整块功能零覆盖 —— 而这正是「扇形按钮点了没反应」溜到真机的原因。 */
  444. const floatyState = { windows: [] };
  445. g.floaty = {
  446. checkPermission: () => true,
  447. window: () => {
  448. const w = new MockFloatyWindow();
  449. floatyState.windows.push(w);
  450. return w;
  451. },
  452. closeAll: () => { }
  453. };
  454. g.images = {
  455. read: () => ({ bitmap: { stub: true } }),
  456. release: () => { }
  457. };
  458. g.sleep = () => { };
  459. g.toast = () => { };
  460. g.app = {
  461. viewFile: () => { },
  462. /* 「拉回前台」靠它。桩里记下来,自检据此断言确实按 REORDER_TO_FRONT 发的 Intent。 */
  463. startActivity: (intent) => { g.app.lastIntent = intent; },
  464. /** 「切回 TikTok」靠它 */
  465. launchPackage: (name) => { g.app.lastLaunchedPackage = name; }
  466. };
  467. /* UI 模式下当前 Activity(拉回前台时用它取类名) */
  468. g.activity = {
  469. getClass: () => ({ getName: () => 'org.autojs.autojs.ui.script.ScriptExecuteActivity' })
  470. };
  471. g.threads = { start: (fn) => { fn(); }, shutDownAll: () => { } };
  472. g.setTimeout = (fn) => { fn(); return 0; };
  473. g.setInterval = (fn) => {
  474. intervalSeq += 1;
  475. intervals.set(intervalSeq, fn);
  476. return intervalSeq;
  477. };
  478. g.clearInterval = (handle) => { intervals.delete(handle); };
  479. /* clearTimeout 必须有:状态切换靠"等布局落地 + 兜底定时器",
  480. * 落地时要把兜底那个清掉(真机上是真清,桩里立即执行所以本来就是空操作) */
  481. g.clearTimeout = () => { };
  482. /* 事件登记表:自检可以手动触发 "resume" 之类的生命周期事件 */
  483. const uiEvents = {};
  484. g.ui = {
  485. statusBarColor: () => { },
  486. setContentView: (v) => { mounted.root = v; },
  487. run: (fn) => fn(),
  488. post: (fn) => fn(),
  489. emitter: {
  490. on: (name, fn) => { uiEvents[name] = fn; },
  491. emit: (name, payload) => { if (uiEvents[name]) { uiEvents[name](payload); } }
  492. }
  493. };
  494. g.storages = {
  495. create: (name) => ({
  496. get: (k, d) => {
  497. const bucket = memoryStorage[name] || {};
  498. return Object.prototype.hasOwnProperty.call(bucket, k) ? bucket[k] : d;
  499. },
  500. put: (k, v) => {
  501. memoryStorage[name] = memoryStorage[name] || {};
  502. memoryStorage[name][k] = v;
  503. },
  504. remove: (k) => {
  505. if (memoryStorage[name]) { delete memoryStorage[name][k]; }
  506. }
  507. })
  508. };
  509. /* 文件读写走内存,绝不碰真实磁盘——自检不应该有副作用 */
  510. g.files = {
  511. write: (p, c) => { memoryFiles[p] = String(c); },
  512. read: (p) => (Object.prototype.hasOwnProperty.call(memoryFiles, p) ? memoryFiles[p] : ''),
  513. exists: (p) => Object.prototype.hasOwnProperty.call(memoryFiles, p),
  514. listDir: () => [],
  515. isDir: () => false,
  516. ensureDir: () => { }
  517. };
  518. /* ------------------------------------------------------------ 引擎桩
  519. *
  520. * 入口脚本与产物路径固定下来,让 Engine.locate() 稳定命中。
  521. * 路径刻意做成「项目目录 + 相对位置」,与真机的项目模式一致 ——
  522. * 于是"从 myEngine().source 推出同级目录"这条推导逻辑也被测到,
  523. * 而不是只测"某个写死的路径存在"。
  524. */
  525. const ENTRY_SOURCE = '/sdcard/Scripts/demo/main.js';
  526. const ENGINE_SCRIPT = '/sdcard/Scripts/demo/tiktok-auto/dist/tiktok-auto.js';
  527. memoryFiles[ENGINE_SCRIPT] = '/* 引擎产物(桩) */';
  528. const engineState = {
  529. /** execScriptFile 的调用记录 —— 自检据此断言参数传对了 */
  530. spawned: [],
  531. /** 除宿主之外的引擎。清空即模拟「子引擎已退出」 */
  532. children: [{ id: 'child-1' }]
  533. };
  534. g.engines = {
  535. myEngine: () => ({ id: 'host-1', source: ENTRY_SOURCE, execArgv: {} }),
  536. all: () => [{ id: 'host-1' }].concat(engineState.children),
  537. execScriptFile: (scriptPath, config) => {
  538. engineState.spawned.push({ path: scriptPath, config: config });
  539. /* ★ 真机的 `ScriptExecution.getEngine()` 会返回子引擎对象(也可能是 null,
  540. * 取决于子引擎建好了没)。这里返回 `children[0]` 是为了让
  541. * `Engine.childId` 精确匹配那条路径被测到 —— 否则永远走"任意第二个引擎"
  542. * 的宽口径,精确匹配等于没测。 */
  543. return {
  544. getEngine: () => engineState.children[0] || null
  545. };
  546. }
  547. };
  548. g.dialogs = {
  549. select: () => -1,
  550. input: () => null,
  551. confirm: () => false,
  552. alert: () => { }
  553. };
  554. /* 桩原样转发 console.log,但留一份副本给自检断言用 */
  555. const originalLog = console.log;
  556. g.console = Object.assign({}, console, {
  557. log: (...args) => {
  558. logLines.push(args.join(' '));
  559. originalLog(...args);
  560. }
  561. });
  562. return {
  563. mounted,
  564. memoryStorage,
  565. memoryFiles,
  566. logLines,
  567. /** 手动触发 UI 生命周期事件(如 "resume") */
  568. emitter: g.ui.emitter,
  569. /** 已登记的事件名 → 回调,用于断言"确实注册了" */
  570. uiEvents,
  571. /** 引擎桩的可变状态:调用记录 + 子引擎列表(清空 children 即模拟引擎退出) */
  572. engineState,
  573. ENTRY_SOURCE,
  574. ENGINE_SCRIPT,
  575. /** 已登记的 interval 回调数量(跑完一次运行后应为 1,停止后应为 0)。 */
  576. intervalCount: () => intervals.size,
  577. /** 手动推进一次所有 interval,用于验证运行计时。 */
  578. flushIntervals: () => { intervals.forEach((fn) => fn()); },
  579. /** 已加到 WindowManager 上的窗口(悬浮球现在是两个 overlay 窗口)。 */
  580. wmWindows: () => wmWindows,
  581. /** 已创建的 floaty 窗口(历史遗留:浮窗早已不用 floaty)。 */
  582. floatyWindows: () => floatyState.windows,
  583. /** 造一个触摸事件(走产物真实的 OnTouchListener 通路)。 */
  584. touchEvent: (action, x, y) => new MockMotionEvent(action, x, y),
  585. MotionEvent: MockMotionEvent
  586. };
  587. }
  588. module.exports = { install };