feat(auth): 游客浏览模式 + needUserInfo 授权链路修复

- PUBLIC_PAGES 放开四个 tab 页(微信审核要求不强制授权),游客可浏览,
  首页/事件/设备页未登录展示引导登录空态不再强制踢登录;mine 游客态
  菜单收起只留登录入口;守卫单测同步
- needUserInfo 实测首登 Content 为 null:openId 改为可缺省的加速字段,
  授权重调靠新 code 即可,登录页不再因缺 openId 阻断;补缺省分支单测
- http 层首登 Content 为 null 时先初始化再挂 message/token 字段
- 生产网关域名替换占位符(aiotcloud.celex-technology.com)
This commit is contained in:
ozh 2026-10-09 14:33:04 +08:00
parent da86294fde
commit 42b6ceb69c
11 changed files with 144 additions and 36 deletions

View File

@ -9,7 +9,7 @@ import { ApiError, BusinessError, AuthExpiredError } from './errors'
// 网关分服务前缀:登录走 user 服务,其余业务接口走 biz 服务(后端约定 2026-09-17) // 网关分服务前缀:登录走 user 服务,其余业务接口走 biz 服务(后端约定 2026-09-17)
const GATEWAY = process.env.NODE_ENV === 'development' const GATEWAY = process.env.NODE_ENV === 'development'
? 'http://192.168.10.11:39101' ? 'http://192.168.10.11:39101'
: 'https://PROD_PLACEHOLDER' // 正式域名占位符,上线前必须替换(spec D2) : 'https://aiotcloud.celex-technology.com' // 线上网关域名(2026-09-22 接入)
const USER_BASE = `${GATEWAY}/api/user` const USER_BASE = `${GATEWAY}/api/user`
const BIZ_BASE = `${GATEWAY}/api/biz` const BIZ_BASE = `${GATEWAY}/api/biz`
@ -122,13 +122,16 @@ export async function request<T>(
throw new BusinessError(body.Message || '业务错误', bizCode) throw new BusinessError(body.Message || '业务错误', bizCode)
} }
// 200 成功内的子状态(Message 携带,如 needUserInfo/noMobile),附到返回值供调用方分支处理 // 200 成功内的子状态(Message 携带,如 needUserInfo/noMobile),附到返回值供调用方分支处理
// 首登场景 Content 为 null(仅返回绑定凭证),需先初始化才能挂字段
const subState = body.Message const subState = body.Message
if (subState && subState !== '') { if (subState && subState !== '') {
if (!body.Content) body.Content = {} as T & { message?: string }
;(body.Content as Record<string, unknown>).message = subState ;(body.Content as Record<string, unknown>).message = subState
} }
// 后端把登录 token 放响应头 Token(仅登录接口返回;Content.code 恒为 null)——附到返回值供登录流程落地 // 后端把登录 token 放响应头 Token(仅登录接口返回)——附到返回值供登录流程落地
const resToken = pickResponseToken(res.header) const resToken = pickResponseToken(res.header)
if (resToken) { if (resToken) {
if (!body.Content) body.Content = {} as T & { message?: string }
;(body.Content as Record<string, unknown>).token = resToken ;(body.Content as Record<string, unknown>).token = resToken
} }
return body.Content return body.Content

View File

