refactor: 确立独立子工程架构 (backend, admin, mpuniapp) 并同步更新 AGENTS.md 规范

这个提交包含在:
wikiboss
2026-09-20 17:05:22 +08:00
父节点 de3cf8b870
当前提交 9fa1d1fbd0
共有 1679 个文件被更改,包括 204798 次插入 和 16817 次删除

查看文件

@@ -0,0 +1,334 @@
jest.setTimeout(30000)
const platformInfo = process.env.uniTestPlatformInfo.toLocaleLowerCase()
const isIOS = platformInfo.startsWith('ios')
const isMP = platformInfo.startsWith('mp')
describe('ExtApi-StorageInfoTest', () => {
if ( isMP) {
it('skip', async () => {
expect(1).toBe(1);
});
return;
}
if (
isIOS &&
(
platformInfo.indexOf('15.5') != -1 ||
platformInfo.indexOf('14.5') != -1 ||
platformInfo.indexOf('13.7') != -1 ||
platformInfo.indexOf('12.4') != -1
)
) {
// TODO: 排查 ios 不兼容版本 测试异常原因
it('ios 15.5 14.5 13.7 12.4 测试异常', () => {
expect(1).toBe(1)
})
return
}
const StorageKeysBlackList = [
'$$STAT__DBDATA:__UNI__HelloUniAppX',
'_STAT_LAST_PAGE_ROUTE',
'UNI_STAT_DATA:__UNI__HelloUniAppX',
'UNI_STAT_LAST_PAGE_ROUTE'
]
const filterStorageKeys = (keys) => {
return keys.filter(key => {
return !StorageKeysBlackList.includes(key)
})
}
const PAGE_PATH = '/pages/API/storage/storage'
let page;
// 测试辅助函数
async function setPageData(newData) {
return await page.setData({ data: newData });
}
function getData (key = '') {
return new Promise(async (resolve, reject) => {
const data = await page.data('data')
resolve(key ? data[key] : data)
})
}
beforeAll(async () => {
page = await program.reLaunch(PAGE_PATH)
await page.waitFor(600);
});
it('Check async properties', async () => {
// 异步存储测试
await setPageData({
key: "autotest_key_mock",
data: "长安大道连狭斜,青牛白马七香车。玉辇纵横过主第,金鞭络绎向侯家。龙衔宝盖承朝日,凤吐流苏带晚霞。百尺游丝争绕树,一群娇鸟共啼花。游蜂戏蝶千门侧,碧树银台万种色。复道交窗作合欢,双阙连甍垂凤翼。"
})
await page.waitFor(600)
const btnSetStorageButtonInfo = await page.$('.btn-setstorageAsync')
await btnSetStorageButtonInfo.tap()
await page.waitFor(600)
const btnGetStorageButtonInfo = await page.$('.btn-getstorageAsync')
await btnGetStorageButtonInfo.tap()
await page.waitFor(600)
expect(await getData('apiGetData')).toEqual("长安大道连狭斜,青牛白马七香车。玉辇纵横过主第,金鞭络绎向侯家。龙衔宝盖承朝日,凤吐流苏带晚霞。百尺游丝争绕树,一群娇鸟共啼花。游蜂戏蝶千门侧,碧树银台万种色。复道交窗作合欢,双阙连甍垂凤翼。")
// 测试 clear
let btnGetStorageInfoASyncButton = await page.$('.btn-getStorageInfoASync')
// await btnGetStorageInfoASyncButton.tap()
// await page.waitFor(600)
// storageInfoRet = await getData('apiGetData')
// expect(storageInfoRet.keys[0]).toEqual("autotest_key_mock")
const btnClearStorageInfoASyncButton = await page.$('.btn-clearStorageInfoASync')
await btnClearStorageInfoASyncButton.tap()
page.waitFor(600)
await btnGetStorageInfoASyncButton.tap()
await page.waitFor(600)
storageInfoRet = await getData('apiGetData')
expect(filterStorageKeys(storageInfoRet.keys).length).toEqual(0)
await setPageData({
key: "autotest_key_mock",
data: 1100.8989
})
await page.waitFor(600)
await btnSetStorageButtonInfo.tap()
await page.waitFor(600)
await btnGetStorageButtonInfo.tap()
await page.waitFor(600)
expect(await getData('apiGetData')).toEqual(1100.8989)
// 测试 remove
// await btnGetStorageInfoASyncButton.tap()
// await page.waitFor(600)
// storageInfoRet = await getData('apiGetData')
// expect(storageInfoRet.keys[0]).toEqual("autotest_key_mock")
const btnRemoveStorageInfoASyncButton = await page.$('.btn-removeStorageInfoASync')
await btnRemoveStorageInfoASyncButton.tap()
page.waitFor(600)
await btnGetStorageInfoASyncButton.tap()
await page.waitFor(600)
storageInfoRet = await getData('apiGetData')
expect(filterStorageKeys(storageInfoRet.keys).length).toEqual(0)
await setPageData({
key: "autotest_key_mock",
data: 123456789
})
await page.waitFor(600)
await btnSetStorageButtonInfo.tap()
await page.waitFor(600)
await btnGetStorageButtonInfo.tap()
await page.waitFor(600)
expect(await getData('apiGetData')).toEqual(123456789)
let userObj = {
name: "zhangsan",
age: 12
}
await setPageData({
key: "autotest_key_mock",
data: userObj
})
await page.waitFor(600)
await btnSetStorageButtonInfo.tap()
await page.waitFor(600)
await btnGetStorageButtonInfo.tap()
await page.waitFor(600)
expect(await getData('apiGetData')).toEqual(userObj)
await setPageData({
key: "autotest_key_mock",
data: {
name: "zhangsan",
age: 122
}
})
await page.waitFor(600)
await btnSetStorageButtonInfo.tap()
await page.waitFor(600)
await btnGetStorageButtonInfo.tap()
await page.waitFor(600)
let objRet = await getData('apiGetData')
expect(objRet.age).toEqual(122)
});
it('Check sync properties', async () => {
let btnComplexStaticTest = await page.$('.btn-complexStaticTest')
await btnComplexStaticTest.tap()
await page.waitFor(600)
if (!isIOS) {
expect(await getData('staticComplexRet')).toEqual(true)
}
await setPageData({
key: "autotest_key_mock",
data: "长安大道连狭斜,青牛白马七香车。玉辇纵横过主第,金鞭络绎向侯家。龙衔宝盖承朝日,凤吐流苏带晚霞。百尺游丝争绕树,一群娇鸟共啼花。游蜂戏蝶千门侧,碧树银台万种色。复道交窗作合欢,双阙连甍垂凤翼。"
})
await page.waitFor(600)
let btnSetStorageButtonInfo = await page.$('.btn-setstorageSync')
await btnSetStorageButtonInfo.tap()
await page.waitFor(600)
let btnGetStorageButtonInfo = await page.$('.btn-getstorageSync')
await btnGetStorageButtonInfo.tap()
await page.waitFor(600)
expect(await getData('apiGetData')).toEqual("长安大道连狭斜,青牛白马七香车。玉辇纵横过主第,金鞭络绎向侯家。龙衔宝盖承朝日,凤吐流苏带晚霞。百尺游丝争绕树,一群娇鸟共啼花。游蜂戏蝶千门侧,碧树银台万种色。复道交窗作合欢,双阙连甍垂凤翼。")
// 测试clear
const btnGetStorageInfoSyncButton = await page.$('.btn-getStorageInfoSync')
// await btnGetStorageInfoSyncButton.tap()
// await page.waitFor(600)
// let storageInfoRet = await getData('apiGetData')
// expect(storageInfoRet.keys[0]).toEqual("autotest_key_mock")
const btnClearStorageInfoSyncButton = await page.$('.btn-clearStorageInfoSync')
await btnClearStorageInfoSyncButton.tap()
await btnGetStorageInfoSyncButton.tap()
await page.waitFor(600)
storageInfoRet = await getData('apiGetData')
expect(filterStorageKeys(storageInfoRet.keys).length).toEqual(0)
await setPageData({
key: "autotest_key_mock",
data: 12345789.235689
})
await page.waitFor(600)
btnSetStorageButtonInfo = await page.$('.btn-setstorageSync')
await btnSetStorageButtonInfo.tap()
await page.waitFor(600)
btnGetStorageButtonInfo = await page.$('.btn-getstorageSync')
await btnGetStorageButtonInfo.tap()
await page.waitFor(600)
expect(await getData('apiGetData')).toEqual(12345789.235689)
// 测试 remove
// await btnGetStorageInfoSyncButton.tap()
// await page.waitFor(600)
// storageInfoRet = await getData('apiGetData')
// expect(storageInfoRet.keys[0]).toEqual("autotest_key_mock")
const btnRemoveStorageInfoSyncButton = await page.$('.btn-removeStorageInfoSync')
await btnRemoveStorageInfoSyncButton.tap()
page.waitFor(600)
await btnGetStorageInfoSyncButton.tap()
await page.waitFor(600)
storageInfoRet = await getData('apiGetData')
expect(filterStorageKeys(storageInfoRet.keys).length).toEqual(0)
await setPageData({
key: "autotest_key_mock",
data: 0
})
await page.waitFor(600)
btnSetStorageButtonInfo = await page.$('.btn-setstorageSync')
await btnSetStorageButtonInfo.tap()
await page.waitFor(600)
btnGetStorageButtonInfo = await page.$('.btn-getstorageSync')
await btnGetStorageButtonInfo.tap()
await page.waitFor(600)
expect(await getData('apiGetData')).toEqual(0)
await setPageData({
key: "autotest_key_mock",
data: {
name: "tom",
age: 10
}
})
await page.waitFor(600)
btnSetStorageButtonInfo = await page.$('.btn-setstorageSync')
await btnSetStorageButtonInfo.tap()
await page.waitFor(600)
btnGetStorageButtonInfo = await page.$('.btn-getstorageSync')
await btnGetStorageButtonInfo.tap()
await page.waitFor(600)
let objRet = await getData('apiGetData')
expect(objRet.name).toEqual("tom")
await setPageData({
key: "autotest_key_mock",
data: JSON.stringify({
name: "james",
age: 12,
from: "american"
})
})
await page.waitFor(600)
btnSetStorageButtonInfo = await page.$('.btn-setstorageSync')
await btnSetStorageButtonInfo.tap()
await page.waitFor(600)
btnGetStorageButtonInfo = await page.$('.btn-getstorageSync')
await btnGetStorageButtonInfo.tap()
await page.waitFor(600)
let jsonStr = await getData('apiGetData')
// 顺序不能保证,验证长度和各个属性来区分
let parseObj = JSON.parse(jsonStr)
expect(jsonStr.length).toEqual(43)
expect(parseObj['age']).toEqual(12)
expect(parseObj['from']).toEqual('american')
expect(parseObj['name']).toEqual('james')
await setPageData({
key: "autotest_key_mock",
data: "1234567890"
})
await page.waitFor(600)
btnSetStorageButtonInfo = await page.$('.btn-setstorageSync')
await btnSetStorageButtonInfo.tap()
await page.waitFor(600)
btnGetStorageButtonInfo = await page.$('.btn-getstorageSync')
await btnGetStorageButtonInfo.tap()
await page.waitFor(600)
let strRet = await getData('apiGetData')
// 顺序不能保证,验证长度和各个属性来区分
expect(typeof strRet).toEqual("string")
expect(strRet).toEqual("1234567890")
await setPageData({
key: "autotest_key_mock",
data: "1234567.890"
})
await page.waitFor(600)
btnSetStorageButtonInfo = await page.$('.btn-setstorageSync')
await btnSetStorageButtonInfo.tap()
await page.waitFor(600)
btnGetStorageButtonInfo = await page.$('.btn-getstorageSync')
await btnGetStorageButtonInfo.tap()
await page.waitFor(600)
let strRet2 = await getData('apiGetData')
// 顺序不能保证,验证长度和各个属性来区分
expect(typeof strRet2).toEqual("string")
expect(strRet2).toEqual("1234567.890")
});
it('saveUTSJSONObject', async () => {
await page.callMethod('jest_saveUTSJSONObject')
await page.waitFor(600)
const {
jest_saveUTSJSONObjectSyncResult,
jest_saveUTSJSONObjectAsyncResult
} = await page.data('data')
expect(jest_saveUTSJSONObjectSyncResult).toBe(1)
expect(jest_saveUTSJSONObjectAsyncResult).toBe(1)
})
// ios js 层与原生层通信时,原始数据类型丢失
if (!isIOS) {
it('saveUTSJSONObjectArray', async () => {
await page.callMethod('jest_saveUTSJSONObjectArray')
await page.waitFor(600)
const {
jest_saveUTSJSONObjectArraySyncResult
} = await page.data('data')
expect(jest_saveUTSJSONObjectArraySyncResult).toBe(1)
})
}
});

