接入前端P0页面真实接口
This commit is contained in:
40
client/src/tests/ReservationStatusBadge.spec.ts
Normal file
40
client/src/tests/ReservationStatusBadge.spec.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import ReservationStatusBadge from '@/components/reservation/ReservationStatusBadge.vue'
|
||||
import zhCN from '@/i18n/locales/zh-CN'
|
||||
|
||||
function mountWithI18n(status: string) {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'zh-CN',
|
||||
messages: {
|
||||
'zh-CN': zhCN,
|
||||
},
|
||||
})
|
||||
|
||||
return mount(ReservationStatusBadge, {
|
||||
props: {
|
||||
status,
|
||||
},
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('ReservationStatusBadge', () => {
|
||||
it('renders stable status code through i18n label', () => {
|
||||
const wrapper = mountWithI18n('PENDING_CONFIRM')
|
||||
|
||||
expect(wrapper.text()).toContain('待确认')
|
||||
expect(wrapper.classes()).toContain('status-badge--info')
|
||||
})
|
||||
|
||||
it('falls back to the raw code for unknown status', () => {
|
||||
const wrapper = mountWithI18n('UNKNOWN_STATUS')
|
||||
|
||||
expect(wrapper.text()).toContain('UNKNOWN_STATUS')
|
||||
})
|
||||
})
|
||||
110
client/src/tests/ReservationTaskFieldRenderer.spec.ts
Normal file
110
client/src/tests/ReservationTaskFieldRenderer.spec.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import ReservationTaskFieldRenderer from '@/components/reservation/ReservationTaskFieldRenderer.vue'
|
||||
import zhCN from '@/i18n/locales/zh-CN'
|
||||
import type { ReservationTaskFieldResult } from '@/types/reservation'
|
||||
|
||||
const fields: ReservationTaskFieldResult[] = [
|
||||
{
|
||||
row_number: 1,
|
||||
card_name: 'New Booking',
|
||||
display_area: '客人信息',
|
||||
field_path: 'guest.name',
|
||||
display_name: '客人姓名',
|
||||
visible: 'Y',
|
||||
editable: 'Y',
|
||||
input_editable: 'Y',
|
||||
select_editable: 'N',
|
||||
date_picker: 'N',
|
||||
number_input: 'N',
|
||||
file_display: 'N',
|
||||
table_editable: 'N',
|
||||
enum_options: null,
|
||||
required_rule: 'Y',
|
||||
display_condition: null,
|
||||
validation_rule: null,
|
||||
write_path: 'reservation.guest.name',
|
||||
opera_write_participation: 'Y',
|
||||
opera_parameter_mapping: 'profile.name',
|
||||
notes: null,
|
||||
value: '王建国',
|
||||
},
|
||||
{
|
||||
row_number: 2,
|
||||
card_name: 'New Booking',
|
||||
display_area: '预订信息',
|
||||
field_path: 'room.type',
|
||||
display_name: '房型',
|
||||
visible: 'Y',
|
||||
editable: 'Y',
|
||||
input_editable: 'N',
|
||||
select_editable: 'Y',
|
||||
date_picker: 'N',
|
||||
number_input: 'N',
|
||||
file_display: 'N',
|
||||
table_editable: 'N',
|
||||
enum_options: '豪华 Q1A, 行政套房',
|
||||
required_rule: 'Y',
|
||||
display_condition: null,
|
||||
validation_rule: null,
|
||||
write_path: 'reservation.room.type',
|
||||
opera_write_participation: 'Y',
|
||||
opera_parameter_mapping: 'roomType',
|
||||
notes: null,
|
||||
value: '豪华 Q1A',
|
||||
},
|
||||
]
|
||||
|
||||
function mountRenderer(readOnly = false) {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'zh-CN',
|
||||
messages: {
|
||||
'zh-CN': zhCN,
|
||||
},
|
||||
})
|
||||
|
||||
return mount(ReservationTaskFieldRenderer, {
|
||||
props: {
|
||||
fields,
|
||||
modelValue: {
|
||||
'guest.name': '王建国',
|
||||
'room.type': '豪华 Q1A',
|
||||
},
|
||||
readOnly,
|
||||
},
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('ReservationTaskFieldRenderer', () => {
|
||||
it('emits updates using field_path as key', async () => {
|
||||
const wrapper = mountRenderer()
|
||||
const input = wrapper.find('textarea')
|
||||
|
||||
await input.setValue('Mark Lee')
|
||||
|
||||
expect(wrapper.emitted('update:modelValue')?.[0]?.[0]).toMatchObject({
|
||||
'guest.name': 'Mark Lee',
|
||||
'room.type': '豪华 Q1A',
|
||||
})
|
||||
})
|
||||
|
||||
it('renders editable select from enum options', () => {
|
||||
const wrapper = mountRenderer()
|
||||
|
||||
expect(wrapper.find('select').exists()).toBe(true)
|
||||
expect(wrapper.findAll('option')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('renders static values when the task is read only', () => {
|
||||
const wrapper = mountRenderer(true)
|
||||
|
||||
expect(wrapper.find('textarea').exists()).toBe(false)
|
||||
expect(wrapper.text()).toContain('王建国')
|
||||
})
|
||||
})
|
||||
70
client/src/tests/reservationOperaPanel.spec.ts
Normal file
70
client/src/tests/reservationOperaPanel.spec.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import ReservationOperaPanel from '@/components/reservation/ReservationOperaPanel.vue'
|
||||
import zhCN from '@/i18n/locales/zh-CN'
|
||||
import type { ReservationOperaOperationResult } from '@/types/reservation'
|
||||
|
||||
vi.mock('@/services/reservationService', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/services/reservationService')>()
|
||||
return {
|
||||
...actual,
|
||||
executeReservationOperaOperation: vi.fn(),
|
||||
retryReservationOperaOperation: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
const service = await import('@/services/reservationService')
|
||||
|
||||
function createOperation(): ReservationOperaOperationResult {
|
||||
return {
|
||||
operation_id: 'op-1',
|
||||
task_id: '10001',
|
||||
operation_sequence: 1,
|
||||
operation_code: 'CREATE_RESERVATION',
|
||||
operation_name: 'Create reservation',
|
||||
operation_status: 'PENDING',
|
||||
attempt_count: 0,
|
||||
last_error_message: null,
|
||||
attempts: [],
|
||||
}
|
||||
}
|
||||
|
||||
function mountPanel() {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'zh-CN',
|
||||
messages: {
|
||||
'zh-CN': zhCN,
|
||||
},
|
||||
})
|
||||
return mount(ReservationOperaPanel, {
|
||||
props: {
|
||||
taskId: '10001',
|
||||
operations: [createOperation()],
|
||||
executable: true,
|
||||
},
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('ReservationOperaPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(service.executeReservationOperaOperation).mockReset()
|
||||
vi.mocked(service.retryReservationOperaOperation).mockReset()
|
||||
})
|
||||
|
||||
it('shows operation errors when execute fails', async () => {
|
||||
vi.mocked(service.executeReservationOperaOperation).mockRejectedValue(new Error('OPERA is unavailable'))
|
||||
|
||||
const wrapper = mountPanel()
|
||||
await wrapper.find('button[title="执行"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('OPERA is unavailable')
|
||||
expect(wrapper.find('button[title="执行"]').attributes('disabled')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
15
client/src/tests/reservationRouter.spec.ts
Normal file
15
client/src/tests/reservationRouter.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { router } from '@/router'
|
||||
|
||||
describe('reservation router', () => {
|
||||
it('exposes the P0 frontend routes', () => {
|
||||
expect(router.resolve('/reservation/orders').name).toBe('reservation-order-list')
|
||||
expect(router.resolve('/reservation/tasks').name).toBe('reservation-task-list')
|
||||
expect(router.resolve('/reservation/orders/20001').name).toBe('reservation-order-detail')
|
||||
expect(router.resolve('/reservation/tasks/10001').name).toBe('reservation-task-detail')
|
||||
expect(router.resolve('/reservation/source-messages/30001/conversation').name).toBe(
|
||||
'reservation-source-message-conversation',
|
||||
)
|
||||
})
|
||||
})
|
||||
151
client/src/tests/reservationService.spec.ts
Normal file
151
client/src/tests/reservationService.spec.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
EndpointPendingError,
|
||||
fetchReservationOrderDetail,
|
||||
fetchReservationOrders,
|
||||
fetchReservationTaskDetail,
|
||||
fetchReservationTaskList,
|
||||
fetchSourceMessageConversation,
|
||||
} from '@/services/reservationService'
|
||||
|
||||
const jsonHeaders = {
|
||||
headers: {
|
||||
get: (name: string) => (name.toLowerCase() === 'content-type' ? 'application/json' : null),
|
||||
},
|
||||
}
|
||||
|
||||
function mockJsonResponse(payload: unknown): Response {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => payload,
|
||||
text: async () => JSON.stringify(payload),
|
||||
...jsonHeaders,
|
||||
} as Response
|
||||
}
|
||||
|
||||
describe('reservationService real API mode', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('fetches the task list from the backend by default', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
items: [
|
||||
{
|
||||
task_id: '10001',
|
||||
order_id: '20001',
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
display_order_key: 'GRP-001',
|
||||
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: 'Booking Request',
|
||||
created_at: '2026-07-08T03:00:00Z',
|
||||
updated_at: '2026-07-08T03:10:00Z',
|
||||
},
|
||||
],
|
||||
page: {
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
total: 1,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const result = await fetchReservationTaskList({
|
||||
task_status: 'PENDING_CONFIRM',
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
})
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/reservation/tasks?task_status=PENDING_CONFIRM&page_num=1&page_size=20',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
expect(result.items).toHaveLength(1)
|
||||
expect(result.items[0]?.source_subject).toBe('Booking Request')
|
||||
})
|
||||
|
||||
it('fetches order detail from the backend by default', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
order: {
|
||||
order_id: '20001',
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
order_status: 'ACTIVE',
|
||||
temporary_order_no: null,
|
||||
confirmation_number: 'CNF123456',
|
||||
group_code: 'GRP-001',
|
||||
block_code: null,
|
||||
allotment_code: null,
|
||||
display_name: 'GRP-001',
|
||||
created_at: '2026-07-08T03:00:00Z',
|
||||
updated_at: '2026-07-08T03:10:00Z',
|
||||
},
|
||||
tasks: [],
|
||||
warnings: [],
|
||||
}),
|
||||
)
|
||||
|
||||
const result = await fetchReservationOrderDetail('20001')
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/reservation/orders/20001?include_tasks=true&include_source_summary=true',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
expect(result.order.display_name).toBe('GRP-001')
|
||||
})
|
||||
|
||||
it('fetches task detail from the backend by default', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
task_id: '10001',
|
||||
order_id: '20001',
|
||||
source_message_id: '30001',
|
||||
system_task_type: 'NEW_BOOKING',
|
||||
task_card_type: 'NEW_BOOKING',
|
||||
task_status: 'PENDING_CONFIRM',
|
||||
field_contract_version: '20260708-3.0',
|
||||
draft_payload: null,
|
||||
confirmed_payload: null,
|
||||
availability: {
|
||||
blocked: false,
|
||||
read_only: false,
|
||||
editable: true,
|
||||
confirmable: true,
|
||||
executable: false,
|
||||
blocked_by_task_id: null,
|
||||
blocked_reason: null,
|
||||
},
|
||||
fields: [],
|
||||
opera_operations: [],
|
||||
}),
|
||||
)
|
||||
|
||||
const result = await fetchReservationTaskDetail('10001')
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/reservation/tasks/10001',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
expect(result.task_id).toBe('10001')
|
||||
})
|
||||
|
||||
it('marks order list as pending instead of returning fixture data', async () => {
|
||||
await expect(fetchReservationOrders()).rejects.toBeInstanceOf(EndpointPendingError)
|
||||
})
|
||||
|
||||
it('marks source message conversation as pending instead of returning fixture data', async () => {
|
||||
await expect(fetchSourceMessageConversation('30001')).rejects.toBeInstanceOf(EndpointPendingError)
|
||||
})
|
||||
})
|
||||
183
client/src/tests/reservationTaskDetailPanel.spec.ts
Normal file
183
client/src/tests/reservationTaskDetailPanel.spec.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||
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 ReservationTaskDetailPanel from '@/components/reservation/ReservationTaskDetailPanel.vue'
|
||||
import type {
|
||||
ReservationTaskAuditLogResult,
|
||||
ReservationTaskDetailResult,
|
||||
ReservationTaskPayloadMutationResult,
|
||||
} from '@/types/reservation'
|
||||
|
||||
vi.mock('@/services/reservationService', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/services/reservationService')>()
|
||||
return {
|
||||
...actual,
|
||||
fetchReservationTaskDetail: vi.fn(),
|
||||
fetchReservationTaskAudits: vi.fn(),
|
||||
saveReservationTaskDraft: vi.fn(),
|
||||
confirmReservationTask: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
const service = await import('@/services/reservationService')
|
||||
|
||||
function createTaskDetail(): ReservationTaskDetailResult {
|
||||
return {
|
||||
task_id: '10001',
|
||||
order_id: '20001',
|
||||
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: 'thread-30001',
|
||||
conversation_message_count: 2,
|
||||
system_task_type: 'NEW_BOOKING',
|
||||
task_card_type: 'NEW_BOOKING',
|
||||
task_status: 'PENDING_CONFIRM',
|
||||
field_contract_version: '20260708-3.0',
|
||||
draft_payload: null,
|
||||
confirmed_payload: null,
|
||||
availability: {
|
||||
blocked: false,
|
||||
read_only: false,
|
||||
editable: true,
|
||||
confirmable: true,
|
||||
executable: false,
|
||||
blocked_by_task_id: null,
|
||||
blocked_reason: null,
|
||||
},
|
||||
fields: [],
|
||||
opera_operations: [],
|
||||
}
|
||||
}
|
||||
|
||||
function createMutationResult(taskStatus = 'PENDING_CONFIRM'): ReservationTaskPayloadMutationResult {
|
||||
return {
|
||||
task_id: '10001',
|
||||
order_id: '20001',
|
||||
task_status: taskStatus,
|
||||
draft_payload: {},
|
||||
confirmed_payload: taskStatus === 'READY' ? {} : null,
|
||||
opera_operations: [],
|
||||
}
|
||||
}
|
||||
|
||||
function createAudit(auditId: string, action: string): ReservationTaskAuditLogResult {
|
||||
return {
|
||||
audit_id: auditId,
|
||||
order_id: '20001',
|
||||
task_id: '10001',
|
||||
operation_id: null,
|
||||
actor_type: 'USER',
|
||||
actor_id: 'andy',
|
||||
action,
|
||||
reason: null,
|
||||
before_snapshot: null,
|
||||
after_snapshot: null,
|
||||
occurred_at: '2026-07-08T03:00:00Z',
|
||||
}
|
||||
}
|
||||
|
||||
async function mountPanel() {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'zh-CN',
|
||||
messages: {
|
||||
'zh-CN': zhCN,
|
||||
},
|
||||
})
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/reservation/tasks/:taskId', component: { template: '<div />' } },
|
||||
{ path: '/reservation/orders/:orderId', component: { template: '<div />' } },
|
||||
{ path: '/reservation/source-messages/:sourceMessageId/conversation', component: { template: '<div />' } },
|
||||
],
|
||||
})
|
||||
await router.push('/reservation/tasks/10001')
|
||||
await router.isReady()
|
||||
|
||||
const wrapper = mount(ReservationTaskDetailPanel, {
|
||||
props: {
|
||||
taskId: '10001',
|
||||
},
|
||||
global: {
|
||||
plugins: [i18n, router],
|
||||
stubs: {
|
||||
RouterLink: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
await flushPromises()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
function findButton(wrapper: VueWrapper, label: string) {
|
||||
const button = wrapper.findAll('button').find((item) => item.text().includes(label))
|
||||
if (!button) {
|
||||
throw new Error(`Button ${label} was not found.`)
|
||||
}
|
||||
return button
|
||||
}
|
||||
|
||||
describe('ReservationTaskDetailPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(service.fetchReservationTaskDetail).mockReset()
|
||||
vi.mocked(service.fetchReservationTaskAudits).mockReset()
|
||||
vi.mocked(service.saveReservationTaskDraft).mockReset()
|
||||
vi.mocked(service.confirmReservationTask).mockReset()
|
||||
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(createTaskDetail())
|
||||
vi.mocked(service.fetchReservationTaskAudits).mockResolvedValue({
|
||||
task_id: '10001',
|
||||
items: [],
|
||||
})
|
||||
})
|
||||
|
||||
it('shows draft save errors instead of leaving an unhandled rejection', async () => {
|
||||
vi.mocked(service.saveReservationTaskDraft).mockRejectedValue(new Error('Draft save failed'))
|
||||
|
||||
const wrapper = await mountPanel()
|
||||
await findButton(wrapper, '保存草稿').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('Draft save failed')
|
||||
expect(findButton(wrapper, '保存草稿').attributes('disabled')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('shows confirm errors instead of leaving an unhandled rejection', async () => {
|
||||
vi.mocked(service.confirmReservationTask).mockRejectedValue(new Error('Confirm failed'))
|
||||
|
||||
const wrapper = await mountPanel()
|
||||
await findButton(wrapper, '确认').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('Confirm failed')
|
||||
expect(findButton(wrapper, '确认').attributes('disabled')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('refreshes audit records after saving a draft', async () => {
|
||||
vi.mocked(service.fetchReservationTaskAudits)
|
||||
.mockResolvedValueOnce({
|
||||
task_id: '10001',
|
||||
items: [createAudit('audit-1', 'TASK_LOADED')],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
task_id: '10001',
|
||||
items: [createAudit('audit-2', 'DRAFT_SAVED')],
|
||||
})
|
||||
vi.mocked(service.saveReservationTaskDraft).mockResolvedValue(createMutationResult())
|
||||
|
||||
const wrapper = await mountPanel()
|
||||
expect(wrapper.text()).toContain('TASK_LOADED')
|
||||
|
||||
await findButton(wrapper, '保存草稿').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(service.fetchReservationTaskAudits).toHaveBeenCalledTimes(2)
|
||||
expect(wrapper.text()).toContain('DRAFT_SAVED')
|
||||
expect(wrapper.text()).not.toContain('TASK_LOADED')
|
||||
})
|
||||
})
|
||||
165
client/src/tests/reservationViews.spec.ts
Normal file
165
client/src/tests/reservationViews.spec.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
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 { EndpointPendingError } from '@/services/reservationService'
|
||||
import ReservationOrderListView from '@/views/reservation/ReservationOrderListView.vue'
|
||||
import ReservationSourceMessageConversationView from '@/views/reservation/ReservationSourceMessageConversationView.vue'
|
||||
import ReservationTaskListView from '@/views/reservation/ReservationTaskListView.vue'
|
||||
|
||||
vi.mock('@/services/reservationService', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/services/reservationService')>()
|
||||
return {
|
||||
...actual,
|
||||
fetchReservationOrders: vi.fn(),
|
||||
fetchReservationTaskList: vi.fn(),
|
||||
fetchSourceMessageConversation: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
const service = await import('@/services/reservationService')
|
||||
|
||||
function createTaskListResult(displayOrderKey: string, sourceSubject: string) {
|
||||
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: {
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
total: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createDeferred<T>() {
|
||||
let resolve!: (value: T) => void
|
||||
let reject!: (reason?: unknown) => void
|
||||
const promise = new Promise<T>((promiseResolve, promiseReject) => {
|
||||
resolve = promiseResolve
|
||||
reject = promiseReject
|
||||
})
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
|
||||
async function mountWithPlugins(component: object, initialPath = '/') {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'zh-CN',
|
||||
messages: {
|
||||
'zh-CN': zhCN,
|
||||
},
|
||||
})
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div />' } },
|
||||
{ path: '/reservation/orders/:orderId', component: { template: '<div />' } },
|
||||
{ path: '/reservation/tasks/:taskId', component: { template: '<div />' } },
|
||||
{ path: '/reservation/source-messages/:sourceMessageId/conversation', component: { template: '<div />' } },
|
||||
],
|
||||
})
|
||||
await router.push(initialPath)
|
||||
await router.isReady()
|
||||
|
||||
return mount(component, {
|
||||
global: {
|
||||
plugins: [i18n, router],
|
||||
stubs: {
|
||||
RouterLink: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('reservation P0 views', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(service.fetchReservationOrders).mockReset()
|
||||
vi.mocked(service.fetchReservationTaskList).mockReset()
|
||||
vi.mocked(service.fetchSourceMessageConversation).mockReset()
|
||||
})
|
||||
|
||||
it('shows pending state for the order list endpoint', async () => {
|
||||
vi.mocked(service.fetchReservationOrders).mockRejectedValue(
|
||||
new EndpointPendingError('GET /api/reservation/orders is pending backend implementation.'),
|
||||
)
|
||||
|
||||
const wrapper = await mountWithPlugins(ReservationOrderListView)
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
expect(wrapper.text()).toContain('接口待接入')
|
||||
expect(wrapper.text()).not.toContain('#ORD-083')
|
||||
})
|
||||
|
||||
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('接口待补')
|
||||
})
|
||||
|
||||
it('keeps the latest task list response when filters change quickly', async () => {
|
||||
const firstRequest = createDeferred<Awaited<ReturnType<typeof service.fetchReservationTaskList>>>()
|
||||
const secondRequest = createDeferred<Awaited<ReturnType<typeof service.fetchReservationTaskList>>>()
|
||||
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('shows pending state for the source message conversation endpoint', async () => {
|
||||
vi.mocked(service.fetchSourceMessageConversation).mockRejectedValue(
|
||||
new EndpointPendingError('GET /api/source-messages/30001/conversation is pending backend implementation.'),
|
||||
)
|
||||
|
||||
const wrapper = await mountWithPlugins(
|
||||
ReservationSourceMessageConversationView,
|
||||
'/reservation/source-messages/30001/conversation',
|
||||
)
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
expect(service.fetchSourceMessageConversation).toHaveBeenCalledWith('30001')
|
||||
expect(wrapper.text()).toContain('接口待接入')
|
||||
expect(wrapper.text()).not.toContain('完整邮件正文')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user