| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621 |
- /**
- * android-stub.js — 最小 Android / AutoX 运行环境模拟
- *
- * 为什么需要它:AutoX 设备端没有 linter、没有构建、没有测试框架
- * (见 autox-coding-standards 的「设备端没有门禁」)。
- * 产物一旦发到手机上,只能靠肉眼看界面判断对错。
- *
- * 这个桩把 android.* / ui / storages / files / dialogs 等替换成可观测的假实现,
- * 让 tools/smoke.js 能在 PC 上把整个产物真实跑起来:
- * · 模块能否加载(作用域与依赖顺序是否正确)
- * · 界面树能否构建完整(每个 View 的构造是否踩到不存在的 API)
- * · 状态读写是否正确(点击后配置有没有真的变)
- * 桩只覆盖产物真正用到的 API,不追求完整。
- */
- 'use strict';
- /* ------------------------------------------------------------ 视图基类 */
- class MockLayoutParams {
- constructor(w, h) {
- this.width = w;
- this.height = h;
- this.weight = 0;
- this.margins = { l: 0, t: 0, r: 0, b: 0 };
- }
- setMargins(l, t, r, b) {
- this.margins = { l, t, r, b };
- return this;
- }
- }
- class MockView {
- constructor() {
- this.children = [];
- this.parent = null;
- this.layoutParams = null;
- this.listeners = {};
- this.padding = { l: 0, t: 0, r: 0, b: 0 };
- this.background = null;
- this.minWidth = 0;
- this.minHeight = 0;
- this.visibility = 0;
- this.tag = null;
- }
- setLayoutParams(p) { this.layoutParams = p; return this; }
- getLayoutParams() { return this.layoutParams; }
- addView(v, lp) {
- if (lp) { v.layoutParams = lp; }
- v.parent = this;
- this.children.push(v);
- return this;
- }
- removeAllViews() {
- this.children.forEach((c) => { c.parent = null; });
- this.children = [];
- return this;
- }
- removeView(v) {
- this.children = this.children.filter((c) => c !== v);
- v.parent = null;
- }
- getChildCount() { return this.children.length; }
- getChildAt(index) { return this.children[index] || null; }
- getParent() { return this.parent; }
- setOnClickListener(fn) { this.listeners.click = fn; }
- setOnKeyListener(l) { this.listeners.key = l; }
- setOnTouchListener(l) { this.listeners.touch = l; }
- setBackground(d) { this.background = d; }
- setPadding(l, t, r, b) { this.padding = { l, t, r, b }; }
- setMinimumHeight(h) { this.minHeight = h; }
- setMinimumWidth(w) { this.minWidth = w; }
- setMinWidth(w) { this.minWidth = w; }
- setClickable(v) { this.clickable = v; }
- setFocusable(v) { this.focusable = v; }
- setFocusableInTouchMode(v) { this.focusableInTouchMode = v; }
- setGravity(g) { this.gravity = g; }
- setVisibility(v) { this.visibility = v; }
- getVisibility() { return this.visibility === undefined ? 0 : this.visibility; }
- setAlpha(a) { this.alpha = a; }
- getAlpha() { return this.alpha === undefined ? 1 : this.alpha; }
- setClipToOutline(v) { this.clipToOutline = v; }
- /* 悬浮球的布局轨迹靠它们:真机读的是「布局结果」,桩里就用 LayoutParams 里写的值顶上 */
- addOnLayoutChangeListener(l) {
- if (!this.layoutListeners) { this.layoutListeners = []; }
- this.layoutListeners.push(l);
- }
- getLocationOnScreen(out) {
- if (out) { out[0] = this.getLeft(); out[1] = this.getTop(); }
- return out;
- }
- /* 悬浮球的状态切换靠"等这一帧画完"(onPreDraw)。桩里不做绘制,
- * 所以监听只是登记下来,真正的兜底由 setTimeout(桩里立即执行)走完 —— 结论一样。 */
- getViewTreeObserver() {
- return {
- addOnPreDrawListener: (l) => { if (!this.preDrawListeners) { this.preDrawListeners = []; } this.preDrawListeners.push(l); },
- removeOnPreDrawListener: () => { }
- };
- }
- setTag(t) { this.tag = t; }
- getTag() { return this.tag; }
- requestFocus() { this.focused = true; }
- /* 几何:桩不做真正的测量,直接用 LayoutParams 里写的值当"量完之后"的结果。
- * 悬浮球的 dumpLayout() 会打印这些数字,有了它们,离线也能核验"球/按钮被放到哪",
- * 而不是只能看到一片 0。 */
- getWidth() { return numOr(this.layoutParams && this.layoutParams.width, 0); }
- getHeight() { return numOr(this.layoutParams && this.layoutParams.height, 0); }
- getLeft() { return numOr(this.layoutParams && this.layoutParams.leftMargin, 0); }
- getTop() { return numOr(this.layoutParams && this.layoutParams.topMargin, 0); }
- getX() { return this.getLeft(); }
- getY() { return this.getTop(); }
- /* 测试辅助:模拟点击 */
- performClick() { if (this.listeners.click) { this.listeners.click(this); } }
- /** 测试辅助:模拟一次触摸(走与真机同一条通路的 OnTouchListener) */
- dispatchTouch(event) {
- if (this.listeners.touch) { return this.listeners.touch.onTouch(this, event); }
- return false;
- }
- }
- /** 负值(MATCH_PARENT / WRAP_CONTENT)不该被当成真实尺寸上报 */
- function numOr(v, fallback) {
- return (typeof v === 'number' && v > 0) ? v : fallback;
- }
- /* 可见性常量:用**真机的数字**(0/4/8)。桩里不给的话就是 undefined,
- * 而 `setVisibility(undefined)` 不报错 —— 于是"某个视图到底是不是 GONE"永远断言不出来。 */
- MockView.VISIBLE = 0;
- MockView.INVISIBLE = 4;
- MockView.GONE = 8;
- /**
- * TextView 桩。
- *
- * ★ `setText` 必须**同步触发** TextWatcher —— 真机就是这个顺序
- * (已在设备上实测:watcher 回调在 setText 返回前就跑完了)。
- * 如果桩里不触发,那些「程序化回写会自触发」的坑就测不出来,
- * 界面会在真机上陷入「回写 → 触发监听 → 再回写」的循环。
- */
- class MockTextView extends MockView {
- constructor() {
- super();
- this.text = '';
- this.textSize = 0;
- this.textColor = null;
- this.typeface = null;
- this.singleLine = false;
- this.ellipsize = null;
- this.hint = '';
- this.hintColor = null;
- this.maxWidth = 0;
- this.watchers = [];
- this.selection = 0;
- }
- setText(t) {
- const prev = this.text;
- this.text = String(t === null || t === undefined ? '' : t);
- this.notifyText(prev, this.text);
- }
- getText() { return this.text; }
- addTextChangedListener(w) { this.watchers.push(w); }
- removeTextChangedListener(w) { this.watchers = this.watchers.filter((x) => x !== w); }
- notifyText(prev, next) {
- this.watchers.forEach((w) => {
- if (w.beforeTextChanged) { w.beforeTextChanged(prev, 0, prev.length, next.length); }
- if (w.onTextChanged) { w.onTextChanged(next, 0, prev.length, next.length); }
- if (w.afterTextChanged) { w.afterTextChanged(next); }
- });
- }
- /** 测试辅助:模拟用户逐字输入(与 IME 走同一条通路) */
- type(str) {
- for (let i = 0; i < String(str).length; i++) {
- const prev = this.text;
- this.text = prev + String(str).charAt(i);
- this.notifyText(prev, this.text);
- }
- }
- /** 测试辅助:模拟连续退格清空 */
- eraseAll() {
- while (this.text.length > 0) {
- const prev = this.text;
- this.text = prev.substring(0, prev.length - 1);
- this.notifyText(prev, this.text);
- }
- }
- /** 测试辅助:退一格(从尾部删一个字符) */
- backspace() {
- if (this.text.length === 0) { return; }
- const prev = this.text;
- this.text = prev.substring(0, prev.length - 1);
- this.notifyText(prev, this.text);
- }
- /* 监听器在桩里存的是**回调表**(真机上也是个实现了接口的对象),
- * 所以统一通过方法名派发,别把回调表当函数直接调 */
- setOnFocusChangeListener(l) { this.focusListener = l; }
- notifyFocus(hasFocus) {
- const l = this.focusListener;
- if (!l) { return; }
- if (typeof l === 'function') { l(this, hasFocus); return; }
- if (typeof l.onFocusChange === 'function') { l.onFocusChange(this, hasFocus); }
- }
- gainFocus() { this.focused = true; this.notifyFocus(true); }
- loseFocus() { this.focused = false; this.notifyFocus(false); }
- setTextSize(s) { this.textSize = s; }
- setTextColor(c) { this.textColor = c; }
- setTypeface(t) { this.typeface = t; }
- setSingleLine(v) { this.singleLine = v; }
- setEllipsize(e) { this.ellipsize = e; }
- setHint(h) { this.hint = h; }
- setHintTextColor(c) { this.hintColor = c; }
- setMaxWidth(w) { this.maxWidth = w; }
- setMaxLines() { }
- setLineSpacing(add, mult) { this.lineSpacing = { add, mult }; }
- setSelection(i) { this.selection = i; }
- setSelectAllOnFocus(v) { this.selectAllOnFocus = v; }
- setImeOptions(o) { this.imeOptions = o; }
- }
- class MockEditText extends MockTextView {
- setInputType(t) { this.inputType = t; }
- }
- class MockScrollView extends MockView {
- setVerticalScrollBarEnabled() { }
- setOverScrollMode() { }
- }
- class MockLinearLayout extends MockView {
- setOrientation(o) { this.orientation = o; }
- }
- class MockFrameLayout extends MockView { }
- /** WindowManager.LayoutParams 桩:悬浮球用的是裸 WindowManager 这条路 */
- class MockWMLayoutParams {
- constructor() {
- this.type = 0;
- this.format = 0;
- this.flags = 0;
- this.gravity = 0;
- this.x = 0;
- this.y = 0;
- this.width = 0;
- this.height = 0;
- }
- }
- class MockImageView extends MockView {
- setImageBitmap(b) { this.bitmap = b; }
- setImageDrawable(d) { this.drawable = d; }
- setScaleType(s) { this.scaleType = s; }
- }
- MockImageView.ScaleType = { FIT_XY: 'FIT_XY', CENTER_CROP: 'CENTER_CROP', FIT_CENTER: 'FIT_CENTER' };
- /**
- * MotionEvent 桩。
- *
- * 悬浮球靠 getAction()/getRawX()/getRawY() 判断「单击 vs 拖动 vs 取消」,
- * 而这三者的区分是本模块最容易出错的地方(ACTION_CANCEL 曾经被当成 UP 处理,
- * 导致"每次展开立刻又收起")。所以它必须能被离线构造出来,否则那段逻辑永远测不到。
- */
- class MockMotionEvent {
- constructor(action, rawX, rawY) {
- this.action = action;
- this.rawX = rawX;
- this.rawY = rawY;
- }
- getAction() { return this.action; }
- getRawX() { return this.rawX; }
- getRawY() { return this.rawY; }
- getX() { return this.rawX; }
- getY() { return this.rawY; }
- }
- MockMotionEvent.ACTION_DOWN = 0;
- MockMotionEvent.ACTION_UP = 1;
- MockMotionEvent.ACTION_MOVE = 2;
- MockMotionEvent.ACTION_CANCEL = 3;
- /**
- * floaty 窗口桩。
- *
- * 真机上 `floaty.window(xml)` 返回一个已经挂在 WindowManager 上的窗口对象,
- * `findView(id)` 从它膨胀出来的视图树里取视图。桩只需要两件事:
- * · `findView()` 给回一个可用的 FrameLayout(产物只在里面 add/remove 子视图)
- * · 记录 setSize/setPosition 的入参 —— 悬浮球的坐标公式算错过两次(dp/px 混用、
- * 漏算根视图内边距),有了这份记录,离线就能断言"球应该落在哪"。
- */
- class MockFloatyWindow {
- constructor() {
- this.root = new MockFrameLayout();
- this.width = 0;
- this.height = 0;
- this.x = 0;
- this.y = 0;
- this.closed = false;
- this.focusDisabled = false;
- this.adjustEnabled = true;
- }
- findView() { return this.root; }
- setSize(w, h) { this.width = w; this.height = h; return this; }
- setPosition(x, y) { this.x = x; this.y = y; return this; }
- setAdjustEnabled(v) { this.adjustEnabled = !!v; return this; }
- disableFocus() { this.focusDisabled = true; return this; }
- close() { this.closed = true; return this; }
- getWidth() { return this.width; }
- getHeight() { return this.height; }
- getX() { return this.x; }
- getY() { return this.y; }
- }
- MockLinearLayout.LayoutParams = MockLayoutParams;
- MockFrameLayout.LayoutParams = MockLayoutParams;
- class MockGradientDrawable {
- constructor() {
- this.colors = null;
- this.color = null;
- this.radius = 0;
- this.stroke = null;
- }
- setShape(s) { this.shape = s; }
- setColor(c) { this.color = c; }
- setCornerRadius(r) { this.radius = r; }
- setStroke(w, c) { this.stroke = { width: w, color: c }; }
- setOrientation(o) { this.orientation = o; }
- setColors(list) { this.colors = list; }
- setGradientType(t) { this.gradientType = t; }
- setGradientRadius(r) { this.gradientRadius = r; }
- /* 登录按钮的「校验中」态靠整体降透明度表达 */
- setAlpha(a) { this.alpha = a; }
- }
- /* GradientDrawable 的静态常量:产物在代码里设置形状与渐变方向时会读它们 */
- MockGradientDrawable.RECTANGLE = 0;
- MockGradientDrawable.OVAL = 1;
- MockGradientDrawable.LINE = 2;
- MockGradientDrawable.LINEAR_GRADIENT = 0;
- MockGradientDrawable.RADIAL_GRADIENT = 1;
- MockGradientDrawable.SWEEP_GRADIENT = 2;
- MockGradientDrawable.Orientation = {
- LEFT_RIGHT: 0, RIGHT_LEFT: 1, TOP_BOTTOM: 2, BOTTOM_TOP: 3, TL_BR: 4, BL_TR: 5
- };
- class MockIntent {
- constructor(action, uri) { this.action = action; this.uri = uri || null; this.extras = {}; }
- setData(uri) { this.uri = uri; return this; }
- putExtra(k, v) { this.extras[k] = v; return this; }
- addFlags() { return this; }
- }
- /* 真机上的值,桩里保持同一个数字:断言能对得上,
- * 也不会因为桩漏了常量而静默变成 undefined(那会让"拉回前台"悄悄失效)。 */
- MockIntent.FLAG_ACTIVITY_REORDER_TO_FRONT = 0x20000000;
- class MockUri {
- static parse(s) { return { raw: s }; }
- }
- class MockAdapterListener {
- constructor(obj) { Object.assign(this, obj || {}); }
- }
- /**
- * Java 接口桩:真机上 `new Interface({...})` 走的是 Rhino 的 JavaAdapter,
- * 拿到的是一个实现了该接口的对象。桩里直接把回调表原样返回 ——
- * 只要「回调能被存下来并在对的时机被调用」,被测代码的行为就与真机一致。
- */
- MockView.OnFocusChangeListener = function (handlers) { return handlers; };
- MockView.OnClickListener = function (handlers) { return handlers; };
- MockView.OnKeyListener = function (handlers) { return handlers; };
- /* 悬浮球的球体靠它收手势(单击 / 拖动 / 取消),必须能构造 */
- MockView.OnTouchListener = function (handlers) { return handlers; };
- MockView.OnLayoutChangeListener = function (handlers) { return handlers; };
- MockView.OnPreDrawListener = function (handlers) { return handlers; };
- /* ------------------------------------------------------------ 状态容器 */
- function install() {
- const android = {
- graphics: {
- drawable: { GradientDrawable: MockGradientDrawable },
- Typeface: { DEFAULT_BOLD: 'BOLD', MONOSPACE: 'MONO', DEFAULT: 'DEFAULT' },
- PixelFormat: { OPAQUE: -1 }
- },
- widget: {
- TextView: MockTextView,
- EditText: MockEditText,
- ScrollView: MockScrollView,
- LinearLayout: MockLinearLayout,
- FrameLayout: MockFrameLayout,
- ImageView: MockImageView,
- AdapterView: { OnItemSelectedListener: MockAdapterListener }
- },
- view: {
- View: MockView,
- Gravity: { CENTER: 17, CENTER_HORIZONTAL: 1, CENTER_VERTICAL: 16, TOP: 48, LEFT: 3, RIGHT: 5, END: 8388613 },
- KeyEvent: { KEYCODE_BACK: 4 },
- MotionEvent: MockMotionEvent,
- /* 注意必须是 function 而不是箭头函数:产物里是 `new android.view.ViewTreeObserver.OnPreDrawListener({...})` */
- ViewTreeObserver: { OnPreDrawListener: function (h) { return h; } },
- WindowManager: { LayoutParams: MockWMLayoutParams },
- inputmethod: { EditorInfo: { IME_ACTION_DONE: 6, IME_ACTION_NEXT: 5 } }
- },
- text: {
- TextUtils: { TruncateAt: { START: 'START', END: 'END', MIDDLE: 'MIDDLE' } },
- InputType: { TYPE_CLASS_TEXT: 1, TYPE_CLASS_NUMBER: 2, TYPE_NUMBER_FLAG_DECIMAL: 8192 },
- TextWatcher: function (handlers) { return handlers; }
- },
- content: {
- Intent: MockIntent,
- Context: { MODE_PRIVATE: 0 },
- pm: { PackageManager: { PERMISSION_GRANTED: 0, PERMISSION_DENIED: -1 } }
- },
- net: { Uri: MockUri },
- os: { Build: { MODEL: 'SmokeDevice', VERSION: { RELEASE: '14', SDK: '34' } } }
- };
- const memoryStorage = {};
- const memoryFiles = {};
- const mounted = { root: null };
- const logLines = [];
- /* 计时器:
- * · setTimeout 立即执行 —— 登录流程用它留一帧给「登录中…」,
- * 自检里没有理由真的等 450ms;
- * · setInterval **不自动跑**,只登记句柄(自动跑就是死循环)。
- * 需要验证计时逻辑时由测试调用 flushIntervals() 手动推进。 */
- const intervals = new Map();
- let intervalSeq = 0;
- const wmWindows = [];
- const wmStub = {
- addView: (view, lp) => { view.setLayoutParams(lp); wmWindows.push({ view: view, lp: lp }); },
- updateViewLayout: (view, lp) => { view.setLayoutParams(lp); },
- removeView: (view) => {
- const i = wmWindows.findIndex((w) => w.view === view);
- if (i >= 0) { wmWindows.splice(i, 1); }
- }
- };
- const context = {
- getResources: () => ({
- getDisplayMetrics: () => ({ density: 2.75 }),
- getIdentifier: () => 0,
- getDimensionPixelSize: () => 66
- }),
- getPackageName: () => 'org.autojs.autoxjs.v7',
- getPackageManager: () => ({
- getPackageInfo: (name) => {
- /* 让 TikTok 主包与 AutoX 自身"存在",
- * 既验证 device.js 的多包探测与回退,也覆盖 appVersionName 的成功路径 */
- if (name === 'com.zhiliaoapp.musically') { return { versionName: '39.5.4' }; }
- if (name === 'org.autojs.autoxjs.v7') { return { versionName: '9.3.11' }; }
- throw new Error('NameNotFoundException: ' + name);
- }
- }),
- /* 悬浮球走裸 WindowManager(不用 floaty),桩里给它一个可观测的实现:
- * 记录加过哪些窗口、它们的 LayoutParams,方便断言「两个窗口、尺寸恒定、触摸开关」。 */
- getSystemService: (name) => (name === 'window' ? wmStub : null),
- /* 通知权限桩成「未授予」:Android 13+ 才走这条分支,
- * 而这正是「通知标可选」那段逻辑唯一的触发条件 */
- checkSelfPermission: () => -1,
- startActivity: () => { }
- };
- const g = globalThis;
- g.android = android;
- g.context = context;
- g.device = { width: 1080, height: 1920 };
- g.auto = { service: null };
- /* 悬浮球的三件套:floaty(开窗)/ images(品牌图)/ MotionEvent(手势)。
- * 缺任意一个,悬浮球在离线自检里都会"静默不建"(产物把创建包在 try 里),
- * 于是整块功能零覆盖 —— 而这正是「扇形按钮点了没反应」溜到真机的原因。 */
- const floatyState = { windows: [] };
- g.floaty = {
- checkPermission: () => true,
- window: () => {
- const w = new MockFloatyWindow();
- floatyState.windows.push(w);
- return w;
- },
- closeAll: () => { }
- };
- g.images = {
- read: () => ({ bitmap: { stub: true } }),
- release: () => { }
- };
- g.sleep = () => { };
- g.toast = () => { };
- g.app = {
- viewFile: () => { },
- /* 「拉回前台」靠它。桩里记下来,自检据此断言确实按 REORDER_TO_FRONT 发的 Intent。 */
- startActivity: (intent) => { g.app.lastIntent = intent; },
- /** 「切回 TikTok」靠它 */
- launchPackage: (name) => { g.app.lastLaunchedPackage = name; }
- };
- /* UI 模式下当前 Activity(拉回前台时用它取类名) */
- g.activity = {
- getClass: () => ({ getName: () => 'org.autojs.autojs.ui.script.ScriptExecuteActivity' })
- };
- g.threads = { start: (fn) => { fn(); }, shutDownAll: () => { } };
- g.setTimeout = (fn) => { fn(); return 0; };
- g.setInterval = (fn) => {
- intervalSeq += 1;
- intervals.set(intervalSeq, fn);
- return intervalSeq;
- };
- g.clearInterval = (handle) => { intervals.delete(handle); };
- /* clearTimeout 必须有:状态切换靠"等布局落地 + 兜底定时器",
- * 落地时要把兜底那个清掉(真机上是真清,桩里立即执行所以本来就是空操作) */
- g.clearTimeout = () => { };
- /* 事件登记表:自检可以手动触发 "resume" 之类的生命周期事件 */
- const uiEvents = {};
- g.ui = {
- statusBarColor: () => { },
- setContentView: (v) => { mounted.root = v; },
- run: (fn) => fn(),
- post: (fn) => fn(),
- emitter: {
- on: (name, fn) => { uiEvents[name] = fn; },
- emit: (name, payload) => { if (uiEvents[name]) { uiEvents[name](payload); } }
- }
- };
- g.storages = {
- create: (name) => ({
- get: (k, d) => {
- const bucket = memoryStorage[name] || {};
- return Object.prototype.hasOwnProperty.call(bucket, k) ? bucket[k] : d;
- },
- put: (k, v) => {
- memoryStorage[name] = memoryStorage[name] || {};
- memoryStorage[name][k] = v;
- },
- remove: (k) => {
- if (memoryStorage[name]) { delete memoryStorage[name][k]; }
- }
- })
- };
- /* 文件读写走内存,绝不碰真实磁盘——自检不应该有副作用 */
- g.files = {
- write: (p, c) => { memoryFiles[p] = String(c); },
- read: (p) => (Object.prototype.hasOwnProperty.call(memoryFiles, p) ? memoryFiles[p] : ''),
- exists: (p) => Object.prototype.hasOwnProperty.call(memoryFiles, p),
- listDir: () => [],
- isDir: () => false,
- ensureDir: () => { }
- };
- /* ------------------------------------------------------------ 引擎桩
- *
- * 入口脚本与产物路径固定下来,让 Engine.locate() 稳定命中。
- * 路径刻意做成「项目目录 + 相对位置」,与真机的项目模式一致 ——
- * 于是"从 myEngine().source 推出同级目录"这条推导逻辑也被测到,
- * 而不是只测"某个写死的路径存在"。
- */
- const ENTRY_SOURCE = '/sdcard/Scripts/demo/main.js';
- const ENGINE_SCRIPT = '/sdcard/Scripts/demo/tiktok-auto/dist/tiktok-auto.js';
- memoryFiles[ENGINE_SCRIPT] = '/* 引擎产物(桩) */';
- const engineState = {
- /** execScriptFile 的调用记录 —— 自检据此断言参数传对了 */
- spawned: [],
- /** 除宿主之外的引擎。清空即模拟「子引擎已退出」 */
- children: [{ id: 'child-1' }]
- };
- g.engines = {
- myEngine: () => ({ id: 'host-1', source: ENTRY_SOURCE, execArgv: {} }),
- all: () => [{ id: 'host-1' }].concat(engineState.children),
- execScriptFile: (scriptPath, config) => {
- engineState.spawned.push({ path: scriptPath, config: config });
- return { getEngine: () => null };
- }
- };
- g.dialogs = {
- select: () => -1,
- input: () => null,
- confirm: () => false,
- alert: () => { }
- };
- /* 桩原样转发 console.log,但留一份副本给自检断言用 */
- const originalLog = console.log;
- g.console = Object.assign({}, console, {
- log: (...args) => {
- logLines.push(args.join(' '));
- originalLog(...args);
- }
- });
- return {
- mounted,
- memoryStorage,
- memoryFiles,
- logLines,
- /** 手动触发 UI 生命周期事件(如 "resume") */
- emitter: g.ui.emitter,
- /** 已登记的事件名 → 回调,用于断言"确实注册了" */
- uiEvents,
- /** 引擎桩的可变状态:调用记录 + 子引擎列表(清空 children 即模拟引擎退出) */
- engineState,
- ENTRY_SOURCE,
- ENGINE_SCRIPT,
- /** 已登记的 interval 回调数量(跑完一次运行后应为 1,停止后应为 0)。 */
- intervalCount: () => intervals.size,
- /** 手动推进一次所有 interval,用于验证运行计时。 */
- flushIntervals: () => { intervals.forEach((fn) => fn()); },
- /** 已加到 WindowManager 上的窗口(悬浮球现在是两个 overlay 窗口)。 */
- wmWindows: () => wmWindows,
- /** 已创建的 floaty 窗口(历史遗留:浮窗早已不用 floaty)。 */
- floatyWindows: () => floatyState.windows,
- /** 造一个触摸事件(走产物真实的 OnTouchListener 通路)。 */
- touchEvent: (action, x, y) => new MockMotionEvent(action, x, y),
- MotionEvent: MockMotionEvent
- };
- }
- module.exports = { install };
|