查看文件

@@ -0,0 +1,525 @@
<template>
<!-- #ifdef APP && !VUE3-VAPOR -->
<scroll-view class="page-scroll-view">
<!-- #endif -->
<view>
<page-head :title="data.title"></page-head>
<view class="uni-common-mt">
<view class="uni-list">
<view class="uni-list-cell uni-list-cell-line">
<view class="uni-list-cell-left">
<view class="uni-label">key</view>
</view>
<view class="uni-list-cell-db">
<input class="uni-input" type="text" placeholder="请输入key" name="key" :value="data.key" :maxlength="-1"
@input="keyChange" />
</view>
</view>
<view class="uni-list-cell">
<view class="uni-list-cell-left">
<view class="uni-label">value</view>
</view>
<view class="uni-list-cell-db">
<input class="uni-input" type="text" placeholder="请输入value" name="data"
:value="typeof data.data === 'string' ? data.data : JSON.stringify(data.data)" :maxlength="-1" @input="dataChange" />
</view>
</view>
</view>
<view class="uni-padding-wrap">
<view class="uni-btn-v">
<button class="uni-btn btn-getStorageInfoASync" type="primary" @tap="getStorageInfo">
获取存储概述信息-异步
</button>
<button class="uni-btn btn-getStorageInfoSync" @tap="getStorageInfoSync">
获取存储概述信息-同步
</button>
</view>
<text class="uni-list-cell-db-text">{{ data.storageInfo }}</text>
<view class="uni-row">
<button type="default" style="width:50%" @tap="strMock">
填充字符串
</button>
<button type="default" style="width:50%" @tap="complexMock">
填充复杂对象
</button>
</view>
<view class="uni-row">
<button type="default" style="width:50%" @tap="numberMock">
填充整型
</button>
<button type="default" style="width:50%" @tap="floatMock">
填充浮点型
</button>
</view>
<view class="uni-row">
<button type="default" style="width:50%" @tap="jsonLikeMock">
填充json字符串
</button>
<button type="default" style="width:50%" @tap="longLikeMock">
填充整数字符串
</button>
</view>
<view class="uni-row">
<button type="default" style="width:50%" @tap="floatLikeMock">
填充浮点字符串
</button>
<button type="default" style="width:50%" @tap="negativeLikeMock">
填充负数字符串
</button>
</view>
<view class="uni-row">
<button type="default" class="uni-btn btn-complexStaticTest" style="width:100%" @tap="complexStaticTest">
字面量读写测试
</button>
</view>
</view>
<view class="uni-padding-wrap">
<view class="uni-btn-v">
<button type="primary" class="uni-btn btn-setstorageAsync" @tap="setStorage">
存储数据-异步
</button>
<button class="uni-btn btn-getstorageAsync" @tap="getStorage">读取数据-异步</button>
<button class="uni-btn btn-removeStorageInfoASync" @tap="removeStorage">移除数据-异步</button>
<button class="uni-btn btn-clearStorageInfoASync" @tap="clearStorage">清理数据-异步</button>
</view>
<view class="uni-btn-v">
<button type="primary" class="uni-btn btn-setstorageSync" @tap="setStorageSync">
存储数据-同步
</button>
<button class="uni-btn btn-getstorageSync" @tap="getStorageSync">读取数据-同步</button>
<button class="uni-btn btn-removeStorageInfoSync" @tap="removeStorageSync">
移除数据-同步
</button>
<button class="uni-btn btn-clearStorageInfoSync" @tap="clearStorageSync">
清理数据-同步
</button>
</view>
</view>
</view>
<button type="primary" @click="goto">前往storage管理器</button>
</view>
<!-- #ifdef APP && !VUE3-VAPOR -->
</scroll-view>
<!-- #endif -->
</template>
<script setup lang="uts">
type DataType = {
title: string
key: string
data: any
apiGetData: any | null
storageInfo: string
staticComplexRet: boolean
jest_saveUTSJSONObjectSyncResult: number
jest_saveUTSJSONObjectAsyncResult: number
jest_saveUTSJSONObjectArraySyncResult: number
}
// 使用reactive避免ref数据在自动化测试中无法访问
const data = reactive({
title: 'get/set/clearStorage',
key: '',
data: '',
apiGetData: '',
storageInfo: '',
staticComplexRet: false,
jest_saveUTSJSONObjectSyncResult: 0,
jest_saveUTSJSONObjectAsyncResult: 0,
jest_saveUTSJSONObjectArraySyncResult: 0
} as DataType)
const getStorageInfo = () => {
uni.getStorageInfo({
success: (res) => {
data.apiGetData = res
data.storageInfo = JSON.stringify(res)
},
})
}
const getStorageInfoSync = () => {
try {
const res = uni.getStorageInfoSync()
data.apiGetData = res
data.storageInfo = JSON.stringify(res)
} catch (e) {
// error
console.log(e)
}
}
const jsonLikeMock = () => {
data.key = 'key_' + Math.random()
data.data = JSON.stringify({
name: "james",
age: 12,
from: "american"
});
}
const longLikeMock = () => {
data.key = 'key_' + Math.random()
data.data = "1234567890"
}
const floatLikeMock = () => {
data.key = 'key_' + Math.random()
data.data = "321456.1234567890"
}
const negativeLikeMock = () => {
data.key = 'key_' + Math.random()
data.data = "-321456"
}
const strMock = () => {
data.key = 'key_' + Math.random()
data.data = '测试字符串数据,长度为16个字符'
}
const complexStaticTest = () => {
uni.setStorageSync("key_complexStaticMock", {
name: "张三",
age: 12
})
let savedData = uni.getStorageSync("key_complexStaticMock")
data.staticComplexRet = false
if (savedData instanceof UTSJSONObject) {
if ((savedData as UTSJSONObject).getNumber('age') == 12) {
data.staticComplexRet = true
uni.showToast({
icon: 'success',
title: '测试通过'
})
}
}
}
const complexMock = () => {
data.key = 'key_' + Math.random()
let jsonObj = {
name: '张三',
age: 12,
classMate: [
{
id: 1001,
name: '李四',
},
{
id: 1002,
name: 'jack ma',
},
],
}
data.data = jsonObj
}
const numberMock = () => {
data.key = 'key_' + Math.random()
data.data = 10011
}
const floatMock = () => {
data.key = 'key_' + Math.random()
data.data = 3.1415926535893384626
}
const keyChange = (e : InputEvent) => {
data.key = e.detail.value
}
const dataChange = (e : InputEvent) => {
data.data = e.detail.value
}
const getStorage = () => {
var key = data.key
if (key.length == 0) {
// #ifndef MP
uni.hideModal({
modalPage:null
})
// #endif
uni.showModal({
title: '读取数据失败',
content: 'key 不能为空',
showCancel: false,
})
} else {
uni.getStorage({
key: key,
success: (res) => {
data.apiGetData = res.data
let desc : string = typeof data.apiGetData
if ("object" == desc) {
desc = desc + ": " + JSON.stringify(data.apiGetData)
} else {
desc = desc + ": " + data.apiGetData
}
// #ifndef MP
uni.hideModal({
modalPage:null
})
// #endif
uni.showModal({
title: '读取数据成功',
content: desc,
showCancel: false,
})
},
fail: () => {
// #ifndef MP
uni.hideModal({
modalPage:null
})
// #endif
uni.showModal({
title: '读取数据失败',
content: '找不到 key 对应的数据',
showCancel: false,
})
},
})
}
}
const getStorageSync = () => {
var key = data.key
if (key.length == 0) {
// #ifndef MP
uni.hideModal({
modalPage:null
})
// #endif
uni.showModal({
title: '读取数据失败',
content: 'key 不能为空',
showCancel: false,
})
} else {
data.apiGetData = uni.getStorageSync(key)
let desc : string = typeof data.apiGetData
if ("object" == desc) {
desc = desc + ": " + JSON.stringify(data.apiGetData)
} else {
desc = desc + ": " + data.apiGetData
}
// #ifndef MP
uni.hideModal({
modalPage:null
})
// #endif
uni.showModal({
title: '读取数据成功',
content: desc,
showCancel: false,
})
}
}
const setStorage = () => {
var key = data.key
var storageData = data.data
if (key.length == 0) {
// #ifndef MP
uni.hideModal({
modalPage:null
})
// #endif
uni.showModal({
title: '保存数据失败',
content: 'key 不能为空',
showCancel: false,
})
} else {
uni.setStorage({
key: key,
data: storageData,
success: () => {
// #ifndef MP
uni.hideModal({
modalPage:null
})
// #endif
uni.showModal({
title: '存储数据成功',
showCancel: false,
})
},
fail: () => {
// #ifndef MP
uni.hideModal({
modalPage:null
})
// #endif
uni.showModal({
title: '储存数据失败!',
showCancel: false,
})
},
})
}
}
const setStorageSync = () => {
var key = data.key
var storageData = data.data
if (key.length == 0) {
// #ifndef MP
uni.hideModal({
modalPage:null
})
// #endif
uni.showModal({
title: '保存数据失败',
content: 'key 不能为空',
showCancel: false,
})
} else {
uni.setStorageSync(key, storageData)
// #ifndef MP
uni.hideModal({
modalPage:null
})
// #endif
uni.showModal({
title: '存储数据成功',
showCancel: false,
})
}
}
const removeStorage = () => {
uni.removeStorage({
key: data.key,
success: () => {
// #ifndef MP
uni.hideModal({
modalPage:null
})
// #endif
uni.showModal({
title: '移除数据成功',
showCancel: false,
})
},
fail: () => {
// #ifndef MP
uni.hideModal({
modalPage:null
})
// #endif
uni.showModal({
title: '移除数据失败',
showCancel: false,
})
},
})
}
const removeStorageSync = () => {
uni.removeStorageSync(data.key)
// #ifndef MP
uni.hideModal({
modalPage:null
})
// #endif
uni.showModal({
title: '移除数据成功',
showCancel: false,
})
}
const clearStorage = () => {
data.key = ''
data.data = ''
uni.clearStorage({
success: function (_) {
// #ifndef MP
uni.hideModal({
modalPage:null
})
// #endif
uni.showModal({
title: '清除数据成功',
showCancel: false,
})
},
fail: function (_) {
// #ifndef MP
uni.hideModal({
modalPage:null
})
// #endif
uni.showModal({
title: '清除数据失败',
showCancel: false,
})
},
})
}
const clearStorageSync = () => {
data.key = ''
data.data = ''
uni.clearStorageSync()
// #ifndef MP
uni.hideModal({
modalPage:null
})
// #endif
uni.showModal({
title: '清除数据成功',
content: ' ',
showCancel: false,
})
}
const jest_saveUTSJSONObject = () => {
const key = 'test_key_saveUTSJSONObject'
uni.setStorageSync(key, {
a: {
b: 1
}
})
const dataSync = uni.getStorageSync(key) as UTSJSONObject
const dataSyncA = dataSync['a'] as UTSJSONObject
data.jest_saveUTSJSONObjectSyncResult = dataSyncA.get('b') as number
uni.getStorage({
key,
success: (res) => {
const dataAsync = res.data as UTSJSONObject
const dataAsyncA = dataAsync['a'] as UTSJSONObject
data.jest_saveUTSJSONObjectAsyncResult = dataAsyncA.get('b') as number
console.log('data.jest_saveUTSJSONObjectSyncResult: ' + data.jest_saveUTSJSONObjectSyncResult)
console.log('data.jest_saveUTSJSONObjectAsyncResult: ' + data.jest_saveUTSJSONObjectAsyncResult)
}
})
}
const jest_saveUTSJSONObjectArray = () => {
const key = 'test_key_saveUTSJSONObjectArray'
uni.setStorageSync(key, [{
a: 1
}] as UTSJSONObject[])
const dataSync = uni.getStorageSync(key) as UTSJSONObject[]
data.jest_saveUTSJSONObjectArraySyncResult = dataSync[0].get('a') as number
}
const goto = () => {
uni.navigateTo({
url:"/pages/API/storage/storagemanage"
})
}
defineExpose({
data,
jest_saveUTSJSONObject,
jest_saveUTSJSONObjectArray
})
</script>
<style>
</style>

