修复V4任务详情来源邮件展示
This commit is contained in:
@@ -11,6 +11,7 @@ import type {
|
||||
ReservationV4OrderTaskDetailResult,
|
||||
ReservationV4SourceNotificationDetailResult,
|
||||
ReservationV4TaskCardResult,
|
||||
SourceMessageConversationResult,
|
||||
} from '@/types/reservation'
|
||||
import ReservationV4OrderTaskDetailView from '@/views/reservation/ReservationV4OrderTaskDetailView.vue'
|
||||
import ReservationV4SourceNotificationDetailView from '@/views/reservation/ReservationV4SourceNotificationDetailView.vue'
|
||||
@@ -27,6 +28,7 @@ vi.mock('@/services/reservationService', async (importOriginal) => {
|
||||
fetchReservationV4RateCodeLookups: vi.fn(),
|
||||
fetchReservationV4RoomTypeLookups: vi.fn(),
|
||||
fetchReservationV4SourceNotificationDetail: vi.fn(),
|
||||
fetchSourceMessageConversation: vi.fn(),
|
||||
resolveReservationV4OrderTaskCardReview: vi.fn(),
|
||||
}
|
||||
})
|
||||
@@ -43,7 +45,9 @@ describe('reservation V4 pages', () => {
|
||||
vi.mocked(service.fetchReservationV4RateCodeLookups).mockReset()
|
||||
vi.mocked(service.fetchReservationV4RoomTypeLookups).mockReset()
|
||||
vi.mocked(service.fetchReservationV4SourceNotificationDetail).mockReset()
|
||||
vi.mocked(service.fetchSourceMessageConversation).mockReset()
|
||||
vi.mocked(service.resolveReservationV4OrderTaskCardReview).mockReset()
|
||||
vi.mocked(service.fetchSourceMessageConversation).mockResolvedValue(createSourceMessageConversationResult())
|
||||
mockCatalogLookups()
|
||||
})
|
||||
|
||||
@@ -150,6 +154,139 @@ describe('reservation V4 pages', () => {
|
||||
expect(wrapper.text()).not.toContain('oss://bucket')
|
||||
})
|
||||
|
||||
it('renders V4 source message display at the bottom with only the current email body collapsed', async () => {
|
||||
const detail = createOrderTaskDetail()
|
||||
detail.source_message_card!.fields = [
|
||||
createField('/source_message/ai_payload_json', {
|
||||
display_name: 'AI payload',
|
||||
value: 'AI_PAYLOAD_SHOULD_NOT_RENDER',
|
||||
}),
|
||||
createField('/source_message/raw_evidence', {
|
||||
display_name: 'Raw evidence',
|
||||
value: 'RAW_EVIDENCE_SHOULD_NOT_RENDER',
|
||||
}),
|
||||
createField('/source_message/attachment_url', {
|
||||
display_name: 'Attachment URL',
|
||||
value: 'https://oss.example/private/source-field.pdf',
|
||||
}),
|
||||
]
|
||||
const currentBody = `Current trigger email body opening. ${'Current booking detail. '.repeat(24)}CURRENT_BODY_TAIL`
|
||||
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
|
||||
vi.mocked(service.fetchSourceMessageConversation).mockResolvedValue(createSourceMessageConversationResult({
|
||||
sourceMessageId: '30001',
|
||||
currentBody,
|
||||
}))
|
||||
|
||||
const wrapper = await mountWithPlugins(
|
||||
ReservationV4OrderTaskDetailView,
|
||||
'/reservation/order-tasks/9001',
|
||||
)
|
||||
await flushPromises()
|
||||
|
||||
expect(service.fetchSourceMessageConversation).toHaveBeenCalledWith('30001')
|
||||
const pageText = wrapper.text()
|
||||
const basicIndex = pageText.indexOf(zhCN.taskV4.basicInformationCard)
|
||||
const businessIndex = pageText.indexOf(zhCN.taskV4.businessCards)
|
||||
const sourceIndex = pageText.indexOf(zhCN.taskV4.sourceMessageCard)
|
||||
expect(basicIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(businessIndex).toBeGreaterThan(basicIndex)
|
||||
expect(sourceIndex).toBeGreaterThan(businessIndex)
|
||||
const sections = wrapper.findAll('.th-section')
|
||||
expect(sections[sections.length - 1]?.text()).toContain(zhCN.taskV4.sourceMessageCard)
|
||||
expect(pageText).toContain('Current trigger email body opening.')
|
||||
expect(pageText).not.toContain('OLDER_MESSAGE_BODY')
|
||||
expect(pageText).not.toContain('REPLY_MESSAGE_BODY')
|
||||
expect(pageText).not.toContain('CURRENT_BODY_TAIL')
|
||||
expect(pageText).not.toContain('AI_PAYLOAD_SHOULD_NOT_RENDER')
|
||||
expect(pageText).not.toContain('RAW_EVIDENCE_SHOULD_NOT_RENDER')
|
||||
expect(pageText).not.toContain('https://oss.example/private/source-field.pdf')
|
||||
|
||||
const expandButton = wrapper
|
||||
.findAll('button')
|
||||
.find((button) => button.text().includes(zhCN.taskV4.sourceMessage.showFullBody))
|
||||
expect(expandButton).toBeTruthy()
|
||||
await expandButton!.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('CURRENT_BODY_TAIL')
|
||||
expect(wrapper.text()).toContain(zhCN.taskV4.sourceMessage.collapseBody)
|
||||
})
|
||||
|
||||
it('falls back to source summary when current source message body cannot be loaded', async () => {
|
||||
const detail = createOrderTaskDetail()
|
||||
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
|
||||
vi.mocked(service.fetchSourceMessageConversation).mockRejectedValue(new ApiError('Forbidden', 403, {
|
||||
code: 'SOURCE_MESSAGE_ORIGINAL_READ_FORBIDDEN',
|
||||
message: 'Forbidden',
|
||||
}))
|
||||
|
||||
const wrapper = await mountWithPlugins(
|
||||
ReservationV4OrderTaskDetailView,
|
||||
'/reservation/order-tasks/9001',
|
||||
)
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('Please book one twin room.')
|
||||
expect(wrapper.text()).toContain(zhCN.taskV4.sourceMessage.bodyLoadFailed)
|
||||
expect(wrapper.text()).toContain(zhCN.task.viewConversation)
|
||||
})
|
||||
|
||||
it('renders only sanitized source message html and never raw html body', async () => {
|
||||
const detail = createOrderTaskDetail()
|
||||
const sanitizedHtml = `<p>Sanitized current email opening. ${'Sanitized detail. '.repeat(24)}SANITIZED_HTML_TAIL</p>`
|
||||
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
|
||||
vi.mocked(service.fetchSourceMessageConversation).mockResolvedValue(createSourceMessageConversationResult({
|
||||
sourceMessageId: '30001',
|
||||
currentBody: '',
|
||||
sanitizedHtml,
|
||||
rawHtml: '<script>RAW_HTML_SHOULD_NOT_RENDER</script>',
|
||||
htmlRenderMode: 'SANITIZED_HTML',
|
||||
}))
|
||||
|
||||
const wrapper = await mountWithPlugins(
|
||||
ReservationV4OrderTaskDetailView,
|
||||
'/reservation/order-tasks/9001',
|
||||
)
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('Sanitized current email opening.')
|
||||
expect(wrapper.text()).not.toContain('SANITIZED_HTML_TAIL')
|
||||
expect(wrapper.text()).not.toContain('RAW_HTML_SHOULD_NOT_RENDER')
|
||||
|
||||
const expandButton = wrapper
|
||||
.findAll('button')
|
||||
.find((button) => button.text().includes(zhCN.taskV4.sourceMessage.showFullBody))
|
||||
expect(expandButton).toBeTruthy()
|
||||
await expandButton!.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('SANITIZED_HTML_TAIL')
|
||||
expect(wrapper.html()).toContain('<p>Sanitized current email opening.')
|
||||
expect(wrapper.html()).not.toContain('RAW_HTML_SHOULD_NOT_RENDER')
|
||||
})
|
||||
|
||||
it('does not render sanitized html when backend marks the email body as text only', async () => {
|
||||
const detail = createOrderTaskDetail()
|
||||
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
|
||||
vi.mocked(service.fetchSourceMessageConversation).mockResolvedValue(createSourceMessageConversationResult({
|
||||
sourceMessageId: '30001',
|
||||
currentBody: 'Plain text fallback body.',
|
||||
sanitizedHtml: '<p>SANITIZED_HTML_SHOULD_NOT_RENDER</p>',
|
||||
rawHtml: '<p>RAW_HTML_SHOULD_NOT_RENDER</p>',
|
||||
htmlRenderMode: 'TEXT_ONLY',
|
||||
}))
|
||||
|
||||
const wrapper = await mountWithPlugins(
|
||||
ReservationV4OrderTaskDetailView,
|
||||
'/reservation/order-tasks/9001',
|
||||
)
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('Plain text fallback body.')
|
||||
expect(wrapper.text()).not.toContain('SANITIZED_HTML_SHOULD_NOT_RENDER')
|
||||
expect(wrapper.text()).not.toContain('RAW_HTML_SHOULD_NOT_RENDER')
|
||||
})
|
||||
|
||||
it('renders New Booking Room Information as a business form and confirms stable final values', async () => {
|
||||
const detail = createOrderTaskDetail({
|
||||
businessEventType: 'NEW_BOOKING',
|
||||
@@ -393,6 +530,8 @@ describe('reservation V4 pages', () => {
|
||||
|
||||
const roomCard = wrapper.find('[data-testid="room-information-card"]')
|
||||
expect(roomCard.text()).toContain(zhCN.taskV4.roomInformation.changeSummary)
|
||||
expect(roomCard.text()).toContain(zhCN.taskV4.roomInformation.currentValues)
|
||||
expect(roomCard.text()).toContain(zhCN.taskV4.roomInformation.proposedValues)
|
||||
expect(roomCard.text()).toContain('2026-07-26')
|
||||
expect(roomCard.text()).toContain('2026-07-27')
|
||||
expect(roomCard.text()).toContain('3')
|
||||
@@ -986,6 +1125,81 @@ function createOrderTaskDetail(options: {
|
||||
}
|
||||
}
|
||||
|
||||
function createSourceMessageConversationResult(options: {
|
||||
sourceMessageId?: string
|
||||
currentBody?: string
|
||||
sanitizedHtml?: string | null
|
||||
rawHtml?: string | null
|
||||
htmlRenderMode?: SourceMessageConversationResult['messages'][number]['html_render_mode']
|
||||
} = {}): SourceMessageConversationResult {
|
||||
const sourceMessageId = options.sourceMessageId ?? '30001'
|
||||
const currentBody = options.currentBody ?? 'Current trigger email body.'
|
||||
return {
|
||||
conversation: {
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
channel: 'EMAIL',
|
||||
external_conversation_id: 'thread-1',
|
||||
subject: 'Booking Request',
|
||||
message_count: 3,
|
||||
first_received_at: '2026-07-08T02:00:00Z',
|
||||
last_received_at: '2026-07-08T04:00:00Z',
|
||||
},
|
||||
messages: [
|
||||
createConversationMessage({
|
||||
id: '29999',
|
||||
text_body: 'OLDER_MESSAGE_BODY',
|
||||
received_at: '2026-07-08T02:00:00Z',
|
||||
}),
|
||||
createConversationMessage({
|
||||
id: sourceMessageId,
|
||||
text_body: currentBody,
|
||||
html_body: options.rawHtml ?? null,
|
||||
html_body_sanitized: options.sanitizedHtml ?? null,
|
||||
html_render_mode: options.htmlRenderMode,
|
||||
received_at: '2026-07-08T03:00:00Z',
|
||||
attachments: [
|
||||
{
|
||||
mediaType: 'ATTACHMENT',
|
||||
fileName: 'current-message.pdf',
|
||||
contentType: 'application/pdf',
|
||||
sizeBytes: 2048,
|
||||
externalUrl: 'https://oss.example/private/current-message.pdf',
|
||||
externalMediaId: 'media-current',
|
||||
},
|
||||
],
|
||||
}),
|
||||
createConversationMessage({
|
||||
id: '30002',
|
||||
text_body: 'REPLY_MESSAGE_BODY',
|
||||
received_at: '2026-07-08T04:00:00Z',
|
||||
}),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function createConversationMessage(
|
||||
overrides: Partial<SourceMessageConversationResult['messages'][number]> & { id: string },
|
||||
): SourceMessageConversationResult['messages'][number] {
|
||||
return {
|
||||
id: overrides.id,
|
||||
external_message_id: overrides.external_message_id ?? `external-${overrides.id}`,
|
||||
external_conversation_id: overrides.external_conversation_id ?? 'thread-1',
|
||||
sender_summary: overrides.sender_summary ?? 'guest@example.test',
|
||||
subject: overrides.subject ?? 'Booking Request',
|
||||
received_at: overrides.received_at ?? '2026-07-08T03:00:00Z',
|
||||
source_sent_at: overrides.source_sent_at ?? null,
|
||||
text_body: overrides.text_body ?? null,
|
||||
html_body: overrides.html_body ?? null,
|
||||
html_body_sanitized: overrides.html_body_sanitized ?? null,
|
||||
html_sanitize_required: overrides.html_sanitize_required ?? false,
|
||||
html_render_mode: overrides.html_render_mode ?? (overrides.html_body_sanitized ? 'SANITIZED_HTML' : 'TEXT_ONLY'),
|
||||
inline_images: overrides.inline_images ?? [],
|
||||
attachments: overrides.attachments ?? [],
|
||||
related_orders: overrides.related_orders ?? [],
|
||||
related_tasks: overrides.related_tasks ?? [],
|
||||
}
|
||||
}
|
||||
|
||||
function createRoomInformationDisplayPayload(overrides: {
|
||||
event_type?: string
|
||||
booking_type?: string
|
||||
|
||||
Reference in New Issue
Block a user