提交一次全量代码
This commit is contained in:
@@ -32,9 +32,10 @@ describe('ReservationStatusBadge', () => {
|
||||
expect(wrapper.classes()).toContain('status-badge--info')
|
||||
})
|
||||
|
||||
it('falls back to the raw code for unknown status', () => {
|
||||
it('falls back to a localized unknown status label', () => {
|
||||
const wrapper = mountWithI18n('UNKNOWN_STATUS')
|
||||
|
||||
expect(wrapper.text()).toContain('UNKNOWN_STATUS')
|
||||
expect(wrapper.text()).toContain('未知状态')
|
||||
expect(wrapper.text()).not.toContain('UNKNOWN_STATUS')
|
||||
})
|
||||
})
|
||||
|
||||
33
client/src/tests/i18nLocales.spec.ts
Normal file
33
client/src/tests/i18nLocales.spec.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import enUS from '@/i18n/locales/en-US'
|
||||
import thTH from '@/i18n/locales/th-TH'
|
||||
import zhCN from '@/i18n/locales/zh-CN'
|
||||
|
||||
describe('reservation i18n locales', () => {
|
||||
it('keeps Chinese, English, and Thai locale keys aligned', () => {
|
||||
const zhKeys = collectKeyPaths(zhCN)
|
||||
|
||||
expect(collectKeyPaths(enUS)).toEqual(zhKeys)
|
||||
expect(collectKeyPaths(thTH)).toEqual(zhKeys)
|
||||
})
|
||||
|
||||
it('includes Thai labels for the P0 reservation navigation', () => {
|
||||
expect(thTH.nav.orders).toBe('รายการออเดอร์')
|
||||
expect(thTH.nav.taskQueue).toBe('คิวงาน')
|
||||
expect(thTH.taskList.viewConversation).toBe('ดูเธรดอีเมล')
|
||||
})
|
||||
})
|
||||
|
||||
function collectKeyPaths(value: unknown, prefix = ''): string[] {
|
||||
if (!isRecord(value)) {
|
||||
return [prefix]
|
||||
}
|
||||
return Object.keys(value)
|
||||
.sort()
|
||||
.flatMap((key) => collectKeyPaths(value[key], prefix ? `${prefix}.${key}` : key))
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
70
client/src/tests/reservationAppShell.spec.ts
Normal file
70
client/src/tests/reservationAppShell.spec.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import ReservationAppShell from '@/layouts/ReservationAppShell.vue'
|
||||
import enUS from '@/i18n/locales/en-US'
|
||||
import thTH from '@/i18n/locales/th-TH'
|
||||
import zhCN from '@/i18n/locales/zh-CN'
|
||||
|
||||
vi.mock('@/services/healthService', () => ({
|
||||
fetchBackendHealth: vi.fn().mockResolvedValue({
|
||||
status: 'UP',
|
||||
service: 'test',
|
||||
}),
|
||||
}))
|
||||
|
||||
async function mountShell() {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'zh-CN',
|
||||
fallbackLocale: 'zh-CN',
|
||||
messages: {
|
||||
'zh-CN': zhCN,
|
||||
'en-US': enUS,
|
||||
'th-TH': thTH,
|
||||
},
|
||||
})
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/reservation/orders',
|
||||
component: { template: '<div />' },
|
||||
meta: { titleKey: 'nav.orders' },
|
||||
},
|
||||
{
|
||||
path: '/reservation/tasks',
|
||||
component: { template: '<div />' },
|
||||
meta: { titleKey: 'nav.taskQueue' },
|
||||
},
|
||||
],
|
||||
})
|
||||
await router.push('/reservation/orders')
|
||||
await router.isReady()
|
||||
|
||||
return mount(ReservationAppShell, {
|
||||
global: {
|
||||
plugins: [i18n, router, createPinia()],
|
||||
},
|
||||
slots: {
|
||||
default: '<section />',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('ReservationAppShell', () => {
|
||||
it('switches between Chinese, English, and Thai labels', async () => {
|
||||
const wrapper = await mountShell()
|
||||
|
||||
expect(wrapper.text()).toContain('订单列表')
|
||||
|
||||
await wrapper.findAll('button').find((button) => button.text() === 'ไทย')?.trigger('click')
|
||||
expect(wrapper.text()).toContain('รายการออเดอร์')
|
||||
|
||||
await wrapper.findAll('button').find((button) => button.text() === 'EN')?.trigger('click')
|
||||
expect(wrapper.text()).toContain('Order list')
|
||||
})
|
||||
})
|
||||
33
client/src/tests/reservationDisplay.spec.ts
Normal file
33
client/src/tests/reservationDisplay.spec.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import zhCN from '@/i18n/locales/zh-CN'
|
||||
import {
|
||||
formatReservationActorType,
|
||||
formatReservationReadonlyReason,
|
||||
formatReservationTaskTypeSummary,
|
||||
} from '@/utils/reservationDisplay'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'zh-CN',
|
||||
messages: {
|
||||
'zh-CN': zhCN,
|
||||
},
|
||||
})
|
||||
|
||||
describe('reservationDisplay', () => {
|
||||
it('formats known task type and subtype codes without exposing duplicated raw codes', () => {
|
||||
expect(formatReservationTaskTypeSummary(i18n.global.t, 'NEW_BOOKING', 'NEW_BOOKING')).toBe('新预订')
|
||||
expect(formatReservationTaskTypeSummary(i18n.global.t, 'UPDATE_BOOKING', 'RATE_CHANGE')).toBe('修改预订 / 价格调整')
|
||||
})
|
||||
|
||||
it('formats readonly reason and actor codes with user-facing labels', () => {
|
||||
expect(formatReservationReadonlyReason(i18n.global.t, 'PREVIOUS_TASK_NOT_FINISHED')).toBe('前序任务未完成')
|
||||
expect(formatReservationActorType(i18n.global.t, 'USER')).toBe('人工用户')
|
||||
})
|
||||
|
||||
it('uses localized fallback labels for unknown stable codes', () => {
|
||||
expect(formatReservationReadonlyReason(i18n.global.t, 'SOME_BACKEND_CODE')).toBe('暂不可处理')
|
||||
})
|
||||
})
|
||||
@@ -63,12 +63,13 @@ describe('reservationService real API mode', () => {
|
||||
|
||||
const result = await fetchReservationTaskList({
|
||||
task_status: 'PENDING_CONFIRM',
|
||||
order_status: 'ACTIVE',
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
})
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/reservation/tasks?hotel_id=HOTEL-TEST&task_status=PENDING_CONFIRM&page_num=1&page_size=20',
|
||||
'/api/reservation/tasks?hotel_id=HOTEL-TEST&task_status=PENDING_CONFIRM&order_status=ACTIVE&page_num=1&page_size=20',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
expect(result.items).toHaveLength(1)
|
||||
@@ -204,7 +205,9 @@ describe('reservationService real API mode', () => {
|
||||
source_sent_at: '2026-07-08T01:58:00Z',
|
||||
text_body: '完整邮件正文',
|
||||
html_body: '<p>完整邮件正文</p>',
|
||||
html_body_sanitized: '<p>完整邮件正文</p>',
|
||||
html_sanitize_required: true,
|
||||
html_render_mode: 'SANITIZED_HTML',
|
||||
inline_images: [],
|
||||
attachments: [],
|
||||
related_orders: [
|
||||
|
||||
@@ -259,13 +259,15 @@ describe('ReservationTaskDetailPanel', () => {
|
||||
vi.mocked(service.saveReservationTaskDraft).mockResolvedValue(createMutationResult())
|
||||
|
||||
const wrapper = await mountPanel()
|
||||
expect(wrapper.text()).toContain('TASK_LOADED')
|
||||
expect(wrapper.text()).toContain('任务读取')
|
||||
|
||||
await findButton(wrapper, '保存草稿').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(service.fetchReservationTaskAudits).toHaveBeenCalledTimes(2)
|
||||
expect(wrapper.text()).toContain('DRAFT_SAVED')
|
||||
expect(wrapper.text()).toContain('草稿保存')
|
||||
expect(wrapper.text()).not.toContain('任务读取')
|
||||
expect(wrapper.text()).not.toContain('DRAFT_SAVED')
|
||||
expect(wrapper.text()).not.toContain('TASK_LOADED')
|
||||
})
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ describe('ReservationTaskQueue', () => {
|
||||
])
|
||||
|
||||
expect(wrapper.text()).toContain('不可处理')
|
||||
expect(wrapper.text()).toContain('PREVIOUS_TASK_NOT_FINISHED')
|
||||
expect(wrapper.text()).toContain('前序任务未完成')
|
||||
expect(wrapper.text()).not.toContain('PREVIOUS_TASK_NOT_FINISHED')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user