查看文件

@@ -0,0 +1,176 @@
const platformInfo = process.env.uniTestPlatformInfo.toLocaleLowerCase()
const isMP = platformInfo.startsWith('mp')
const isWeb = platformInfo.startsWith('web')
const isIos = platformInfo.startsWith('ios')
const isDom2 = process.env.UNI_APP_X_DOM2 === "true"
describe('Storage管理器页面-多类型新增', () => {
if (isIos || isMP) {
it('skip', () => {
expect(1).toBe(1)
})
return
}
if (isDom2) {
it('dom2 暂不支持 .input api', () => {
expect(1).toBe(1)
})
return
}
// 定义类型默认值映射,与页面保持一致
const valueTypeDefaultMap = new Map([
['String', 'hello'],
['Number', '1'],
['Boolean', 'true'],
['Object', '{"name": "张三","age": 12}'],
['Array', '[1, "hello", true, { "key": "value" }]']
])
let page
beforeAll(async () => {
page = await program.navigateTo('/pages/API/storage/storagemanage')
// 开启测试模式,跳过确认弹窗
await page.callMethod('setTestMode', true)
await page.waitFor(500)
const list = await page.callMethod('getStorageList')
// 点击清空按钮
if (list.length) {
const clearBtn = await page.$('.btn-clear')
await clearBtn.tap()
await page.waitFor(300)
}
})
afterAll(async () => {
// 关闭测试模式
await page.callMethod('setTestMode', false)
})
it('1. 新增String类型', async () => {
// 点击新建按钮
const createBtn = await page.$('.btn-create')
await createBtn.tap()
await page.waitFor(300)
// 获取弹窗元素
const mask = await page.$('.dialog-mask')
const keyInput = await mask.$('.edit-input')
const valueTextarea = await mask.$('.edit-textarea')
const saveBtn = await mask.$('.btn-save')
// 输入内容
await keyInput.input('test_string')
await valueTextarea.input('hello world')
await saveBtn.tap()
await page.waitFor(300)
// 验证新增成功
const list = await page.callMethod('getStorageList')
expect(list.length).toBe(1)
if (!isMP) {
expect(list[0].key).toBe('test_string')
expect(list[0].value).toBe('hello world')
}
})
it('2. 编辑String类型', async () => {
// 点击编辑按钮
const editBtn = await page.$('.btn-edit')
await editBtn.tap()
await page.waitFor(300)
// 获取弹窗元素
const mask = await page.$('.dialog-mask')
const keyInput = await mask.$('.edit-input')
const valueTextarea = await mask.$('.edit-textarea')
const saveBtn = await mask.$('.btn-save')
// 修改内容
await keyInput.input('test_string_edited')
await valueTextarea.input('hello world edited')
await saveBtn.tap()
await page.waitFor(300)
// 验证修改成功
const list = await page.callMethod('getStorageList')
// 微信小程序端,基础库3.5.8以上版本,获取 value是[object HTMLElement] 暂时跳过
if (!isMP) {
expect(list[0].key).toBe('test_string_edited')
expect(list[0].value).toBe('hello world edited')
}
})
it('3. 删除String类型', async () => {
// 获取list-view的所有项,并选择第一个
const listView = await page.$('.list-view')
const listItems = await listView.$$('list-item')
await listItems[0].tap()
await page.waitFor(300)
// 点击删除按钮
const deleteBtn = await page.$('.btn-delete')
await deleteBtn.tap()
await page.waitFor(300)
// 验证删除成功
const list = await page.callMethod('getStorageList')
expect(list.length).toBe(0)
})
it('4. 测试切换类型自动填充默认值', async () => {
// 点击新建按钮
const createBtn = await page.$('.btn-create')
await createBtn.tap()
await page.waitFor(300)
// 获取弹窗元素
const mask = await page.$('.dialog-mask')
const typeItems = await mask.$$('.edit-type-radio')
// 测试每种类型
for (let i = 0; i < typeItems.length; i++) {
await typeItems[i].tap()
await page.waitFor(200)
// 验证默认值是否正确
const editValue = await page.data('editValue')
const editValueType = await page.data('editValueType')
if (isWeb && editValueType._value != 'String') {
// 获取 ref 的 value 属性
expect(editValue._value).toBe(valueTypeDefaultMap.get(editValueType._value))
}
}
// 关闭弹窗
const cancelBtn = await mask.$('.btn-cancel')
await cancelBtn.tap()
await page.waitFor(300)
})
it('5. 新增所有类型', async () => {
const types = ['String', 'Number', 'Boolean', 'Object', 'Array']
for (const type of types) {
// 点击新建按钮
const createBtn = await page.$('.btn-create')
await createBtn.tap()
await page.waitFor(300)
// 获取弹窗元素
const mask = await page.$('.dialog-mask')
const keyInput = await mask.$('.edit-input')
const valueTextarea = await mask.$('.edit-textarea')
const saveBtn = await mask.$('.btn-save')
const typeRadios = await mask.$$('.edit-type-radio')
// 选择类型
const typeIndex = types.indexOf(type)
await typeRadios[typeIndex].tap()
await page.waitFor(100)
// 输入内容
await keyInput.input(`test_${type.toLowerCase()}`)
await valueTextarea.input(valueTypeDefaultMap.get(type))
await saveBtn.tap()
await page.waitFor(300)
}
// 验证所有类型都添加成功
const list = await page.callMethod('getStorageList')
if (!isMP) {
expect(list.length).toBe(types.length)
}
})
it('6. 清空所有存储项', async () => {
// 点击清空按钮
const clearBtn = await page.$('.btn-clear')
await clearBtn.tap()
await page.waitFor(300)
// 验证清空成功
const list = await page.callMethod('getStorageList')
expect(list.length).toBe(0)
})
})