@ -6,6 +6,8 @@ import { Navbar } from '@/components/navbar'
import { Loading } from '@/components/loading' import { Loading } from '@/components/loading'
import { StateView } from '@/components/state-view' import { StateView } from '@/components/state-view'
import { BottomSheet } from '@/components/bottom-sheet' import { BottomSheet } from '@/components/bottom-sheet'
import { store } from '@/store'
import { storage } from '@/utils/storage'
import { getHomeDeviceList, type HomeDevice } from '@/api/homeDevice' import { getHomeDeviceList, type HomeDevice } from '@/api/homeDevice'
import { getRoomList, type HomeRoom } from '@/api/homeRoom' import { getRoomList, type HomeRoom } from '@/api/homeRoom'
import { getHomeList, type HomeInfo } from '@/api/home' import { getHomeList, type HomeInfo } from '@/api/home'
@ -47,6 +49,11 @@ export default function DeviceList() {
const [rooms, setRooms] = useState<HomeRoom[]>([]) const [rooms, setRooms] = useState<HomeRoom[]>([])
const [filterRoomId, setFilterRoomId] = useState('') const [filterRoomId, setFilterRoomId] = useState('')
// 游客浏览:未登录不拉数据,展示引导登录(微信审核要求不强制授权)
const isGuest = () =>
!store.getState().auth.token && !storage.get<string>('auth_token')
const isGuestEmpty = isGuest()
// 设备列表随家庭 + 房间筛选联动 // 设备列表随家庭 + 房间筛选联动
const loadDevices = useCallback(async (homeId?: string, roomId?: string) => { const loadDevices = useCallback(async (homeId?: string, roomId?: string) => {
const targetHomeId = homeId ?? currentFamily?.homeId const targetHomeId = homeId ?? currentFamily?.homeId
@ -109,6 +116,12 @@ export default function DeviceList() {
} }
useDidShow(() => { useDidShow(() => {
if (isGuest()) {
setDeviceList([])
setLoading(false)
setLoadError(false)
return
}
loadDevices(undefined, filterRoomId) loadDevices(undefined, filterRoomId)
loadFamilies() loadFamilies()
loadRooms() loadRooms()
@ -124,6 +137,7 @@ export default function DeviceList() {
} }
const goAdd = () => navigateTo(ROUTES.DEVICE_ADD) const goAdd = () => navigateTo(ROUTES.DEVICE_ADD)
const goRoom = () => navigateTo(ROUTES.SCENE_SELECT) const goRoom = () => navigateTo(ROUTES.SCENE_SELECT)
const goLogin = () => navigateTo(ROUTES.LOGIN)
const openFamilySheet = () => { const openFamilySheet = () => {
setPendingFamily(currentFamily) setPendingFamily(currentFamily)
@ -213,7 +227,7 @@ export default function DeviceList() {
)} )}
{/* 「我的设备」分区标题:数量随筛选联动 */} {/* 「我的设备」分区标题:数量随筛选联动 */}
{!loading && !loadError && ( {!loading && !loadError && !isGuestEmpty && (
<View className={styles.sectionTitle}> <View className={styles.sectionTitle}>
<Text>我的设备</Text> <Text>我的设备</Text>
<Text className={styles.sectionCount}>{deviceList.length}</Text> <Text className={styles.sectionCount}>{deviceList.length}</Text>
@ -223,6 +237,8 @@ export default function DeviceList() {
{/* 设备网格 */} {/* 设备网格 */}
{loading ? ( {loading ? (
<View className={styles.stateWrap}><Loading /></View> <View className={styles.stateWrap}><Loading /></View>
) : isGuestEmpty ? (
<StateView status="error" mode="list" text="登录后查看你的设备" retryText="去登录" onRetry={goLogin} />
) : loadError ? ( ) : loadError ? (
<StateView status="error" onRetry={() => loadDevices()} /> <StateView status="error" onRetry={() => loadDevices()} />
) : deviceList.length === 0 ? ( ) : deviceList.length === 0 ? (

View File

@ -3,6 +3,8 @@ import { useDidShow } from '@tarojs/taro'
import { View, Text } from '@tarojs/components' import { View, Text } from '@tarojs/components'
import { Navbar } from '@/components/navbar' import { Navbar } from '@/components/navbar'
import { Empty } from '@/components/empty' import { Empty } from '@/components/empty'
import { store } from '@/store'
import { storage } from '@/utils/storage'
import { getHomeList, type HomeInfo } from '@/api/home' import { getHomeList, type HomeInfo } from '@/api/home'
import { resolveCurrentHome } from '@/store/currentHome' import { resolveCurrentHome } from '@/store/currentHome'
import { getRoomList, type HomeRoom } from '@/api/homeRoom' import { getRoomList, type HomeRoom } from '@/api/homeRoom'
@ -48,8 +50,15 @@ export default function Event() {
] ]
// 当前家庭 + 其房间列表:与智能设备页共享选择(resolveCurrentHome 读持久化), // 当前家庭 + 其房间列表:与智能设备页共享选择(resolveCurrentHome 读持久化),
// 失败时保留"全部"选项不阻塞筛选 // 失败时保留"全部"选项不阻塞筛选;游客(未登录)跳过全部请求
const [guest, setGuest] = useState(false)
useEffect(() => { useEffect(() => {
if (!store.getState().auth.token && !storage.get<string>('auth_token')) {
setGuest(true)
setLoading(false)
return
}
getHomeList() getHomeList()
.then(async (list) => { .then(async (list) => {
const first = resolveCurrentHome(list) const first = resolveCurrentHome(list)
@ -62,6 +71,7 @@ export default function Event() {
}, []) }, [])
useDidShow(() => { useDidShow(() => {
if (guest) return
// 进页首拉:home 就绪与否都调一次,home 为空时后端会报错并保持空态 // 进页首拉:home 就绪与否都调一次,home 为空时后端会报错并保持空态
if (home) loadAlarms(filters) if (home) loadAlarms(filters)
}) })
@ -176,7 +186,16 @@ export default function Event() {
{/* 数据层:抽屉锚点(筛选栏下缘展开,遮罩盖住本层全部) */} {/* 数据层:抽屉锚点(筛选栏下缘展开,遮罩盖住本层全部) */}
<View className={styles.contentArea}> <View className={styles.contentArea}>
<View className={styles.contentBody}> <View className={styles.contentBody}>
{alarmList.length === 0 ? ( {guest ? (
<>
<View className={styles.emptyArea}>
<Empty text="登录后查看告警消息" mode="message" />
</View>
<View className={styles.emptyHint}>
<Text>浏览功能无需登录,查看家人告警时再登录</Text>
</View>
</>
) : alarmList.length === 0 ? (
<> <>
<View className={styles.emptyArea}> <View className={styles.emptyArea}>
<Empty text="暂无告警数据" mode="message" /> <Empty text="暂无告警数据" mode="message" />

View File

@ -12,6 +12,28 @@
align-items: center; align-items: center;
} }
// 游客浏览引导登录按钮(微信审核要求:不强制授权,用户自行选择)
.guestLoginBtn {
margin-top: 32px;
padding: 0 64px;
height: 80px;
background: $color-primary-grad;
border-radius: $radius-pill;
display: flex;
align-items: center;
justify-content: center;
}
.guestLoginBtnPressed {
opacity: 0.85;
}
.guestLoginText {
font-size: 28px;
font-weight: 600;
color: #ffffff;
}
// ===== 家庭圈入口:渐变横幅卡 ===== // ===== 家庭圈入口:渐变横幅卡 =====
.familyEntry { .familyEntry {
margin: 24px; margin: 24px;

View File

@ -11,7 +11,7 @@ import { getAlarmList, type HomeAlarm } from '@/api/homeAlarm'
import { store } from '@/store' import { store } from '@/store'
import { storage } from '@/utils/storage' import { storage } from '@/utils/storage'
import { ROUTES } from '@/constants/routes' import { ROUTES } from '@/constants/routes'
import { navigateTo, reLaunch } from '@/utils/navigation' import { navigateTo } from '@/utils/navigation'
import { splitAlerts } from './splitAlerts' import { splitAlerts } from './splitAlerts'
import styles from './index.module.scss' import styles from './index.module.scss'
@ -19,6 +19,8 @@ export default function Index() {
const [alertList, setAlertList] = useState<HomeAlarm[]>([]) const [alertList, setAlertList] = useState<HomeAlarm[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [loadError, setLoadError] = useState(false) const [loadError, setLoadError] = useState(false)
// 游客浏览标记:未登录不拉数据、不弹错误,展示引导登录空态
const [guest, setGuest] = useState(false)
// 每次进入页面重拉默认家庭的全部告警(与事件页"全部"筛选同参数) // 每次进入页面重拉默认家庭的全部告警(与事件页"全部"筛选同参数)
const loadAlerts = useCallback(async () => { const loadAlerts = useCallback(async () => {
@ -41,12 +43,16 @@ export default function Index() {
} }
}, []) }, [])
// 冷启动直达兜底:小程序入口页不受 addInterceptor 保护,未登录时踢到登录页 // 每次进入页面重拉当前家庭告警;未登录(游客浏览)展示引导登录空态,不强制跳登录
useDidShow(() => { useDidShow(() => {
if (!store.getState().auth.token && !storage.get<string>('auth_token')) { if (!store.getState().auth.token && !storage.get<string>('auth_token')) {
reLaunch(ROUTES.LOGIN) setAlertList([])
setLoading(false)
setLoadError(false)
setGuest(true)
return return
} }
setGuest(false)
loadAlerts() loadAlerts()
}) })
@ -61,6 +67,13 @@ export default function Index() {
{loading ? ( {loading ? (
<View className={styles.stateWrap}><Loading /></View> <View className={styles.stateWrap}><Loading /></View>
) : guest ? (
<View className={styles.stateWrap}>
<Empty text="登录后查看家人动态" mode="message" subText="浏览功能无需登录,需要查看告警与设备时再登录" />
<View className={styles.guestLoginBtn} hoverClass={styles.guestLoginBtnPressed} onClick={() => navigateTo(ROUTES.LOGIN)}>
<Text className={styles.guestLoginText}>去登录</Text>
</View>
</View>
) : loadError ? ( ) : loadError ? (
<StateView status="error" onRetry={loadAlerts} /> <StateView status="error" onRetry={loadAlerts} />
) : ( ) : (

View File

@ -55,11 +55,9 @@ export default function Login() {
// 手机号授权暂缓(后端字段未定):提示并保持当前态,等后端确认后接续 // 手机号授权暂缓(后端字段未定):提示并保持当前态,等后端确认后接续
throw new Error('后端要求绑定手机号,该流程暂未开通') throw new Error('后端要求绑定手机号,该流程暂未开通')
} }
// openId 兜底链:响应 openId → 本地缓存(needUserInfo 首次已写入)→ 当前 state // needUserInfo 实测 Content 为 null(无 openId),授权重调靠新 code 即可,
const openIdNext = // openId 仅作有则传的加速字段,缺失不再阻断
result.openId ?? storage.get<string>(WX_OPENID_STORAGE_KEY) ?? openId setOpenId(result.openId ?? storage.get<string>(WX_OPENID_STORAGE_KEY) ?? '')
if (!openIdNext) throw new Error('响应缺少 openId,无法继续授权')
setOpenId(openIdNext)
setNeedAuth(true) setNeedAuth(true)
setLoading(false) setLoading(false)
return return
@ -85,14 +83,15 @@ export default function Login() {
} }
} }
// needUserInfo:getUserInfo 授权用户资料(后端文档约定的授权接口) // needUserInfo 二次授权:wx.getUserInfo 拿 iv/encryptedData(后端文档约定,
// getUserProfile 的资料后端不认;getUserInfo 不受 tap 上下文限制,授权弹窗由微信拉起)
const handleAuthUserInfo = async () => { const handleAuthUserInfo = async () => {
if (loading || !openId) return if (loading) return
setLoading(true) setLoading(true)
try { try {
applyResult(await completeUserInfoLogin(openId)) applyResult(await completeUserInfoLogin(openId || undefined))
} catch (e) { } catch (e) {
// 授权失败/取消:保留待授权态,用户可重试 // 授权/登录失败:保留待授权态,用户可重试
console.error('[login-page] completeUserInfoLogin 失败', e) console.error('[login-page] completeUserInfoLogin 失败', e)
toast.error(e instanceof Error && e.message ? e.message : '授权失败') toast.error(e instanceof Error && e.message ? e.message : '授权失败')
setLoading(false) setLoading(false)

View File

@ -41,8 +41,9 @@ export async function performWxLogin(): Promise<WxMiniappLoginResponse> {
} }
// 重调登录接口的公共尾部:换新 code(一次性)→ 组装参数 → 调用 → 缓存 openId // 重调登录接口的公共尾部:换新 code(一次性)→ 组装参数 → 调用 → 缓存 openId
// openId 可缺省:needUserInfo 首登响应 Content 为 null(无 openId),靠 code + 授权资料即可重调
async function reloginWith( async function reloginWith(
openId: string, openId: string | undefined,
extra: { iv?: string; encryptedData?: string; phoneCode?: string } extra: { iv?: string; encryptedData?: string; phoneCode?: string }
): Promise<WxMiniappLoginResponse> { ): Promise<WxMiniappLoginResponse> {
let code: string let code: string
@ -52,7 +53,7 @@ async function reloginWith(
} catch { } catch {
throw new Error('获取微信登录凭证失败,请重试') throw new Error('获取微信登录凭证失败,请重试')
} }
const params = { appId: resolveAppId(), code, openId, ...extra } const params = { appId: resolveAppId(), code, ...(openId ? { openId } : {}), ...extra }
const result = await wxMiniappLogin(params) const result = await wxMiniappLogin(params)
if (result.openId) storage.set(WX_OPENID_STORAGE_KEY, result.openId) if (result.openId) storage.set(WX_OPENID_STORAGE_KEY, result.openId)
return result return result
@ -60,7 +61,7 @@ async function reloginWith(
// needUserInfo 二次授权:getUserInfo 拿 iv/encryptedData → 重调登录接口补用户资料 // needUserInfo 二次授权:getUserInfo 拿 iv/encryptedData → 重调登录接口补用户资料
// 后端文档约定:wx.login 返回 needUserInfo 时须用 wx.getUserInfo 授权(getUserProfile 拿的资料后端不认) // 后端文档约定:wx.login 返回 needUserInfo 时须用 wx.getUserInfo 授权(getUserProfile 拿的资料后端不认)
export async function completeUserInfoLogin(openId: string): Promise<WxMiniappLoginResponse> { export async function completeUserInfoLogin(openId: string | undefined): Promise<WxMiniappLoginResponse> {
let profile: { iv: string; encryptedData: string } let profile: { iv: string; encryptedData: string }
try { try {
const r = await Taro.getUserInfo({ withCredentials: true }) const r = await Taro.getUserInfo({ withCredentials: true })

View File

@ -45,13 +45,20 @@ export default function Mine() {
</View> </View>
<View className={styles.menuCard}> <View className={styles.menuCard}>
<ListRowGroup> {loggedOut ? (
<ListRow label="配置设备" arrow onClick={() => navigateTo(ROUTES.DEVICE_BLE_CONFIG)} /> <ListRowGroup>
<ListRow label="场所管理" arrow onClick={() => navigateTo(ROUTES.SCENE_SELECT)} /> {/* 游客态:菜单收起,只留一个登录入口(微信审核要求不强制授权) */}
<ListRow label="手机号" value={user?.mobile || '未绑定'} arrow /> <ListRow label="登录后使用完整功能" arrow onClick={goLogin} />
<ListRow label="当前版本" value="2.8.44" /> </ListRowGroup>
<ListRow label="退出登录" arrow danger onClick={logout} /> ) : (
</ListRowGroup> <ListRowGroup>
<ListRow label="配置设备" arrow onClick={() => navigateTo(ROUTES.DEVICE_BLE_CONFIG)} />
<ListRow label="场所管理" arrow onClick={() => navigateTo(ROUTES.SCENE_SELECT)} />
<ListRow label="手机号" value={user?.mobile || '未绑定'} arrow />
<ListRow label="当前版本" value="2.8.44" />
<ListRow label="退出登录" arrow danger onClick={logout} />
</ListRowGroup>
)}
</View> </View>
</View> </View>
) )

View File

@ -5,8 +5,15 @@ import { ROUTES } from '@/constants/routes'
// 在拦截器内改写 url 会把 http 请求地址误改成页面路径(request:fail invalid url), // 在拦截器内改写 url 会把 http 请求地址误改成页面路径(request:fail invalid url),
// 故不用全局拦截器,改为在 navigation 统一出口做跳转前校验。 // 故不用全局拦截器,改为在 navigation 统一出口做跳转前校验。
// 登录页及无需登录即可访问的页面(放行名单) // 游客可浏览页面(微信审核要求:进入首页即要求授权登录会被拒,
export const PUBLIC_PAGES = new Set<string>([ROUTES.LOGIN]) // tab 页与登录页放开,操作型页面按需登录)
export const PUBLIC_PAGES = new Set<string>([
ROUTES.LOGIN,
ROUTES.HOME,
ROUTES.DEVICE_LIST,
ROUTES.EVENT,
ROUTES.MINE
])
// 登录页路径(navigation 守卫改投目标) // 登录页路径(navigation 守卫改投目标)
export const ROUTES_LOGIN = ROUTES.LOGIN export const ROUTES_LOGIN = ROUTES.LOGIN

View File

@ -136,6 +136,8 @@ describe('needUserInfo 首次登录分支', () => {
}) })
describe('completeUserInfoLogin 二次授权', () => { describe('completeUserInfoLogin 二次授权', () => {
// 2026-09-24 定稿:内部走 wx.getUserInfo 取资料(后端不认 getUserProfile),
// openId 可缺省(needUserInfo 首登响应 Content 为 null)
it('getUserInfo(withCredentials) → 换新 code → 带 iv/encryptedData/openId 重调登录', async () => { it('getUserInfo(withCredentials) → 换新 code → 带 iv/encryptedData/openId 重调登录', async () => {
mockGetUserInfo.mockResolvedValue({ iv: 'iv-1', encryptedData: 'enc-1' }) mockGetUserInfo.mockResolvedValue({ iv: 'iv-1', encryptedData: 'enc-1' })
mockWxMiniappLogin.mockResolvedValue(result({ openId: 'o-need' })) mockWxMiniappLogin.mockResolvedValue(result({ openId: 'o-need' }))
@ -151,6 +153,18 @@ describe('completeUserInfoLogin 二次授权', () => {
}) })
}) })
it('openId 缺省时请求体不带 openId 字段(needUserInfo 首登无 openId 场景)', async () => {
mockGetUserInfo.mockResolvedValue({ iv: 'iv-1', encryptedData: 'enc-1' })
mockWxMiniappLogin.mockResolvedValue(result())
await completeUserInfoLogin(undefined)
expect(mockWxMiniappLogin).toHaveBeenCalledWith({
appId: 'wx-appid-live',
code: 'wx-code-1',
iv: 'iv-1',
encryptedData: 'enc-1'
})
})
it('用户拒绝授权(errMsg 形状 reject)抛带 message 的 Error', async () => { it('用户拒绝授权(errMsg 形状 reject)抛带 message 的 Error', async () => {
mockGetUserInfo.mockRejectedValue({ errMsg: 'getUserInfo:fail auth deny' }) mockGetUserInfo.mockRejectedValue({ errMsg: 'getUserInfo:fail auth deny' })
await expect(completeUserInfoLogin('o-need')).rejects.toThrow('getUserInfo:fail auth deny') await expect(completeUserInfoLogin('o-need')).rejects.toThrow('getUserInfo:fail auth deny')

View File

@ -52,13 +52,16 @@ describe('navigation 登录守卫', () => {
expect(mockNavigateTo).toHaveBeenCalledWith({ url: ROUTES.LOGIN }) expect(mockNavigateTo).toHaveBeenCalledWith({ url: ROUTES.LOGIN })
}) })
it('未登录 reLaunch/redirectTo/switchTab 同样被守卫', () => { it('未登录 reLaunch/redirectTo 到受保护页被守卫(tab 页在游客白名单放行)', () => {
reLaunch(ROUTES.HOME) reLaunch(ROUTES.DEVICE_ADD)
redirectTo(ROUTES.DEVICE_ADD) redirectTo(ROUTES.FAMILY)
switchTab(ROUTES.EVENT)
expect(mockReLaunch).toHaveBeenCalledWith({ url: ROUTES.LOGIN }) expect(mockReLaunch).toHaveBeenCalledWith({ url: ROUTES.LOGIN })
expect(mockRedirectTo).toHaveBeenCalledWith({ url: ROUTES.LOGIN }) expect(mockRedirectTo).toHaveBeenCalledWith({ url: ROUTES.LOGIN })
expect(mockSwitchTab).toHaveBeenCalledWith({ url: ROUTES.LOGIN }) })
it('未登录 switchTab 到 tab 页:放行(游客模式白名单,微信审核要求)', () => {
switchTab(ROUTES.EVENT)
expect(mockSwitchTab).toHaveBeenCalledWith({ url: ROUTES.EVENT })
}) })
it('已登录 navigateTo:原样放行', () => { it('已登录 navigateTo:原样放行', () => {
@ -85,9 +88,13 @@ describe('navigation 登录守卫', () => {
}) })
describe('isProtectedPage', () => { describe('isProtectedPage', () => {
it('登录页返回 false,其余页面返回 true', () => { it('登录页与四个 tab 页返回 false(游客可浏览),其余页面返回 true', () => {
expect(isProtectedPage(ROUTES.LOGIN)).toBe(false) expect(isProtectedPage(ROUTES.LOGIN)).toBe(false)
expect(isProtectedPage(ROUTES.HOME)).toBe(true) expect(isProtectedPage(ROUTES.HOME)).toBe(false)
expect(isProtectedPage(ROUTES.DEVICE_LIST)).toBe(false)
expect(isProtectedPage(ROUTES.EVENT)).toBe(false)
expect(isProtectedPage(ROUTES.MINE)).toBe(false)
expect(isProtectedPage(ROUTES.FAMILY)).toBe(true)
expect(isProtectedPage(`${ROUTES.LOGIN}?from=xxx`)).toBe(false) expect(isProtectedPage(`${ROUTES.LOGIN}?from=xxx`)).toBe(false)
}) })
}) })