141 lines
5.9 KiB
JavaScript
141 lines
5.9 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict'
|
|
|
|
// Exercises the installed mobile client's real store code without modifying or starting the app.
|
|
// Run: node --test scripts/test-moderation-client.cjs [CLIENT_REPO may be provided in the environment].
|
|
const assert = require('node:assert/strict')
|
|
const fs = require('node:fs')
|
|
const path = require('node:path')
|
|
const vm = require('node:vm')
|
|
const { test } = require('node:test')
|
|
|
|
const clientRoot = process.env.CLIENT_REPO || path.resolve(__dirname, '../../../Friendship-and-chat-uniapp')
|
|
const storePath = path.join(clientRoot, 'src/chat-store/chatStore.js')
|
|
const enumsPath = path.join(clientRoot, 'src/common/enums.js')
|
|
const clone = (value) => value === undefined ? undefined : JSON.parse(JSON.stringify(value))
|
|
const chatInfo = { type: 'PRIVATE', targetId: 7 }
|
|
|
|
function harness() {
|
|
const memory = new Map()
|
|
const context = vm.createContext({
|
|
defineStore: (_name, definition) => definition,
|
|
useUserStore: () => ({ userInfo: { id: 42 } }),
|
|
useFriendStore: () => ({ findFriend: () => null }),
|
|
useGroupStore: () => ({ findGroup: () => null }),
|
|
console: { log() {} },
|
|
uni: {
|
|
setStorageSync: (key, value) => memory.set(key, clone(value)),
|
|
getStorageSync: (key) => clone(memory.get(key)),
|
|
removeStorageSync: (key) => memory.delete(key)
|
|
}
|
|
})
|
|
const enumSource = fs.readFileSync(enumsPath, 'utf8').replace(/export\s*\{[\s\S]*?\}\s*;?\s*$/, '')
|
|
vm.runInContext(enumSource, context, { filename: enumsPath })
|
|
const storeSource = fs.readFileSync(storePath, 'utf8')
|
|
.replace(/^import\s+.*?;?\s*$/gm, '')
|
|
.replace('export default defineStore', 'globalThis.definition = defineStore')
|
|
vm.runInContext(storeSource, context, { filename: storePath })
|
|
const definition = context.definition
|
|
const store = definition.state()
|
|
for (const [key, action] of Object.entries(definition.actions)) store[key] = action.bind(store)
|
|
for (const [key, getter] of Object.entries(definition.getters)) {
|
|
Object.defineProperty(store, key, { get: () => getter(store) })
|
|
}
|
|
return { store, memory }
|
|
}
|
|
|
|
function message(id, content = 'original private content') {
|
|
return { id, type: 0, status: 3, content, sendId: 7, recvId: 42, sendTime: 1000 + id, selfSend: false }
|
|
}
|
|
|
|
function quote(id) {
|
|
return { ...message(id, 'reply text'), quoteMessage: message(101) }
|
|
}
|
|
|
|
function conversation(messages, hotMinIdx = 0) {
|
|
return {
|
|
...chatInfo, showName: 'Sender', messages, hotMinIdx, unreadCount: 2,
|
|
readedMessageIdx: 0, lastContent: 'original private content', lastSendTime: 1101,
|
|
stored: false, hidden: false
|
|
}
|
|
}
|
|
|
|
function recall(selfSend = false) {
|
|
// The persisted event has its own ID; its legacy-client wire projection must use the source ID.
|
|
return { id: 101, content: '101', type: 10, status: 3, sendId: 7, recvId: 42, sendTime: 2000, selfSend }
|
|
}
|
|
|
|
function assertHidden(value, expectedText = '对方撤回了一条消息') {
|
|
assert.equal(value.type, 21)
|
|
assert.equal(value.status, 2)
|
|
assert.equal(value.content, expectedText)
|
|
}
|
|
|
|
for (const { name, split, messages } of [
|
|
{ name: 'hot source and hot quote', split: 0, messages: [message(101), quote(102)] },
|
|
{ name: 'cold source and hot quote', split: 1, messages: [message(101), quote(102)] },
|
|
{ name: 'cold source and cold quote', split: 2, messages: [message(101), quote(102), message(103, 'later message')] }
|
|
]) {
|
|
test(`legacy RECALL hides ${name} and persists both affected partitions`, () => {
|
|
const { store, memory } = harness()
|
|
const chat = conversation(clone(messages), split)
|
|
store.chats = [chat]
|
|
store.saveToStorage(true)
|
|
store.recallMessage(recall(), chatInfo)
|
|
assertHidden(chat.messages[0])
|
|
assertHidden(chat.messages[1].quoteMessage, '引用内容已撤回')
|
|
const cold = memory.get('chats-app-42-PRIVATE-7')?.messages || []
|
|
const hot = memory.get('chats-app-42-PRIVATE-7-hot')?.messages || []
|
|
const persisted = [...cold, ...hot]
|
|
assertHidden(persisted.find((item) => item.id === 101))
|
|
assertHidden(persisted.find((item) => item.id === 102).quoteMessage, '引用内容已撤回')
|
|
assert.equal(chat.unreadCount, 2)
|
|
assert.equal(chat.lastContent, '对方撤回了一条消息')
|
|
})
|
|
}
|
|
|
|
test('repeat notifications with status 3 do not add unread counts or duplicate rows', () => {
|
|
const { store, memory } = harness()
|
|
const chat = conversation([message(101), quote(102)], 1)
|
|
store.chats = [chat]
|
|
store.saveToStorage(true)
|
|
store.recallMessage(recall(), chatInfo)
|
|
store.recallMessage(recall(), chatInfo)
|
|
assert.equal(chat.unreadCount, 2)
|
|
assert.equal(chat.messages.length, 2)
|
|
assert.equal(memory.get('chats-app-42-PRIVATE-7-hot').unreadCount, 2)
|
|
assertHidden(chat.messages[0])
|
|
})
|
|
|
|
test('sender receives the same compatible projection and sees their own recall tip', () => {
|
|
const { store } = harness()
|
|
const chat = conversation([message(101)])
|
|
store.chats = [chat]
|
|
store.recallMessage(recall(true), chatInfo)
|
|
assertHidden(chat.messages[0], '你撤回了一条消息')
|
|
assert.equal(chat.unreadCount, 2)
|
|
})
|
|
|
|
test('offline buffer processing persists hidden source and quotes after refresh', () => {
|
|
const { store, memory } = harness()
|
|
store.initChats({ chats: [conversation([message(101), quote(102)], 2)], privateMsgMaxId: 102 })
|
|
store.setLoading(true)
|
|
store.recallMessage(recall(), chatInfo)
|
|
assert.equal(memory.size, 0)
|
|
store.setLoading(false)
|
|
store.refreshChats()
|
|
assertHidden(store.chats[0].messages[0])
|
|
assertHidden(store.chats[0].messages[1].quoteMessage, '引用内容已撤回')
|
|
const cold = memory.get('chats-app-42-PRIVATE-7').messages
|
|
assertHidden(cold[0])
|
|
assertHidden(cold[1].quoteMessage, '引用内容已撤回')
|
|
assert.equal(store.chats[0].unreadCount, 2)
|
|
})
|
|
|
|
test('unknown conversation is ignored without creating empty chats or unread counts', () => {
|
|
const { store, memory } = harness()
|
|
assert.doesNotThrow(() => store.recallMessage(recall(), chatInfo))
|
|
assert.equal(store.chats.length, 0)
|
|
assert.equal(memory.size, 0)
|
|
})
|