查看文件

@@ -0,0 +1,548 @@
<template>
<view class="container">
<view class="header">
<text class="uni-title-text">Storage管理器</text>
<button class="btn btn-create" type="primary" @click="openEditDialogNew">新建</button>
<button class="btn btn-clear" type="default" @click="confirmClear" v-if="storageList.length>0">清空所有</button>
</view>
<list-view class="list-view" v-if="storageList.length>0">
<list-item v-for="(item, index) in storageList" :key="item.key" @click="showDetail(item)">
<view class="item-block">
<view class="item-row">
<text class="item-label">Key:</text>
<text class="item-key">{{ item.key }}</text>
</view>
<view class="item-row">
<text class="item-label">Data:</text>
<text class="item-key">{{ truncate(item.value) }}</text>
</view>
<view class="item-row">
<text class="item-label">Type:</text>
<text class="item-key">{{ item.type }}</text>
</view>
<view class="item-row item-actions-row">
<button class="btn btn-delete" type="default" @click.stop="confirmDelete(index)">删除</button>
<button class="btn btn-edit" type="primary" @click.stop="openEditDialogEdit(item, index)">编辑</button>
</view>
</view>
</list-item>
</list-view>
<view v-else class="uni-center">
<text class="uni-hello-text">暂无数据</text>
</view>
<!-- 详情弹窗 -->
<view v-if="showDetailDialog" class="dialog-mask" @click="closeDetail">
<view class="dialog-content" @click.stop>
<text class="dialog-title">详情</text>
<view class="detail-row">
<text class="item-label">Key:</text>
<text class="item-key">{{ detailItem.key }}</text>
</view>
<view class="detail-row">
<text class="item-label">Data:</text>
<text class="item-key uni-list-cell-db-text">{{ detailItem.value }}</text>
</view>
<view class="detail-row">
<text class="item-label">Type:</text>
<text class="item-key">{{ detailItem.type }}</text>
</view>
<view class="uni-common-mt popup-actions">
<button class="btn mr-20" type="primary" @click="openEditDialogEdit(detailItem, getDetailIndex())">编辑</button>
<button class="btn mr-20" type="warn" @click="confirmDelete(getDetailIndex())">删除</button>
<button class="btn" @click="closeDetail">关闭</button>
</view>
</view>
</view>
<!-- 新建/编辑弹窗 -->
<view v-show="showEditDialog" class="dialog-mask" @click="closeEdit">
<view class="dialog-content" @click.stop>
<text class="dialog-title">{{ isEditing ? '编辑' : '新建' }}</text>
<view class="edit-row">
<text class="edit-label">Key</text>
<input v-model="editKey" placeholder="请输入key" class="edit-input" />
</view>
<view class="edit-row">
<text class="edit-label">Value</text>
<textarea v-model="editValue" placeholder="请输入value" class="edit-textarea" />
</view>
<view class="edit-row" v-if="!isEditing">
<text class="edit-label">类型</text>
<radio-group class="edit-type-group" @change="onValueTypeChange">
<radio v-for="vt in valueTypeOptions" :key="vt" :value="vt" :checked="editValueType===vt"
class="edit-type-radio">
<text>{{ vt }}</text>
</radio>
</radio-group>
</view>
<view class="popup-actions">
<button class="btn mr-20 btn-cancel" type="default" @click="closeEdit">取消</button>
<button class="btn btn-save" type="primary" @click="saveEdit">保存</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
type StorageItem = {
key : string
value : string
type : string
}
type StorageList = Array<StorageItem>
const storageList = ref([] as StorageItem[])
const newKey = ref('')
const newValue = ref('')
const isEditing = ref(false)
const editIndex = ref(-1)
const detailItem = ref({ key: '', value: '', type: '' } as StorageItem)
const editKey = ref('')
const editValue = ref('')
const editValueType = ref('Number')
const showDetailDialog = ref(false)
const showEditDialog = ref(false)
const valueTypeOptions = ['String', 'Number', 'Boolean', 'Object', 'Array']
const valueTypeDefaultMap = new Map<string, string>()
valueTypeDefaultMap.set('String', '')
valueTypeDefaultMap.set('Number', '1')
valueTypeDefaultMap.set('Boolean', 'true')
valueTypeDefaultMap.set('Object', `{"name": "张三","age": 12}`)
valueTypeDefaultMap.set('Array', `[1, "hello", true, { "key": "value" }]`)
// 自动化测试使用
const isTestMode = ref(false)
function getStorageList() : StorageList {
const list : StorageList = []
const storageInfo = uni.getStorageInfoSync()
storageInfo.keys.forEach((key : string) => {
const value = uni.getStorageSync(key)
let strValue : string | null = null
let typeStr : string = typeof value
if (value != null) {
if (typeStr == 'object') {
const jsonStr = JSON.stringify(value)
strValue = jsonStr
if (Array.isArray(JSON.parse(jsonStr))) {
typeStr = 'Array'
} else {
typeStr = 'Object'
}
} else if (typeStr == 'boolean') {
strValue = value == true ? 'true' : 'false'
typeStr = 'Boolean'
} else if (typeStr == 'number') {
strValue = value.toString()
typeStr = 'Number'
} else {
strValue = value.toString()
typeStr = 'String'
}
}
if (strValue != null) {
list.push({
key: key,
value: strValue,
type: typeStr
})
}
})
return list
}
function setStorage(key : string, value : any) {
try {
uni.setStorageSync(key, value)
} catch (e) {
console.error('Storage set error:', e)
}
}
function removeStorage(key : string) {
try {
uni.removeStorageSync(key)
} catch (e) {
console.error('Storage remove error:', e)
}
}
function clearStorage() {
try {
uni.clearStorageSync()
} catch (e) {
console.error('Storage clear error:', e)
}
}
function getStorage(key : string) : string | null {
try {
const value = uni.getStorageSync(key)
return value != null ? value.toString() : null
} catch (e) {
console.error('Storage get error:', e)
return null
}
}
function refreshList() {
const list = getStorageList()
console.log('list: ',list);
if (!isEditing.value && editKey.value != '') {
const idx = list.findIndex(item => item.key === editKey.value)
if (idx > 0) {
const spliced = list.splice(idx, 1)
if (spliced.length > 0) {
list.unshift(spliced[0])
}
}
}
storageList.value = list
}
function truncate(value : string) : string {
if (value.length > 100) {
return value.slice(0, 100) + '...'
}
return value
}
function showDetail(item : StorageItem) {
detailItem.value = item
showDetailDialog.value = true
}
function closeDetail() {
showDetailDialog.value = false
}
function openEditDialogNew() {
editKey.value = ''
editValueType.value = 'String'
editValue.value = valueTypeDefaultMap.get('String') ?? ''
isEditing.value = false
editIndex.value = -1
showEditDialog.value = true
}
function openEditDialogEdit(item : StorageItem, index : number) {
editKey.value = item.key
editValue.value = item.value
isEditing.value = true
editIndex.value = index
editValueType.value = valueTypeOptions.indexOf(item.type) >= 0 ? item.type : 'String'
showEditDialog.value = true
closeDetail()
}
function saveEdit() {
if (!isTestMode.value && (editKey.value.trim() === '' || editValue.value.trim() === '')) {
uni.showModal({
title: '提示',
content: 'Key 和 Value 都不能为空',
showCancel: false,
})
return
}
let storeValue : any
switch (editValueType.value) {
case 'Number':
storeValue = parseFloat(editValue.value)
break
case 'Boolean':
storeValue = (editValue.value === 'true' || editValue.value === '1')
break
case 'Object':
try {
const obj = JSON.parse(editValue.value)
storeValue = obj as UTSJSONObject
} catch {
storeValue = {} as UTSJSONObject
}
break
case 'Array':
try {
const arr = JSON.parse(editValue.value) as Array<any>
storeValue = arr // 直接存储数组
} catch {
storeValue = [] as Array<any>
}
break
default:
storeValue = editValue.value
}
if (editIndex.value >= 0) {
const oldItem = storageList.value[editIndex.value]
if (oldItem.key != editKey.value) {
removeStorage(oldItem.key)
}
setStorage(editKey.value, storeValue)
} else {
setStorage(editKey.value, storeValue)
}
refreshList()
isEditing.value = false
editIndex.value = -1
editKey.value = ''
editValue.value = ''
editValueType.value = 'String'
showEditDialog.value = false
}
function closeEdit() {
showEditDialog.value = false
}
function handleDelete(index : number) {
if (index >= 0 && index < storageList.value.length) {
const item = storageList.value[index]
removeStorage(item.key)
refreshList()
if (isEditing.value && editIndex.value == index) {
isEditing.value = false
editIndex.value = -1
editKey.value = ''
editValue.value = ''
}
}
}
function confirmDelete(index : number) {
// 自动化测试时不显示模态弹窗
if (!isTestMode.value) {
uni.showModal({
title: '确认操作',
content: '确定要删除该项吗?',
showCancel: true,
cancelText: '取消',
confirmText: '确定',
success: (res) => {
if (res.confirm) {
showDetailDialog.value = false
handleDelete(index)
}
}
})
} else {
showDetailDialog.value = false
handleDelete(index)
}
}
function handleClear() {
clearStorage()
refreshList()
isEditing.value = false
editIndex.value = -1
editKey.value = ''
editValue.value = ''
}
function confirmClear() {
// 自动化测试时不显示模态弹窗
if (!isTestMode.value) {
uni.showModal({
title: '确认操作',
content: '确定要清空所有数据吗?',
showCancel: true,
cancelText: '取消',
confirmText: '确定',
success: (res) => {
if (res.confirm) {
showDetailDialog.value = false
handleClear()
}
}
})
} else {
showDetailDialog.value = false
handleClear()
}
}
function getDetailIndex() : number {
return storageList.value.findIndex(item => item.key === detailItem.value.key)
}
function onValueTypeChange(e : UniRadioGroupChangeEvent) {
const type = e.detail.value
editValueType.value = type
if (valueTypeDefaultMap.has(type)) {
editValue.value = valueTypeDefaultMap.get(type) ?? ''
} else {
editValue.value = ''
}
}
onLoad(() => {
refreshList()
})
/**
* 仅供自动化测试用例调用,设置测试模式
*/
function setTestMode(val: boolean) {
isTestMode.value = val
}
defineExpose({
editValue,
editValueType,
getStorageList,
setTestMode, // 仅供自动化测试用例调用
})
</script>
<style>
.container {
flex-direction: column;
background: #f7f8fa;
flex: 1;
}
.header {
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 10px;
}
.btn {
height: 40px;
font-size: 16px;
}
.mr-20 {
margin-right: 20px;
}
.list-view {
flex: 1;
width: 100%;
background: #fff;
border-radius: 0;
padding: 0;
}
.item-block {
padding: 13px;
border-bottom: 1px solid #e5e5e5;
background: #fff;
}
.item-row {
flex-direction: row;
align-items: center;
margin-bottom: 4px;
}
.item-label {
color: #888;
font-size: 14px;
margin-right: 4px;
width: 50px;
}
.item-key {
color: #333;
font-size: 15px;
flex: 1;
/* #ifdef WEB */
word-break: break-all;
/* #endif */
}
.item-actions-row {
justify-content: space-between;
margin-top: 6px;
margin-bottom: 0;
}
.dialog-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.18);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.dialog-content {
background: #fff;
border-radius: 10px;
padding: 20px 16px 16px 16px;
min-width: 270px;
max-width: 345px;
flex-direction: column;
align-items: stretch;
position: relative;
box-shadow: none;
}
.dialog-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 14px;
text-align: center;
color: #222;
letter-spacing: 1px;
}
.detail-row {
flex-direction: row;
align-items: flex-start;
margin-bottom: 9px;
}
.popup-actions {
flex-direction: row;
justify-content: flex-end;
margin-top: 16px;
margin-right: 8px;
}
.error-tip {
color: #FF3B30;
font-size: 15px;
margin-bottom: 8px;
text-align: center;
font-weight: bold;
}
.edit-row {
flex-direction: row;
align-items: center;
margin-bottom: 9px;
}
.edit-label {
min-width: 35px;
color: #888;
font-size: 15px;
margin-right: 6px;
}
.edit-input {
flex: 1;
height: 40px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 0 6px;
font-size: 15px;
background: #fff;
}
.edit-textarea {
flex: 1;
min-height: 80px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 10px 6px;
font-size: 15px;
background: #fff;
}
.edit-type-group {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
width: 90%;
}
.edit-type-radio {
margin-right: 12px;
margin-bottom: 6px;
}
</style>