Files
lucky_shop/tests/splash-state.test.js
ZF sun 45c0c2afb1 refactor(鸿蒙元服务): 适配鸿蒙平台隐私托管与启动屏时序优化
1. 适配鸿蒙元服务平台隐私托管,由系统自动弹出隐私弹窗,应用层不再弹出自建弹窗
2. 新增启动屏strict模式,鸿蒙场景下严格等待首页DIY渲染完成再关闭启动屏,避免白屏
3. 新增splash-state单元测试覆盖新旧模式与鸿蒙场景
4. 清理隐私相关冗余代码与注释,统一平台逻辑分支
2026-07-24 15:10:49 +08:00

320 lines
13 KiB
JavaScript

/**
* splash-state.js 单元测试 — 启动屏时序控制
*
* 跑法:
* node --test tests/splash-state.test.js
*
* 测试覆盖:
* - 默认 boot-complete 软隐藏 (MOUNT_GRACE_MS=1s + PAGE_READY_GRACE_MS=2s 兜底)
* - strict 模式: 不走 1+2s 软隐藏兜底, 只等 notifyMounted + notifyPageReady
* (鸿蒙端已接入平台托管, splash 必须真等到首页 diy 渲染好)
* - DEFAULT_TIMEOUT=8s 总超时兜底仍然生效
* - 重复 hide() 防重入
*
* 实现策略:
* splash-state.js 用了 ESM export, vm 不能直接跑.
* 用一段包装代码: 把 export 转成 module.exports, 提取 SplashState 类在沙箱里实例化.
*
* 业务背景:
* 鸿蒙元服务已接入平台隐私托管, 鸿蒙系统自动弹出系统级隐私弹窗.
* 应用层 splash 不能"提前"关闭 (否则出现 splash 关闭 → diy 数据还没到 → 白屏 时段).
* strict 模式确保 splash 真等到 notifyPageReady (首页 diy 渲染完毕回调).
*/
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const SPLASH_SRC = fs.readFileSync(
path.join(__dirname, '..', 'common', 'js', 'splash-state.js'),
'utf8',
);
/**
* 在沙箱里加载 splash-state.js, 提取 SplashState 类
* 关键: 要 mock uni 全局 API, 否则沙箱里会抛 ReferenceError
*/
function loadSplashStateInSandbox() {
const moduleObj = { exports: {} };
// 包装: export default 改为 module.exports
const wrapped = SPLASH_SRC
.replace(/^import\s+.*$/gm, '// (import)')
.replace(/^export\s+default\s+splashState;?$/gm, '')
.replace(/^export\s+\{[\s\S]*?\};?$/gm, '')
+ '\nmodule.exports = { SplashState, splashState };\n'
const sandbox = {
console,
globalThis: {},
module: moduleObj,
exports: moduleObj.exports,
// mock uni 全局 (splash-state 内部判断 needSplash / useNativeLoading)
uni: {
getStorageSync: () => null,
setStorageSync: () => {},
removeStorageSync: () => {},
showLoading: () => {},
hideLoading: () => {},
},
// mock setTimeout/clearTimeout (splash-state 内部用)
setTimeout,
clearTimeout,
setInterval,
clearInterval,
// mock 平台: MP-HARMONY
// (splash-state 内部 typeof uni !== 'undefined' && uni.getStorageSync 检查)
}
sandbox.globalThis = sandbox
vm.createContext(sandbox)
vm.runInContext(wrapped, sandbox, { filename: 'splash-state.js' })
return sandbox.module.exports
}
/**
* 构造一个 SplashState 实例, 跳过 show() 阶段
* (测试 hide/notify 流程, 不需要真实显示 splash)
*/
function makeSplashState() {
const { SplashState } = loadSplashStateInSandbox()
if (typeof SplashState !== 'function') {
throw new Error('SplashState class not exported')
}
const s = new SplashState()
// 直接设置内部状态模拟 splash 已显示
s._visible = true
s._shownAt = Date.now()
s._stageSet = new Set(['boot'])
s._pendingProgress = null
s._pendingHide = null
// 清除 _timeoutId 等
return s
}
// =============================================================
// 1. 默认 boot-complete 软隐藏 (现有行为)
// =============================================================
test('默认: boot-complete 走软隐藏, MOUNT_GRACE_MS=1s + PAGE_READY_GRACE_MS=2s 兜底', () => {
const s = makeSplashState()
const events = []
// 监听 emit
const origEmit = s._emit.bind(s)
s._emit = (event, payload) => {
events.push({ event, payload })
origEmit(event, payload)
}
s.hide({ reason: 'boot-complete', mode: 'fast' })
// 应该标记 _pendingHide, 但还没真关
assert.ok(s._pendingHide, '应该设置 _pendingHide')
assert.equal(s._visible, true, '软隐藏时仍 visible=true')
// 应该设置 mountGraceId (1s 兜底)
assert.ok(s._mountGraceId, '默认模式应该设置 mountGraceId')
// 不应该走 strict 路径
})
// =============================================================
// 2. strict 模式: 鸿蒙端, 不走 MOUNT_GRACE_MS 兜底
// =============================================================
test('strict 模式: mode="strict" 不设置 mountGraceId (1s 兜底跳过)', () => {
const s = makeSplashState()
s.hide({ reason: 'boot-complete', mode: 'strict' })
assert.ok(s._pendingHide, '应该设置 _pendingHide')
assert.equal(s._visible, true, '软隐藏时仍 visible=true')
assert.equal(s._mountGraceId, null, 'strict 模式不应该设置 mountGraceId')
})
test('strict 模式: _strict: true 兼容形式也支持 (App.vue 当前用法)', () => {
const s = makeSplashState()
// 鸿蒙调用方: hide({ reason: 'boot-complete', mode: 'strict' })
// 验证 mode='strict' 即触发 strict 路径
s.hide({ reason: 'boot-complete', mode: 'strict' })
assert.equal(s._mountGraceId, null, '_strict 兼容模式正确识别')
})
// =============================================================
// 3. strict 模式真等 notifyPageReady 不超时
// =============================================================
test('strict 模式: 没调 notifyPageReady 时, _flushPendingHide 不设 pageReadyGraceId', () => {
const s = makeSplashState()
s.hide({ reason: 'boot-complete', mode: 'strict' })
// 模拟组件 mount
s.notifyMounted()
// 此时 _pageReady=false, _softHide 后会调 _flushPendingHide
// strict 模式: 不设 pageReadyGraceId
assert.equal(s._pageReadyGraceId, null,
'strict 模式: 页面未就绪时, 不设置 pageReadyGraceId (1+2s 兜底跳过)')
})
test('strict 模式: 调 notifyPageReady 立即真关', () => {
const s = makeSplashState()
const events = []
const origEmit = s._emit.bind(s)
s._emit = (event, payload) => {
events.push({ event, payload })
origEmit(event, payload)
}
s.hide({ reason: 'boot-complete', mode: 'strict' })
s.notifyMounted() // 组件 mount
s.notifyPageReady() // 页面就绪
// _pageReady=true, _pendingHide 还在 → 应该真关
// notifyPageReady 内部 setTimeout 50ms 后调 _flushPendingHide
return new Promise((resolve) => {
setTimeout(() => {
assert.equal(s._visible, false, 'notifyPageReady 后 splash 应真关')
const hideEvent = events.find(e => e.event === 'splash:hide')
assert.ok(hideEvent, '应该发 splash:hide 事件')
resolve()
}, 100)
})
})
// =============================================================
// 4. 默认模式: 仍然有 pageReadyGraceId 兜底 (保持现有行为)
// =============================================================
test('默认模式: _flushPendingHide 调 _pageReadyGraceId (2s 兜底, 向后兼容)', async () => {
const s = makeSplashState()
s.hide({ reason: 'boot-complete', mode: 'fast' })
s.notifyMounted() // mount 后, 50ms 后 _flushPendingHide 被调
// 给 100ms 让 microtask 跑完
await new Promise(resolve => setTimeout(resolve, 100))
// 默认模式: 应该设置 pageReadyGraceId
assert.ok(s._pageReadyGraceId,
'默认模式: 页面未就绪时, 应该设置 pageReadyGraceId (2s 兜底)')
})
// =============================================================
// 5. DEFAULT_TIMEOUT=8s 总超时仍然兜底
// =============================================================
test('DEFAULT_TIMEOUT: 总超时由 show() 设置的 _timeoutId 兜底, strict 模式也走这个', () => {
const s = makeSplashState()
// 模拟 show() 后 _timeoutId 被设置
s._timeoutId = setTimeout(() => {}, 1000)
s.hide({ reason: 'boot-complete', mode: 'strict' })
// strict 模式: 不取消 _timeoutId (总超时仍然兜底)
// _timeoutId 仍然存在
assert.ok(s._timeoutId, 'strict 模式不取消 _timeoutId, 总超时兜底保留')
clearTimeout(s._timeoutId)
})
// =============================================================
// 6. 重复 hide() 防重入
// =============================================================
test('hide() 重复调用: boot-complete reason 走软隐藏, _pendingHide 保持', () => {
const s = makeSplashState()
s.hide({ reason: 'boot-complete', mode: 'fast' })
const firstPending = s._pendingHide
assert.ok(firstPending, '第一次 hide 后 _pendingHide 有值')
assert.equal(firstPending.reason, 'boot-complete')
// 第二次同 reason 调用: 仍走软隐藏, _pendingHide 被重置为最新 opts
s.hide({ reason: 'boot-complete', mode: 'fast' })
assert.ok(s._pendingHide, '第二次 hide (boot-complete) 后 _pendingHide 仍有值')
assert.equal(s._pendingHide.reason, 'boot-complete', 'reason 仍 boot-complete')
})
test('hide() 重复调用: 非 boot-complete reason 走 _doHide 强关 (设计如此)', () => {
const s = makeSplashState()
s.hide({ reason: 'boot-complete', mode: 'fast' })
assert.ok(s._pendingHide, 'boot-complete 走软隐藏')
// 非 boot-complete reason 走强隐藏分支
s.hide({ reason: 'manual', mode: 'fast' })
// 强关后 _pendingHide=null, _visible=false
assert.equal(s._pendingHide, null, 'manual reason 强关后 _pendingHide 被清')
assert.equal(s._visible, false, 'manual reason 强关后 _visible=false')
})
test('hide() 重复调用: visible=false 后是 noop', () => {
const s = makeSplashState()
s.hide({ reason: 'boot-complete', mode: 'fast' })
// 强制 hide
s._doHide({ reason: 'forced' })
assert.equal(s._visible, false)
const pendingBefore = s._pendingHide
s.hide({ reason: 'manual', mode: 'fast' }) // visible=false, 应该 noop
// 强制 hide 后 _pendingHide 被清 (看 _doHide: this._pendingHide = null)
// 再次 hide 是 noop, _pendingHide 仍是 null
assert.equal(s._pendingHide, pendingBefore, 'visible=false 后 hide 是 noop')
})
// =============================================================
// 7. 业务场景: 鸿蒙完整时序模拟
// =============================================================
test('鸿蒙场景: 模拟 onLaunch → boot 完成 → splash hide(strict) → mount → diy 加载 → notifyPageReady', () => {
const s = makeSplashState()
const events = []
const origEmit = s._emit.bind(s)
s._emit = (event, payload) => {
events.push({ event, payload })
origEmit(event, payload)
}
// t=0: onLaunch 启动, splash 已显示
// t=2s: boot 完成, App.vue 调 hide({ mode: 'strict' })
s.hide({ reason: 'boot-complete', mode: 'strict' })
assert.equal(s._visible, true, 'hide 后仍 visible (软隐藏)')
assert.equal(s._mountGraceId, null, 'strict 不设 mountGraceId')
assert.equal(s._pageReadyGraceId, null, 'strict 不设 pageReadyGraceId')
// t=2.5s: Vue 首页 mount
s.notifyMounted()
// strict 模式: 不设 pageReadyGraceId, 不会"提前"关
assert.equal(s._pageReadyGraceId, null, 'strict: mount 后仍不设 pageReadyGraceId')
// t=4s: diy-group 渲染完, 调 notifyPageReady
s.notifyPageReady()
// setTimeout 50ms 后 _flushPendingHide
return new Promise((resolve) => {
setTimeout(() => {
// 此时应该真关
assert.equal(s._visible, false, 'notifyPageReady 后 splash 真关')
const hideEvent = events.find(e => e.event === 'splash:hide')
assert.ok(hideEvent, '发 splash:hide 事件')
assert.equal(hideEvent.payload.reason, 'boot-complete', 'reason 应是 boot-complete')
// 总耗时 4s, splash 一直显示 (没有"提前"关)
resolve()
}, 100)
})
})
test('鸿蒙场景对比: 默认模式会"提前"关 splash (演示 bug)', () => {
const s = makeSplashState()
const events = []
const origEmit = s._emit.bind(s)
s._emit = (event, payload) => {
events.push({ event, payload })
origEmit(event, payload)
}
// t=0: hide({ mode: 'fast' }) 默认模式
s.hide({ reason: 'boot-complete', mode: 'fast' })
// 1s 内没 mount, 兜底: 设置 mountGraceId
assert.ok(s._mountGraceId, '默认模式: 1s 内没 mount 兜底已设置 (这是 bug 来源)')
// 模拟 mount 在 1s 之前发生 (用 setTimeout 加速)
// 实际场景: t=0.5s 时 Vue mount 完成
return new Promise((resolve) => {
setTimeout(() => {
// t=0.5s: 模拟 mount (此时 1s 兜底还没到, 但我们立即调 notifyMounted 模拟)
s.notifyMounted()
// notifyMounted 内部 setTimeout 50ms 后调 _flushPendingHide
setTimeout(() => {
// 此时 _pageReady=false, 默认模式会设 _pageReadyGraceId
assert.ok(s._pageReadyGraceId,
'默认模式会设 pageReadyGraceId (2s 兜底) - 这是 bug: 1+2=3s 后强关')
resolve()
}, 100)
}, 50)
})
})