提交一次全量代码

This commit is contained in:
andy
2026-07-09 11:59:34 +08:00
parent 855396e553
commit 9fa608f602
58 changed files with 8439 additions and 116 deletions

View File

@@ -8,6 +8,7 @@ import ReservationOrderDetailView from '@/views/reservation/ReservationOrderDeta
import ReservationOrderListView from '@/views/reservation/ReservationOrderListView.vue'
import ReservationSourceMessageConversationView from '@/views/reservation/ReservationSourceMessageConversationView.vue'
import ReservationTaskListView from '@/views/reservation/ReservationTaskListView.vue'
import type { SourceMessageConversationResult } from '@/types/reservation'
vi.mock('@/services/reservationService', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/services/reservationService')>()
@@ -148,8 +149,8 @@ function createOrderDetailResult() {
}
function createSourceMessageConversationResult(
overrides: Partial<ReturnType<typeof createSourceMessageConversationResultBase>> = {},
) {
overrides: Partial<SourceMessageConversationResult> = {},
): SourceMessageConversationResult {
const result = createSourceMessageConversationResultBase()
return {
...result,
@@ -157,7 +158,7 @@ function createSourceMessageConversationResult(
}
}
function createSourceMessageConversationResultBase() {
function createSourceMessageConversationResultBase(): SourceMessageConversationResult {
return {
conversation: {
hotel_id: 'HOTEL-TEST',
@@ -178,8 +179,10 @@ function createSourceMessageConversationResultBase() {
received_at: '2026-07-08T02:00:00Z',
source_sent_at: '2026-07-08T01:58:00Z',
text_body: '完整邮件正文',
html_body: '<strong>不应直接渲染</strong>',
html_body: '<script>alert("raw")</script><strong onclick="alert(1)">原始邮件正文</strong>',
html_body_sanitized: '<strong>清洗后邮件正文</strong>',
html_sanitize_required: true,
html_render_mode: 'SANITIZED_HTML',
inline_images: [],
attachments: [
{
@@ -289,7 +292,9 @@ describe('reservation P0 views', () => {
expect(wrapper.text()).toContain('Booking Request')
expect(wrapper.text()).toContain('guest@example.test')
expect(wrapper.text()).toContain('2')
expect(wrapper.text()).toContain('前序任务未完成')
expect(wrapper.text()).toContain('查看邮件会话')
expect(wrapper.text()).not.toContain('PREVIOUS_TASK_NOT_FINISHED')
expect(wrapper.text()).not.toContain('来源邮件摘要字段待后端补充')
})
@@ -346,8 +351,10 @@ describe('reservation P0 views', () => {
expect(wrapper.text()).toContain('GRP-001')
expect(wrapper.text()).toContain('Booking Request')
expect(wrapper.text()).toContain('新预订')
expect(wrapper.text()).toContain('查看邮件会话')
expect(wrapper.text()).not.toContain('接口待补')
expect(wrapper.text()).not.toContain('NEW_BOOKING / NEW_BOOKING')
})
it('filters and paginates task list with supported backend fields', async () => {
@@ -355,17 +362,20 @@ describe('reservation P0 views', () => {
.mockResolvedValueOnce(createTaskListResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 }))
.mockResolvedValueOnce(createTaskListResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 }))
.mockResolvedValueOnce(createTaskListResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 }))
.mockResolvedValueOnce(createTaskListResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 }))
.mockResolvedValueOnce(createTaskListResult('GRP-002', 'Booking Update', { page_num: 2, page_size: 20, total: 45 }))
const wrapper = await mountWithPlugins(ReservationTaskListView)
await vi.dynamicImportSettled()
const selects = wrapper.findAll('select')
expect(wrapper.text()).toContain('订单状态筛选待后端接入')
expect(selects[2]!.attributes('disabled')).toBeDefined()
expect(wrapper.text()).not.toContain('订单状态筛选待后端接入')
expect(selects[2]!.attributes('disabled')).toBeUndefined()
await selects[1]!.setValue('READY')
await vi.dynamicImportSettled()
await selects[2]!.setValue('ENDED')
await vi.dynamicImportSettled()
await selects[3]!.setValue('true')
await vi.dynamicImportSettled()
@@ -373,10 +383,10 @@ describe('reservation P0 views', () => {
const filteredTaskRequest = taskListCalls[taskListCalls.length - 1]?.[0] ?? {}
expect(filteredTaskRequest).toMatchObject({
task_status: 'READY',
order_status: 'ENDED',
queue_participation: true,
page_num: 1,
})
expect(filteredTaskRequest).not.toHaveProperty('order_status')
await wrapper.findAll('button').find((button) => button.text().includes('下一页'))?.trigger('click')
await vi.dynamicImportSettled()
@@ -384,6 +394,7 @@ describe('reservation P0 views', () => {
expect(service.fetchReservationTaskList).toHaveBeenLastCalledWith(
expect.objectContaining({
task_status: 'READY',
order_status: 'ENDED',
queue_participation: true,
page_num: 2,
page_size: 20,
@@ -436,12 +447,34 @@ describe('reservation P0 views', () => {
expect(service.fetchSourceMessageConversation).toHaveBeenCalledWith('30001')
expect(wrapper.text()).toContain('Booking Request')
expect(wrapper.text()).toContain('不应直接渲染')
expect(wrapper.text()).toContain('清洗后邮件正文')
expect(wrapper.text()).toContain('rooming-list.xlsx')
expect(wrapper.find('.message-body').html()).toContain('<strong>不应直接渲染</strong>')
expect(wrapper.find('.message-body').html()).toContain('<strong>清洗后邮件正文</strong>')
expect(wrapper.find('.message-body').html()).not.toContain('<script>')
expect(wrapper.find('.message-body').html()).not.toContain('onclick')
expect(wrapper.text()).not.toContain('原始邮件正文')
expect(wrapper.text()).not.toContain('接口待接入')
})
it('falls back to text when sanitized html is unavailable', async () => {
const conversationResult = createSourceMessageConversationResult()
conversationResult.messages[0]!.html_body = '<strong>原始 HTML 不上屏</strong>'
conversationResult.messages[0]!.html_body_sanitized = null
conversationResult.messages[0]!.html_render_mode = 'TEXT_ONLY'
conversationResult.messages[0]!.text_body = '纯文本回退正文'
vi.mocked(service.fetchSourceMessageConversation).mockResolvedValue(conversationResult)
const wrapper = await mountWithPlugins(
ReservationSourceMessageConversationView,
'/reservation/source-messages/30001/conversation',
)
await vi.dynamicImportSettled()
expect(wrapper.text()).toContain('纯文本回退正文')
expect(wrapper.text()).not.toContain('原始 HTML 不上屏')
expect(wrapper.find('.message-body--html').exists()).toBe(false)
})
it('does not expose unsafe attachment URLs as links', async () => {
const conversationResult = createSourceMessageConversationResult()
conversationResult.messages[0]!.attachments = [