446 lines
12 KiB
JavaScript
446 lines
12 KiB
JavaScript
/**
|
|
* Property-based tests for exchange request UI functionality
|
|
* **Feature: miniapp-frontend, Property 15: Exchange Request UI**
|
|
* **Validates: Requirements 7.4**
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest'
|
|
import * as fc from 'fast-check'
|
|
|
|
/**
|
|
* Message type constants
|
|
* 与后端 MessageType 枚举保持一致
|
|
*/
|
|
const MessageType = {
|
|
TEXT: 1,
|
|
VOICE: 2,
|
|
IMAGE: 3,
|
|
EXCHANGE_WECHAT: 4,
|
|
EXCHANGE_WECHAT_RESULT: 5,
|
|
EXCHANGE_PHOTO: 6,
|
|
EXCHANGE_PHOTO_RESULT: 7
|
|
}
|
|
|
|
/**
|
|
* Exchange status constants
|
|
*/
|
|
const ExchangeStatus = {
|
|
PENDING: 0,
|
|
ACCEPTED: 1,
|
|
REJECTED: 2
|
|
}
|
|
|
|
/**
|
|
* Helper functions for exchange request UI validation
|
|
* These mirror the validation logic in the chat page
|
|
*/
|
|
|
|
/**
|
|
* Determine if a message is an exchange type message
|
|
* @param {number} messageType - Message type
|
|
* @returns {boolean} Whether the message is an exchange type
|
|
*/
|
|
export function isExchangeMessage(messageType) {
|
|
return messageType === MessageType.EXCHANGE_WECHAT || messageType === MessageType.EXCHANGE_PHOTO
|
|
}
|
|
|
|
/**
|
|
* Determine if exchange action buttons should be shown
|
|
* Property 15: Exchange Request UI
|
|
* @param {Object} message - Message object
|
|
* @param {boolean} message.isMine - Whether the message is sent by current user
|
|
* @param {number} message.messageType - Message type
|
|
* @param {number} message.status - Exchange status
|
|
* @returns {boolean} Whether to show accept/reject buttons
|
|
*/
|
|
export function shouldShowExchangeActions(message) {
|
|
// Only show actions for exchange messages
|
|
if (!isExchangeMessage(message.messageType)) {
|
|
return false
|
|
}
|
|
|
|
// Only show actions for received messages (not mine)
|
|
if (message.isMine) {
|
|
return false
|
|
}
|
|
|
|
// Only show actions when status is pending
|
|
return message.status === ExchangeStatus.PENDING
|
|
}
|
|
|
|
/**
|
|
* Get the exchange title text based on message
|
|
* @param {Object} message - Message object
|
|
* @param {boolean} message.isMine - Whether the message is sent by current user
|
|
* @param {number} message.messageType - Message type
|
|
* @returns {string} Title text
|
|
*/
|
|
export function getExchangeTitle(message) {
|
|
if (message.messageType === MessageType.EXCHANGE_WECHAT) {
|
|
return message.isMine ? '您发起了交换微信请求' : '对方想和您交换微信'
|
|
}
|
|
if (message.messageType === MessageType.EXCHANGE_PHOTO) {
|
|
return message.isMine ? '您发起了交换照片请求' : '对方想和您交换照片'
|
|
}
|
|
return ''
|
|
}
|
|
|
|
/**
|
|
* Get the exchange status text
|
|
* @param {Object} message - Message object
|
|
* @param {boolean} message.isMine - Whether the message is sent by current user
|
|
* @param {number} message.status - Exchange status
|
|
* @returns {string} Status text
|
|
*/
|
|
export function getExchangeStatusText(message) {
|
|
if (message.status === ExchangeStatus.ACCEPTED) {
|
|
return '已接受'
|
|
}
|
|
if (message.status === ExchangeStatus.REJECTED) {
|
|
return '已拒绝'
|
|
}
|
|
if (message.isMine && message.status === ExchangeStatus.PENDING) {
|
|
return '等待对方回应'
|
|
}
|
|
return ''
|
|
}
|
|
|
|
/**
|
|
* Determine if exchange result should be shown
|
|
* @param {Object} message - Message object
|
|
* @returns {boolean} Whether to show exchange result
|
|
*/
|
|
export function shouldShowExchangeResult(message) {
|
|
if (!isExchangeMessage(message.messageType)) {
|
|
return false
|
|
}
|
|
|
|
// Show result when not showing actions
|
|
return !shouldShowExchangeActions(message)
|
|
}
|
|
|
|
// Arbitrary generators for testing
|
|
const messageTypeArb = fc.constantFrom(
|
|
MessageType.TEXT,
|
|
MessageType.VOICE,
|
|
MessageType.IMAGE,
|
|
MessageType.EXCHANGE_WECHAT,
|
|
MessageType.EXCHANGE_PHOTO
|
|
)
|
|
|
|
const exchangeMessageTypeArb = fc.constantFrom(
|
|
MessageType.EXCHANGE_WECHAT,
|
|
MessageType.EXCHANGE_PHOTO
|
|
)
|
|
|
|
const exchangeStatusArb = fc.constantFrom(
|
|
ExchangeStatus.PENDING,
|
|
ExchangeStatus.ACCEPTED,
|
|
ExchangeStatus.REJECTED
|
|
)
|
|
|
|
const messageArb = fc.record({
|
|
id: fc.integer({ min: 1, max: 1000000 }),
|
|
isMine: fc.boolean(),
|
|
messageType: messageTypeArb,
|
|
status: exchangeStatusArb,
|
|
content: fc.string()
|
|
})
|
|
|
|
const exchangeMessageArb = fc.record({
|
|
id: fc.integer({ min: 1, max: 1000000 }),
|
|
isMine: fc.boolean(),
|
|
messageType: exchangeMessageTypeArb,
|
|
status: exchangeStatusArb,
|
|
content: fc.string()
|
|
})
|
|
|
|
describe('Exchange Request UI Property Tests', () => {
|
|
/**
|
|
* Property 15: Exchange Request UI
|
|
* *For any* chat message with exchange type (WeChat or Photo),
|
|
* the message SHALL display accept/reject buttons for the receiver.
|
|
* **Validates: Requirements 7.4**
|
|
*/
|
|
describe('Property 15: Exchange Request UI', () => {
|
|
it('should show exchange actions only for received pending exchange messages', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
exchangeMessageArb,
|
|
(message) => {
|
|
const showActions = shouldShowExchangeActions(message)
|
|
|
|
// Should show actions only when:
|
|
// 1. Not my message (received)
|
|
// 2. Status is pending
|
|
const expectedShowActions = !message.isMine && message.status === ExchangeStatus.PENDING
|
|
|
|
return showActions === expectedShowActions
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should never show exchange actions for sent messages', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.record({
|
|
id: fc.integer({ min: 1, max: 1000000 }),
|
|
isMine: fc.constant(true), // My message
|
|
messageType: exchangeMessageTypeArb,
|
|
status: exchangeStatusArb,
|
|
content: fc.string()
|
|
}),
|
|
(message) => {
|
|
return shouldShowExchangeActions(message) === false
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should never show exchange actions for non-pending messages', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.record({
|
|
id: fc.integer({ min: 1, max: 1000000 }),
|
|
isMine: fc.boolean(),
|
|
messageType: exchangeMessageTypeArb,
|
|
status: fc.constantFrom(ExchangeStatus.ACCEPTED, ExchangeStatus.REJECTED),
|
|
content: fc.string()
|
|
}),
|
|
(message) => {
|
|
return shouldShowExchangeActions(message) === false
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should never show exchange actions for non-exchange messages', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.record({
|
|
id: fc.integer({ min: 1, max: 1000000 }),
|
|
isMine: fc.boolean(),
|
|
messageType: fc.constantFrom(MessageType.TEXT, MessageType.VOICE, MessageType.IMAGE),
|
|
status: exchangeStatusArb,
|
|
content: fc.string()
|
|
}),
|
|
(message) => {
|
|
return shouldShowExchangeActions(message) === false
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should show exchange actions for received pending exchange messages', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.record({
|
|
id: fc.integer({ min: 1, max: 1000000 }),
|
|
isMine: fc.constant(false), // Received message
|
|
messageType: exchangeMessageTypeArb,
|
|
status: fc.constant(ExchangeStatus.PENDING),
|
|
content: fc.string()
|
|
}),
|
|
(message) => {
|
|
return shouldShowExchangeActions(message) === true
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
})
|
|
|
|
/**
|
|
* Exchange title text tests
|
|
*/
|
|
describe('Exchange Title Text', () => {
|
|
it('should return correct title for WeChat exchange messages', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.boolean(), // isMine
|
|
(isMine) => {
|
|
const message = {
|
|
isMine,
|
|
messageType: MessageType.EXCHANGE_WECHAT
|
|
}
|
|
const title = getExchangeTitle(message)
|
|
|
|
if (isMine) {
|
|
return title === '您发起了交换微信请求'
|
|
} else {
|
|
return title === '对方想和您交换微信'
|
|
}
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should return correct title for Photo exchange messages', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.boolean(), // isMine
|
|
(isMine) => {
|
|
const message = {
|
|
isMine,
|
|
messageType: MessageType.EXCHANGE_PHOTO
|
|
}
|
|
const title = getExchangeTitle(message)
|
|
|
|
if (isMine) {
|
|
return title === '您发起了交换照片请求'
|
|
} else {
|
|
return title === '对方想和您交换照片'
|
|
}
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should return empty string for non-exchange messages', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.boolean(),
|
|
fc.constantFrom(MessageType.TEXT, MessageType.VOICE, MessageType.IMAGE),
|
|
(isMine, messageType) => {
|
|
const message = { isMine, messageType }
|
|
return getExchangeTitle(message) === ''
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
})
|
|
|
|
/**
|
|
* Exchange status text tests
|
|
*/
|
|
describe('Exchange Status Text', () => {
|
|
it('should return "已接受" for accepted status', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.boolean(), // isMine
|
|
(isMine) => {
|
|
const message = {
|
|
isMine,
|
|
status: ExchangeStatus.ACCEPTED
|
|
}
|
|
return getExchangeStatusText(message) === '已接受'
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should return "已拒绝" for rejected status', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.boolean(), // isMine
|
|
(isMine) => {
|
|
const message = {
|
|
isMine,
|
|
status: ExchangeStatus.REJECTED
|
|
}
|
|
return getExchangeStatusText(message) === '已拒绝'
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should return "等待对方回应" for sent pending messages', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.constant(true), // isMine = true
|
|
fc.constant(ExchangeStatus.PENDING),
|
|
(isMine, status) => {
|
|
const message = { isMine, status }
|
|
return getExchangeStatusText(message) === '等待对方回应'
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should return empty string for received pending messages', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.constant(false), // isMine = false
|
|
fc.constant(ExchangeStatus.PENDING),
|
|
(isMine, status) => {
|
|
const message = { isMine, status }
|
|
return getExchangeStatusText(message) === ''
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
})
|
|
|
|
/**
|
|
* Exchange result display tests
|
|
*/
|
|
describe('Exchange Result Display', () => {
|
|
it('should show result when not showing actions for exchange messages', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
exchangeMessageArb,
|
|
(message) => {
|
|
const showActions = shouldShowExchangeActions(message)
|
|
const showResult = shouldShowExchangeResult(message)
|
|
|
|
// For exchange messages, either actions or result should be shown, but not both
|
|
return showActions !== showResult
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should not show result for non-exchange messages', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.record({
|
|
id: fc.integer({ min: 1, max: 1000000 }),
|
|
isMine: fc.boolean(),
|
|
messageType: fc.constantFrom(MessageType.TEXT, MessageType.VOICE, MessageType.IMAGE),
|
|
status: exchangeStatusArb,
|
|
content: fc.string()
|
|
}),
|
|
(message) => {
|
|
return shouldShowExchangeResult(message) === false
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
})
|
|
|
|
/**
|
|
* Combined property: UI consistency
|
|
*/
|
|
describe('UI Consistency', () => {
|
|
it('should have mutually exclusive actions and result display for exchange messages', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
exchangeMessageArb,
|
|
(message) => {
|
|
const showActions = shouldShowExchangeActions(message)
|
|
const showResult = shouldShowExchangeResult(message)
|
|
|
|
// Exactly one should be true for exchange messages
|
|
return (showActions && !showResult) || (!showActions && showResult)
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
})
|
|
})
|