import { flushPromises, mount } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import { createI18n } from 'vue-i18n' import { createMemoryHistory, createRouter } from 'vue-router' import zhCN from '@/i18n/locales/zh-CN' import ReservationOrderDetailView from '@/views/reservation/ReservationOrderDetailView.vue' import ReservationOrderListView from '@/views/reservation/ReservationOrderListView.vue' import ReservationSourceMessageConversationView from '@/views/reservation/ReservationSourceMessageConversationView.vue' import ReservationTaskListView from '@/views/reservation/ReservationTaskListView.vue' import { useAuthStore } from '@/stores/authStore' import type { ReservationTaskListResult, SourceMessageConversationResult } from '@/types/reservation' vi.mock('@/services/reservationService', async (importOriginal) => { const actual = await importOriginal() return { ...actual, fetchReservationOrderDetail: vi.fn(), fetchReservationOrders: vi.fn(), fetchReservationTaskList: vi.fn(), fetchSourceMessageConversation: vi.fn(), } }) const service = await import('@/services/reservationService') function createOrderListResult( displayOrderKey = 'GRP-001', page: { page_num: number; page_size: number; total: number } = { page_num: 1, page_size: 20, total: 1, }, ) { return { items: [ { order_id: displayOrderKey === 'GRP-002' ? '20002' : '20001', hotel_id: 'HOTEL-TEST', order_status: 'ACTIVE', display_order_key: displayOrderKey, temporary_order_no: null, confirmation_number: null, group_code: displayOrderKey, display_name: displayOrderKey, open_task_count: 2, next_processable_task_id: '10002', created_at: '2026-07-08T03:00:00Z', updated_at: '2026-07-08T03:10:00Z', }, ], page, } } function createTaskListResult( displayOrderKey: string, sourceSubject: string, page: { page_num: number; page_size: number; total: number } = { page_num: 1, page_size: 20, total: 1, }, ): ReservationTaskListResult { return { items: [ { task_id: displayOrderKey === 'GRP-NEW' ? '10002' : '10001', order_id: '20001', hotel_id: 'HOTEL-TEST', display_order_key: displayOrderKey, temporary_order_no: null, task_type: 'NEW_BOOKING', task_subtype: 'NEW_BOOKING', task_status: 'PENDING_CONFIRM', card_name: 'New Booking', queue_sequence: 1, queue_participation: true, can_process: true, readonly_reason_code: null, source_message_id: '30001', source_subject: sourceSubject, source_sender_summary: null, source_received_at: null, external_conversation_id: null, conversation_message_count: null, created_at: '2026-07-08T03:00:00Z', updated_at: '2026-07-08T03:10:00Z', }, ], page, } } function createSourceMessageOnlyTaskListResult(taskSubtype: 'S10' | 'S99' = 'S10') { return { items: [ { task_id: taskSubtype === 'S10' ? '19990' : '19999', order_id: taskSubtype === 'S10' ? '29990' : '29999', hotel_id: 'HOTEL-TEST', display_order_key: null, temporary_order_no: null, task_type: 'MESSAGE_NOTIFICATION', task_subtype: taskSubtype, result_type: 'source_message_review_notification', ai_task_type: 'Message Notification', route_code: taskSubtype, system_process_category: 'SOURCE_MESSAGE_NOTIFICATION', task_status: 'COMPLETED', card_name: 'Message Notification', queue_sequence: null, queue_participation: false, can_process: false, readonly_reason_code: 'QUEUE_EXCLUDED', source_message_id: taskSubtype === 'S10' ? '39990' : '39999', source_subject: taskSubtype === 'S10' ? 'FYI only' : 'Unclear email', source_sender_summary: 'guest@example.test', source_received_at: '2026-07-08T03:00:00Z', external_conversation_id: 'thread-source-only', conversation_message_count: 1, created_at: '2026-07-08T03:00:00Z', updated_at: '2026-07-08T03:10:00Z', }, ], page: { page_num: 1, page_size: 20, total: 1, }, } } function createParentGroupCancelAllotmentTaskListResult(): ReservationTaskListResult { const result = createTaskListResult('PARENT-2608-BLOCK', 'Parent split confirmation') result.items[0] = { ...result.items[0]!, task_type: 'CANCEL_BOOKING', task_subtype: 'cancel_allotment_control_block', result_type: 'normal_task', ai_task_type: 'Cancel Allotment', route_code: 'R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_NORMAL', system_process_category: 'BUSINESS_TASK', card_name: 'CANCEL_ALLOTMENT', } return result } function createOrderDetailResult(displayName = 'GRP-001') { return { order: { order_id: displayName === 'GRP-002' ? '20002' : '20001', hotel_id: 'HOTEL-TEST', order_status: 'ACTIVE', temporary_order_no: null, confirmation_number: null, group_code: displayName, block_code: null, allotment_code: null, display_name: displayName, created_at: '2026-07-08T03:00:00Z', updated_at: '2026-07-08T03:10:00Z', }, tasks: [ { task_id: '10001', task_type: 'NEW_BOOKING', task_subtype: 'NEW_BOOKING', task_status: 'PENDING_CONFIRM', card_name: 'New Booking', queue_sequence: 1, queue_participation: true, can_process: true, readonly_reason_code: null, source_message_id: '30001', source_subject: 'Booking Request', source_sender_summary: 'guest@example.test', source_received_at: '2026-07-08T03:00:00Z', external_conversation_id: null, conversation_message_count: 2, created_at: '2026-07-08T03:00:00Z', }, { task_id: '10002', task_type: 'UPDATE_BOOKING', task_subtype: 'RATE_CHANGE', task_status: 'BLOCKED', card_name: 'Rate Change', queue_sequence: 2, queue_participation: true, can_process: false, readonly_reason_code: 'PREVIOUS_TASK_NOT_FINISHED', source_message_id: '30002', source_subject: 'Booking Update', source_sender_summary: 'agent@example.test', source_received_at: '2026-07-08T04:00:00Z', external_conversation_id: null, conversation_message_count: 3, created_at: '2026-07-08T04:00:00Z', }, ], warnings: [], } } function createSourceMessageConversationResult( overrides: Partial = {}, ): SourceMessageConversationResult { const result = createSourceMessageConversationResultBase() return { ...result, ...overrides, } } function createSourceMessageConversationResultBase(): SourceMessageConversationResult { return { conversation: { hotel_id: 'HOTEL-TEST', channel: 'EMAIL', external_conversation_id: 'thread-30001', subject: 'Booking Request', message_count: 2, first_received_at: '2026-07-08T02:00:00Z', last_received_at: '2026-07-08T03:00:00Z', }, messages: [ { id: '30001', external_message_id: 'msg-30001', external_conversation_id: 'thread-30001', sender_summary: 'guest@example.test', subject: 'Booking Request', received_at: '2026-07-08T02:00:00Z', source_sent_at: '2026-07-08T01:58:00Z', text_body: '完整邮件正文', html_body: '原始邮件正文', html_body_sanitized: '清洗后邮件正文', html_sanitize_required: true, html_render_mode: 'SANITIZED_HTML', inline_images: [], attachments: [ { mediaType: 'ATTACHMENT', fileName: 'rooming-list.xlsx', contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', sizeBytes: 1024, externalUrl: 'https://example.test/rooming-list.xlsx', externalMediaId: 'media-1', }, ], related_orders: [ { order_id: '20001', display_order_key: 'GRP-001', order_status: 'ACTIVE', }, ], related_tasks: [ { task_id: '10001', order_id: '20001', task_type: 'NEW_BOOKING', task_subtype: 'NEW_BOOKING', task_status: 'PENDING_CONFIRM', card_name: 'New Booking', }, ], }, ], } } function createAttachment(overrides: Record = {}) { return { mediaType: 'ATTACHMENT', fileName: 'rooming-list.xlsx', contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', sizeBytes: 1024, externalUrl: 'https://example.test/rooming-list.xlsx', externalMediaId: 'media-1', ...overrides, } } function createDeferred() { let resolve!: (value: T) => void let reject!: (reason?: unknown) => void const promise = new Promise((promiseResolve, promiseReject) => { resolve = promiseResolve reject = promiseReject }) return { promise, resolve, reject } } function createAuthPayload() { return { access_token: 'session-token', token_type: 'Bearer', expires_at: '2026-07-09T12:00:00Z', user: { id: '1900000000000000001', username: 'admin', display_name: '系统管理员', super_admin: true, }, default_hotel_id: 'HOTEL-TEST', hotels: [ { hotel_id: 'HOTEL-TEST', hotel_name: '测试酒店', time_zone: 'Asia/Bangkok', default_hotel: true, }, { hotel_id: 'HOTEL-BKK', hotel_name: '曼谷酒店', time_zone: 'Asia/Bangkok', default_hotel: false, }, ], permissions: ['HOTEL_SWITCH'], menus: [], } } async function mountWithPlugins(component: object, initialPath = '/', stubs: Record = {}) { const pinia = createPinia() setActivePinia(pinia) const authStore = useAuthStore() authStore.applyLoginResult(createAuthPayload()) const i18n = createI18n({ legacy: false, locale: 'zh-CN', messages: { 'zh-CN': zhCN, }, }) const router = createRouter({ history: createMemoryHistory(), routes: [ { path: '/', component: { template: '
' } }, { path: '/reservation/orders/:orderId', component: { template: '
' } }, { path: '/reservation/tasks/:taskId', component: { template: '
' } }, { path: '/reservation/source-messages/:sourceMessageId/conversation', component: { template: '
' } }, ], }) await router.push(initialPath) await router.isReady() return mount(component, { global: { plugins: [pinia, i18n, router], stubs: { RouterLink: { template: '', }, ...stubs, }, }, }) } describe('reservation P0 views', () => { beforeEach(() => { sessionStorage.clear() localStorage.clear() vi.mocked(service.fetchReservationOrderDetail).mockReset() vi.mocked(service.fetchReservationOrders).mockReset() vi.mocked(service.fetchReservationTaskList).mockReset() vi.mocked(service.fetchSourceMessageConversation).mockReset() }) it('renders active order task source evidence without the stale pending copy', async () => { vi.mocked(service.fetchReservationOrderDetail).mockResolvedValue(createOrderDetailResult()) const wrapper = await mountWithPlugins(ReservationOrderDetailView, '/reservation/orders/20001', { ReservationTaskDetailPanel: { template: '
', }, }) await vi.dynamicImportSettled() 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('来源邮件摘要字段待后端补充') }) it('renders order list items returned by the backend', async () => { vi.mocked(service.fetchReservationOrders).mockResolvedValue(createOrderListResult()) const wrapper = await mountWithPlugins(ReservationOrderListView) await vi.dynamicImportSettled() expect(wrapper.text()).toContain('GRP-001') expect(wrapper.text()).toContain('有效') expect(wrapper.text()).toContain('继续处理') expect(wrapper.text()).not.toContain('接口待接入') }) it('filters order list with order status and business-key fields', async () => { vi.mocked(service.fetchReservationOrders).mockResolvedValue(createOrderListResult()) const wrapper = await mountWithPlugins(ReservationOrderListView) await vi.dynamicImportSettled() await wrapper.find('select[name="order_status"]').setValue('LOGIC_DELETED') await vi.dynamicImportSettled() await wrapper.find('input[name="group_code"]').setValue('GRP-009') await vi.dynamicImportSettled() await wrapper.find('input[name="confirmation_number"]').setValue('CNF-009') await vi.dynamicImportSettled() expect(service.fetchReservationOrders).toHaveBeenLastCalledWith( expect.objectContaining({ order_status: 'LOGIC_DELETED', group_code: 'GRP-009', confirmation_number: 'CNF-009', page_num: 1, }), ) }) it('paginates order list requests with backend page metadata', async () => { vi.mocked(service.fetchReservationOrders) .mockResolvedValueOnce(createOrderListResult('GRP-001', { page_num: 1, page_size: 20, total: 41 })) .mockResolvedValueOnce(createOrderListResult('GRP-002', { page_num: 2, page_size: 20, total: 41 })) const wrapper = await mountWithPlugins(ReservationOrderListView) await vi.dynamicImportSettled() expect(wrapper.text()).toContain('第 1 / 3 页') await wrapper.findAll('button').find((button) => button.text().includes('下一页'))?.trigger('click') await vi.dynamicImportSettled() expect(service.fetchReservationOrders).toHaveBeenLastCalledWith( expect.objectContaining({ page_num: 2, page_size: 20, }), ) expect(wrapper.text()).toContain('GRP-002') expect(wrapper.text()).toContain('第 2 / 3 页') }) it('renders backend-normalized order pagination metadata', async () => { vi.mocked(service.fetchReservationOrders).mockResolvedValue( createOrderListResult('GRP-001', { page_num: 3, page_size: 10, total: 31 }), ) const wrapper = await mountWithPlugins(ReservationOrderListView) await vi.dynamicImportSettled() expect(wrapper.text()).toContain('第 3 / 4 页') }) it('reloads the order list when the selected hotel changes', async () => { vi.mocked(service.fetchReservationOrders) .mockResolvedValueOnce(createOrderListResult('GRP-001')) .mockResolvedValueOnce(createOrderListResult('GRP-002')) const wrapper = await mountWithPlugins(ReservationOrderListView) await vi.dynamicImportSettled() const authStore = useAuthStore() authStore.setSelectedHotelId('HOTEL-BKK') await vi.dynamicImportSettled() expect(service.fetchReservationOrders).toHaveBeenCalledTimes(2) expect(service.fetchReservationOrders).toHaveBeenLastCalledWith( expect.objectContaining({ page_num: 1, }), ) expect(wrapper.text()).toContain('GRP-002') }) it('renders task list items returned by the backend', async () => { vi.mocked(service.fetchReservationTaskList).mockResolvedValue(createTaskListResult('GRP-001', 'Booking Request')) const wrapper = await mountWithPlugins(ReservationTaskListView) await vi.dynamicImportSettled() 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('renders task source received time in the source email column', async () => { const taskListResult = createTaskListResult('GRP-001', 'Booking Request') taskListResult.items[0] = { ...taskListResult.items[0]!, source_sender_summary: 'guest@example.test', source_received_at: '2026-07-08T03:00:00Z', } vi.mocked(service.fetchReservationTaskList).mockResolvedValue(taskListResult) const wrapper = await mountWithPlugins(ReservationTaskListView) await vi.dynamicImportSettled() expect(wrapper.text()).toContain('来源时间') expect(wrapper.text()).toContain('2026-07-08 10:00:00') }) it('filters source-message-only tasks with S10 and S99 subtypes', async () => { vi.mocked(service.fetchReservationTaskList).mockResolvedValue(createSourceMessageOnlyTaskListResult('S10')) const wrapper = await mountWithPlugins(ReservationTaskListView) await vi.dynamicImportSettled() const taskTypeOptions = wrapper.find('select[name="task_type"]').findAll('option') .map((option) => option.attributes('value')) const taskSubtypeOptions = wrapper.find('select[name="task_subtype"]').findAll('option') .map((option) => option.attributes('value')) expect(taskTypeOptions).toEqual([ '', 'NEW_BOOKING', 'UPDATE_BOOKING', 'CANCEL_BOOKING', 'MANUAL_REVIEW', 'SOURCE_MESSAGE_ONLY', ]) expect(taskTypeOptions).not.toContain('INFORMATIONAL_MESSAGE') expect(taskSubtypeOptions).toEqual([ '', 'NEW_BOOKING', 'UPDATE_BOOKING', 'CANCEL_BOOKING', 'RATE_CHANGE', 'UPDATE_STAY_DATES', 'UPDATE_ROOM_TYPE', 'UPDATE_FIX_CHARGE', 'NEW_GROUP_BLOCK', 'NEW_FIT_RESERVATION', 'MANUAL_REVIEW', 'S10', 'S99', ]) expect(taskSubtypeOptions).not.toContain('S000') expect(taskSubtypeOptions).not.toContain('S999') expect(taskSubtypeOptions).not.toContain('linked_parent_release_after_child_split') expect(wrapper.find('select[name="task_type"]').text()).toContain('来源消息只读') expect(wrapper.find('select[name="task_subtype"]').text()).toContain('纯信息类邮件(S10)') expect(wrapper.find('select[name="task_subtype"]').text()).toContain('无法形成业务素材包(S99)') await wrapper.find('select[name="task_type"]').setValue('SOURCE_MESSAGE_ONLY') await vi.dynamicImportSettled() await wrapper.find('select[name="task_subtype"]').setValue('S99') await vi.dynamicImportSettled() expect(service.fetchReservationTaskList).toHaveBeenLastCalledWith( expect.objectContaining({ task_type: 'SOURCE_MESSAGE_ONLY', task_subtype: 'S99', page_num: 1, }), ) const lastTaskRequest = vi.mocked(service.fetchReservationTaskList).mock.calls[ vi.mocked(service.fetchReservationTaskList).mock.calls.length - 1 ]?.[0] ?? {} expect(Object.keys(lastTaskRequest).sort()).toEqual([ 'keyword', 'order_status', 'page_num', 'page_size', 'queue_participation', 'task_status', 'task_subtype', 'task_type', ]) expect(lastTaskRequest).not.toHaveProperty('result_type') expect(lastTaskRequest).not.toHaveProperty('route_code') expect(lastTaskRequest).not.toHaveProperty('system_process_category') }) it('renders R08 parent group cancel allotment as a business card', async () => { vi.mocked(service.fetchReservationTaskList).mockResolvedValue(createParentGroupCancelAllotmentTaskListResult()) const wrapper = await mountWithPlugins(ReservationTaskListView) await vi.dynamicImportSettled() expect(wrapper.text()).toContain('PARENT-2608-BLOCK') expect(wrapper.text()).toContain('Parent Group') expect(wrapper.text()).toContain('取消 Parent Group 配额') expect(wrapper.text()).toContain('取消 Parent Group 整块配额') expect(wrapper.text()).toContain('查看订单') expect(wrapper.text()).not.toContain('适配契约异常') expect(wrapper.text()).not.toContain('linked_parent_release_after_child_split') }) it('renders source-message-only task rows without a visible order action', async () => { vi.mocked(service.fetchReservationTaskList).mockResolvedValue(createSourceMessageOnlyTaskListResult('S10')) const wrapper = await mountWithPlugins(ReservationTaskListView) await vi.dynamicImportSettled() expect(wrapper.text()).toContain('来源消息只读') expect(wrapper.text()).toContain('纯信息类邮件(S10)') expect(wrapper.text()).toContain('来源消息通知') expect(wrapper.text()).toContain('FYI only') expect(wrapper.text()).toContain('查看任务') expect(wrapper.text()).toContain('查看邮件会话') expect(wrapper.text()).not.toContain('查看订单') expect(wrapper.text()).not.toContain('29990') }) it('does not treat a business task as source-message-only only because subtype is S10', async () => { const result = createTaskListResult('GRP-S10-BUSINESS', 'Business booking request') result.items[0] = { ...result.items[0]!, task_type: 'NEW_BOOKING', task_subtype: 'S10', result_type: 'normal_task', ai_task_type: 'NEW_BOOKING', route_code: 'S10', system_process_category: 'BUSINESS_TASK', card_name: 'New Booking', } vi.mocked(service.fetchReservationTaskList).mockResolvedValue(result) const wrapper = await mountWithPlugins(ReservationTaskListView) await vi.dynamicImportSettled() const rowText = wrapper.find('tbody').text() expect(rowText).toContain('GRP-S10-BUSINESS') expect(rowText).toContain('20001') expect(rowText).toContain('查看订单') expect(rowText).not.toContain('来源消息只读') expect(rowText).not.toContain('不关联可见订单') }) it('filters and paginates task list with supported backend fields', async () => { vi.mocked(service.fetchReservationTaskList) .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-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() expect(wrapper.text()).not.toContain('订单状态筛选待后端接入') expect(wrapper.find('select[name="order_status"]').attributes('disabled')).toBeUndefined() expect(wrapper.find('select[name="task_status"]').text()).toContain('执行中') expect(wrapper.find('select[name="task_status"]').text()).toContain('失败') await wrapper.find('select[name="task_status"]').setValue('FAILED') await vi.dynamicImportSettled() await wrapper.find('select[name="task_subtype"]').setValue('RATE_CHANGE') await vi.dynamicImportSettled() await wrapper.find('select[name="order_status"]').setValue('ENDED') await vi.dynamicImportSettled() await wrapper.find('select[name="queue_participation"]').setValue('true') await vi.dynamicImportSettled() const taskListCalls = vi.mocked(service.fetchReservationTaskList).mock.calls const filteredTaskRequest = taskListCalls[taskListCalls.length - 1]?.[0] ?? {} expect(filteredTaskRequest).toMatchObject({ task_status: 'FAILED', task_subtype: 'RATE_CHANGE', order_status: 'ENDED', queue_participation: true, page_num: 1, }) await wrapper.findAll('button').find((button) => button.text().includes('下一页'))?.trigger('click') await vi.dynamicImportSettled() expect(service.fetchReservationTaskList).toHaveBeenLastCalledWith( expect.objectContaining({ task_status: 'FAILED', task_subtype: 'RATE_CHANGE', order_status: 'ENDED', queue_participation: true, page_num: 2, page_size: 20, }), ) expect(wrapper.text()).toContain('GRP-002') expect(wrapper.text()).toContain('第 2 / 3 页') }) it('renders backend-normalized task pagination metadata', async () => { vi.mocked(service.fetchReservationTaskList).mockResolvedValue( createTaskListResult('GRP-001', 'Booking Request', { page_num: 2, page_size: 10, total: 21 }), ) const wrapper = await mountWithPlugins(ReservationTaskListView) await vi.dynamicImportSettled() expect(wrapper.text()).toContain('第 2 / 3 页') }) it('reloads the task list when the selected hotel changes', async () => { vi.mocked(service.fetchReservationTaskList) .mockResolvedValueOnce(createTaskListResult('GRP-001', 'Booking Request')) .mockResolvedValueOnce(createTaskListResult('GRP-002', 'Booking Update')) const wrapper = await mountWithPlugins(ReservationTaskListView) await vi.dynamicImportSettled() const authStore = useAuthStore() authStore.setSelectedHotelId('HOTEL-BKK') await vi.dynamicImportSettled() expect(service.fetchReservationTaskList).toHaveBeenCalledTimes(2) expect(service.fetchReservationTaskList).toHaveBeenLastCalledWith( expect.objectContaining({ page_num: 1, }), ) expect(wrapper.text()).toContain('Booking Update') }) it('reloads order detail when the selected hotel changes', async () => { vi.mocked(service.fetchReservationOrderDetail) .mockResolvedValueOnce(createOrderDetailResult('GRP-001')) .mockResolvedValueOnce(createOrderDetailResult('GRP-002')) const wrapper = await mountWithPlugins(ReservationOrderDetailView, '/reservation/orders/20001', { ReservationTaskDetailPanel: { template: '
', }, }) await vi.dynamicImportSettled() const authStore = useAuthStore() authStore.setSelectedHotelId('HOTEL-BKK') await vi.dynamicImportSettled() expect(service.fetchReservationOrderDetail).toHaveBeenCalledTimes(2) expect(service.fetchReservationOrderDetail).toHaveBeenLastCalledWith('20001') expect(wrapper.text()).toContain('GRP-002') }) it('keeps the latest task list response when filters change quickly', async () => { const firstRequest = createDeferred>>() const secondRequest = createDeferred>>() vi.mocked(service.fetchReservationTaskList) .mockReturnValueOnce(firstRequest.promise) .mockReturnValueOnce(secondRequest.promise) const wrapper = await mountWithPlugins(ReservationTaskListView) await wrapper.find('input[type="search"]').setValue('fresh') expect(service.fetchReservationTaskList).toHaveBeenCalledTimes(2) secondRequest.resolve(createTaskListResult('GRP-NEW', 'New Booking Request')) await flushPromises() expect(wrapper.text()).toContain('GRP-NEW') firstRequest.resolve(createTaskListResult('GRP-OLD', 'Old Booking Request')) await flushPromises() expect(wrapper.text()).toContain('GRP-NEW') expect(wrapper.text()).not.toContain('GRP-OLD') }) it('renders full source message conversation returned by the backend', async () => { vi.mocked(service.fetchSourceMessageConversation).mockResolvedValue(createSourceMessageConversationResult()) const wrapper = await mountWithPlugins( ReservationSourceMessageConversationView, '/reservation/source-messages/30001/conversation', ) await vi.dynamicImportSettled() expect(service.fetchSourceMessageConversation).toHaveBeenCalledWith('30001') expect(wrapper.text()).toContain('Booking Request') expect(wrapper.text()).toContain('清洗后邮件正文') expect(wrapper.text()).toContain('rooming-list.xlsx') expect(wrapper.find('.message-body').html()).toContain('清洗后邮件正文') expect(wrapper.find('.message-body').html()).not.toContain('