File size: 4,597 Bytes
a1b6014 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | import { act, cleanup, renderHook } from '@testing-library/react'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
const { request } = vi.hoisted(() => ({ request: vi.fn(async () => undefined) }))
vi.mock('@/store/gateway', async () => {
const { atom } = await import('nanostores')
return { $gateway: atom<unknown>(null), activeGateway: () => ({ request }) }
})
vi.mock('@/store/session', async () => {
const { atom } = await import('nanostores')
return { $awaitingResponse: atom(false), $busy: atom(false) }
})
vi.mock('react-router', () => ({ useNavigate: () => vi.fn() }))
vi.mock('./local-setup-offer', () => ({ offerLocalSetupTip: () => false }))
vi.mock('./local-runtime-update-offer', () => ({ offerLocalRuntimeUpdateTip: () => false }))
import { en } from '@/i18n/en'
const DAY_MS = 24 * 60 * 60_000
const START = new Date('2026-01-01T12:00:00Z').getTime()
beforeEach(() => {
vi.resetModules()
localStorage.clear()
request.mockClear()
vi.useFakeTimers()
vi.setSystemTime(START)
})
afterEach(() => {
cleanup()
vi.useRealTimers()
})
it('retires both tutorial features after 30 days across launches, then keeps a manual re-enable', async () => {
const { useTipRotation } = await import('./use-tip-rotation')
let tips = await import('@/store/tips')
let tours = await import('@/store/tours')
const firstLaunch = renderHook(() => useTipRotation(en.tips))
expect(tips.$tipsEnabled.get()).toBe(true)
expect(tours.$toursEnabled.get()).toBe(true)
expect(request).not.toHaveBeenCalled()
firstLaunch.unmount()
// Reload updated modules against the same persistent storage, just as an
// app update does. The deadline must remain anchored to the first launch.
vi.setSystemTime(START + 30 * DAY_MS - 30_000)
vi.resetModules()
const returning = await import('./use-tip-rotation')
tips = await import('@/store/tips')
tours = await import('@/store/tours')
const secondLaunch = renderHook(() => returning.useTipRotation(en.tips))
expect(tips.$tipsEnabled.get()).toBe(true)
expect(tours.$toursEnabled.get()).toBe(true)
tips.resetTips() // Replaying the catalog must not restart the month either.
tips.showTip({ side: 'bottom', targets: ['body'], text: 'A pending tip' })
act(() => vi.advanceTimersByTime(30_000))
expect(tips.$tipsEnabled.get()).toBe(false)
expect(tours.$toursEnabled.get()).toBe(false)
expect(tips.$activeTip.get()).toBeNull()
expect(request).toHaveBeenCalledWith('config.set', { key: 'display.in_app_tips', value: 'false' })
expect(request).toHaveBeenCalledWith('config.set', { key: 'display.in_app_tours', value: 'false' })
act(() => {
tips.setTipsEnabled(true)
tours.setToursEnabled(true)
tips.resetTips()
})
secondLaunch.unmount()
vi.setSystemTime(START + 60 * DAY_MS)
vi.resetModules()
const later = await import('./use-tip-rotation')
tips = await import('@/store/tips')
tours = await import('@/store/tours')
renderHook(() => later.useTipRotation(en.tips))
act(() => vi.advanceTimersByTime(30_000))
expect(tips.$tipsEnabled.get()).toBe(true)
expect(tours.$toursEnabled.get()).toBe(true)
})
it('uses existing tip history on upgrade without treating invalid dates as experience', async () => {
localStorage.setItem(
'hermes.desktop.tips.shownAt.v1',
JSON.stringify({
old: START - 31 * DAY_MS,
recent: START - DAY_MS,
invalid: 'not a timestamp',
future: START + DAY_MS,
zero: 0,
negative: -1
})
)
const { useTipRotation } = await import('./use-tip-rotation')
let tips = await import('@/store/tips')
let tours = await import('@/store/tours')
const upgraded = renderHook(() => useTipRotation(en.tips))
expect(tips.$tipsEnabled.get()).toBe(false)
expect(tours.$toursEnabled.get()).toBe(false)
upgraded.unmount()
localStorage.clear()
localStorage.setItem(
'hermes.desktop.tips.shownAt.v1',
JSON.stringify({ invalid: 'not a timestamp', future: START + DAY_MS, zero: 0, negative: -1 })
)
localStorage.setItem('hermes.desktop.tips.rotation.v1', 'false')
vi.resetModules()
const fresh = await import('./use-tip-rotation')
tips = await import('@/store/tips')
tours = await import('@/store/tours')
renderHook(() => fresh.useTipRotation(en.tips))
// No usable history: begin the month now, never turn an existing Off on.
expect(tips.$tipsEnabled.get()).toBe(false)
expect(tours.$toursEnabled.get()).toBe(true)
act(() => {
vi.setSystemTime(START + 30 * DAY_MS)
vi.advanceTimersByTime(30_000)
})
expect(tours.$toursEnabled.get()).toBe(false)
})
|