1. 适配鸿蒙元服务平台隐私托管,由系统自动弹出隐私弹窗,应用层不再弹出自建弹窗 2. 新增启动屏strict模式,鸿蒙场景下严格等待首页DIY渲染完成再关闭启动屏,避免白屏 3. 新增splash-state单元测试覆盖新旧模式与鸿蒙场景 4. 清理隐私相关冗余代码与注释,统一平台逻辑分支
320 lines
13 KiB
JavaScript
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)
|
|
})
|
|
}) |