aea0e16279
原来铃铛只能按关键字和日期筛,任务失败、密钥欠费、麦象异常全混在一起, 用户没法只看自己关心的那类。 后端 NotificationService 增加 scene→大类映射(未知 scene 归「系统通知」兜底), 列表接口新增 category 参数,落到 SQL 是 scene IN (...);「系统通知」作兜底类 还要纳入未登记的 scene,故表达为「= system 或 不在其它三类里」。列表项返回 category,前端不必各自维护一份 scene 映射。前后端两个通知接口同步加参数。 前端铃铛筛选区新增一行类型 chip(全部 + 四类),与关键字、日期一起参与 hasFilter 与重置;空分类不下发,避免后端查询落空。
183 lines
5.8 KiB
TypeScript
183 lines
5.8 KiB
TypeScript
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import {
|
|
formatNotificationTime,
|
|
formatUnreadBadge,
|
|
groupNotificationsByDay,
|
|
hasNewNotification,
|
|
readLastNotifiedId,
|
|
writeLastNotifiedId,
|
|
} from '../src/shared/utils/notification-bell.ts'
|
|
import {
|
|
NOTIFICATION_CATEGORY_OPTIONS,
|
|
buildNotificationListQuery,
|
|
} from '../src/shared/api/types/modules/notification.ts'
|
|
|
|
function createStorage() {
|
|
const store = new Map<string, string>()
|
|
return {
|
|
getItem: (key: string) => (store.has(key) ? (store.get(key) as string) : null),
|
|
setItem: (key: string, value: string) => {
|
|
store.set(key, String(value))
|
|
},
|
|
removeItem: (key: string) => {
|
|
store.delete(key)
|
|
},
|
|
}
|
|
}
|
|
|
|
function setupWindow() {
|
|
const localStorage = createStorage()
|
|
;(globalThis as Record<string, unknown>).window = { localStorage }
|
|
return { localStorage }
|
|
}
|
|
|
|
test('formatUnreadBadge 徽标文案:0 与非法值为空、超过 99 显示 99+', () => {
|
|
assert.equal(formatUnreadBadge(0), '')
|
|
assert.equal(formatUnreadBadge(-1), '')
|
|
assert.equal(formatUnreadBadge(null), '')
|
|
assert.equal(formatUnreadBadge(undefined), '')
|
|
assert.equal(formatUnreadBadge(Number.NaN), '')
|
|
assert.equal(formatUnreadBadge(1), '1')
|
|
assert.equal(formatUnreadBadge(99), '99')
|
|
assert.equal(formatUnreadBadge(100), '99+')
|
|
assert.equal(formatUnreadBadge(9999), '99+')
|
|
})
|
|
|
|
test('hasNewNotification 新通知判定:latestId 需大于已提醒 id', () => {
|
|
assert.equal(hasNewNotification(0, 0), false)
|
|
assert.equal(hasNewNotification(null, 0), false)
|
|
assert.equal(hasNewNotification(5, 5), false)
|
|
assert.equal(hasNewNotification(5, 7), false)
|
|
assert.equal(hasNewNotification(5, 3), true)
|
|
assert.equal(hasNewNotification(5, 0), true)
|
|
assert.equal(hasNewNotification(5, null), true)
|
|
})
|
|
|
|
test('已提醒 id 按用户读写并容错', () => {
|
|
setupWindow()
|
|
assert.equal(readLastNotifiedId('42'), 0, '未写入时按 0 处理')
|
|
|
|
writeLastNotifiedId('42', 123)
|
|
assert.equal(readLastNotifiedId('42'), 123)
|
|
assert.equal(readLastNotifiedId('43'), 0, '不同用户互不影响')
|
|
|
|
writeLastNotifiedId('42', 0)
|
|
assert.equal(readLastNotifiedId('42'), 123, '非法 id 不覆盖已有值')
|
|
|
|
window.localStorage.setItem('notification:last-notified-id:42', 'broken')
|
|
assert.equal(readLastNotifiedId('42'), 0, '损坏值按 0 处理')
|
|
})
|
|
|
|
test('formatNotificationTime 今天/昨天/更早展示', () => {
|
|
const now = new Date(2026, 8, 13, 15, 30)
|
|
assert.equal(
|
|
formatNotificationTime(new Date(2026, 8, 13, 9, 5).toISOString(), now),
|
|
'09:05',
|
|
'今天只显示时分',
|
|
)
|
|
assert.equal(
|
|
formatNotificationTime(new Date(2026, 8, 12, 23, 59).toISOString(), now),
|
|
'昨天 23:59',
|
|
'昨天带前缀',
|
|
)
|
|
assert.equal(
|
|
formatNotificationTime(new Date(2026, 8, 1, 8, 0).toISOString(), now),
|
|
'09-01 08:00',
|
|
'更早显示月-日',
|
|
)
|
|
assert.equal(formatNotificationTime(null, now), '')
|
|
assert.equal(formatNotificationTime('not-a-date', now), '')
|
|
})
|
|
|
|
test('groupNotificationsByDay 按年月日分组:今天/昨天/更早,组内保持原顺序', () => {
|
|
const now = new Date(2026, 8, 13, 15, 30)
|
|
const items = [
|
|
{ id: 4, createdAt: new Date(2026, 8, 13, 15, 0).toISOString() },
|
|
{ id: 3, createdAt: new Date(2026, 8, 13, 9, 0).toISOString() },
|
|
{ id: 2, createdAt: new Date(2026, 8, 12, 23, 0).toISOString() },
|
|
{ id: 1, createdAt: new Date(2026, 8, 1, 8, 0).toISOString() },
|
|
]
|
|
|
|
const groups = groupNotificationsByDay(items, now)
|
|
|
|
assert.equal(groups.length, 3)
|
|
assert.deepEqual(
|
|
groups.map((group) => [group.day, group.label]),
|
|
[
|
|
['2026-09-13', '今天'],
|
|
['2026-09-12', '昨天'],
|
|
['2026-09-01', '2026年9月1日'],
|
|
],
|
|
)
|
|
assert.deepEqual(
|
|
groups[0].items.map((item) => item.id),
|
|
[4, 3],
|
|
'同一天的多条合为一组且保持倒序',
|
|
)
|
|
})
|
|
|
|
test('groupNotificationsByDay 时间缺失/非法归入未知时间组', () => {
|
|
const now = new Date(2026, 8, 13, 15, 30)
|
|
const groups = groupNotificationsByDay(
|
|
[
|
|
{ id: 2, createdAt: null },
|
|
{ id: 1, createdAt: 'not-a-date' },
|
|
],
|
|
now,
|
|
)
|
|
|
|
assert.equal(groups.length, 1)
|
|
assert.equal(groups[0].day, '')
|
|
assert.equal(groups[0].label, '未知时间')
|
|
assert.deepEqual(
|
|
groups[0].items.map((item) => item.id),
|
|
[2, 1],
|
|
)
|
|
})
|
|
|
|
test('buildNotificationListQuery:关键字去空白、空条件不下发、分类与日期原样透传', () => {
|
|
assert.deepEqual(buildNotificationListQuery(), { page: 1, pageSize: 20, onlyUnread: 'false' })
|
|
|
|
const query = buildNotificationListQuery({
|
|
page: 2,
|
|
pageSize: 10,
|
|
keyword: ' 余额不足 ',
|
|
category: 'config_error',
|
|
startDate: '2026-09-10',
|
|
endDate: '2026-09-13',
|
|
})
|
|
assert.deepEqual(query, {
|
|
page: 2,
|
|
pageSize: 10,
|
|
onlyUnread: 'false',
|
|
keyword: '余额不足',
|
|
category: 'config_error',
|
|
startDate: '2026-09-10',
|
|
endDate: '2026-09-13',
|
|
})
|
|
|
|
// 面板清空筛选后不能带空串日期(后端日期绑定会 400),空分类同样不下发
|
|
const cleared = buildNotificationListQuery({
|
|
keyword: ' ',
|
|
category: ' ',
|
|
startDate: '',
|
|
endDate: '',
|
|
})
|
|
assert.equal('keyword' in cleared, false)
|
|
assert.equal('category' in cleared, false)
|
|
assert.equal('startDate' in cleared, false)
|
|
assert.equal('endDate' in cleared, false)
|
|
})
|
|
|
|
/** 类型筛选项必须与后端四个大类逐一对应,漏一个用户就永远筛不到那类通知。 */
|
|
test('类型筛选项覆盖后端四个大类且标签非空', () => {
|
|
assert.deepEqual(
|
|
NOTIFICATION_CATEGORY_OPTIONS.map((option) => option.value),
|
|
['system_error', 'task_error', 'config_error', 'system_notice'],
|
|
)
|
|
for (const option of NOTIFICATION_CATEGORY_OPTIONS) {
|
|
assert.ok(option.label.trim().length > 0, `分类 ${option.value} 缺中文标签`)
|
|
}
|
|
})
|