新增房表生成前端页面
This commit is contained in:
1 parent
d3a52e7605
commit
0715e8334f
16 files changed
+1694
-5
No files matched your search
@@ -0,0 +1,165 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { clearStoredAccessToken, setStoredAccessToken } from '@/services/authSession'
|
||||
import {
|
||||
generateReservationRoomingListExcel,
|
||||
RoomingListGenerationError,
|
||||
} from '@/services/roomingListService'
|
||||
import { setUnauthorizedHandler } from '@/services/httpClient'
|
||||
import type { RoomingListGenerationInput } from '@/types/roomingList'
|
||||
|
||||
function createInput(): RoomingListGenerationInput {
|
||||
return {
|
||||
file: new File(['passport names'], 'source.xlsx', {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
}),
|
||||
hotelId: 'HOTEL-TEST',
|
||||
peoplePerRoom: 2,
|
||||
arrival: '2026-07-20',
|
||||
departure: '2026-07-22',
|
||||
roomType: 'DLX',
|
||||
title: 'MR',
|
||||
rateCode: 'BAR',
|
||||
adults: 2,
|
||||
children: 0,
|
||||
paymentType: 'BT',
|
||||
vip: 'VIP1',
|
||||
nationality: 'TH',
|
||||
email: 'guest@example.test',
|
||||
idType: 'Passport',
|
||||
idNumber: 'A1234567',
|
||||
}
|
||||
}
|
||||
|
||||
function createExcelResponse(headers: Record<string, string> = {}): Response {
|
||||
return new Response(new Blob(['xlsx-binary']), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
...headers,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function createJsonResponse(payload: unknown, status: number): Response {
|
||||
return new Response(JSON.stringify(payload), {
|
||||
status,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('roomingListService', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
clearStoredAccessToken()
|
||||
setUnauthorizedHandler(null)
|
||||
})
|
||||
|
||||
it('uploads source Excel as FormData and returns the generated workbook blob', async () => {
|
||||
const input = createInput()
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
createExcelResponse({
|
||||
'Content-Disposition': 'attachment; filename="rooming-list-HOTEL-TEST-20260718.xlsx"',
|
||||
}),
|
||||
)
|
||||
setStoredAccessToken('session-token')
|
||||
|
||||
const result = await generateReservationRoomingListExcel(input)
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
const [url, init] = fetchMock.mock.calls[0]!
|
||||
expect(url).toBe('/api/reservation/rooming-lists/generations')
|
||||
expect(init).toMatchObject({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
Authorization: 'Bearer session-token',
|
||||
},
|
||||
})
|
||||
expect(init?.headers).not.toHaveProperty('Content-Type')
|
||||
expect(init?.body).toBeInstanceOf(FormData)
|
||||
const formData = init?.body as FormData
|
||||
expect(formData.get('file')).toBe(input.file)
|
||||
expect(formData.get('hotel_id')).toBe('HOTEL-TEST')
|
||||
expect(formData.get('people_per_room')).toBe('2')
|
||||
expect(formData.get('arrival')).toBe('2026-07-20')
|
||||
expect(formData.get('departure')).toBe('2026-07-22')
|
||||
expect(formData.get('room_type')).toBe('DLX')
|
||||
expect(formData.get('title')).toBe('MR')
|
||||
expect(formData.get('rate_code')).toBe('BAR')
|
||||
expect(formData.get('adults')).toBe('2')
|
||||
expect(formData.get('children')).toBe('0')
|
||||
expect(formData.get('payment_type')).toBe('BT')
|
||||
expect(formData.get('vip')).toBe('VIP1')
|
||||
expect(formData.get('nationality')).toBe('TH')
|
||||
expect(formData.get('email')).toBe('guest@example.test')
|
||||
expect(formData.get('id_type')).toBe('Passport')
|
||||
expect(formData.get('id_number')).toBe('A1234567')
|
||||
expect(result.fileName).toBe('rooming-list-HOTEL-TEST-20260718.xlsx')
|
||||
expect(result.blob.type).toBe('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||
})
|
||||
|
||||
it('uses the default file name when Content-Disposition does not provide one', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValue(createExcelResponse())
|
||||
|
||||
const result = await generateReservationRoomingListExcel({
|
||||
...createInput(),
|
||||
hotelId: undefined,
|
||||
title: '',
|
||||
rateCode: '',
|
||||
adults: null,
|
||||
children: null,
|
||||
paymentType: '',
|
||||
vip: '',
|
||||
nationality: '',
|
||||
email: '',
|
||||
idType: '',
|
||||
idNumber: '',
|
||||
})
|
||||
|
||||
expect(result.fileName).toBe('rooming-list.xlsx')
|
||||
})
|
||||
|
||||
it('throws a typed error with backend details for validation failures', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
createJsonResponse(
|
||||
{
|
||||
error_code: 'ROOMING_LIST_VALIDATION_FAILED',
|
||||
message: 'Rooming List 字段校验失败。',
|
||||
details: ['people_per_room: 必填字段缺失。', 'departure: 必须晚于 arrival。'],
|
||||
},
|
||||
400,
|
||||
),
|
||||
)
|
||||
|
||||
await expect(generateReservationRoomingListExcel(createInput())).rejects.toMatchObject({
|
||||
status: 400,
|
||||
errorCode: 'ROOMING_LIST_VALIDATION_FAILED',
|
||||
message: 'Rooming List 字段校验失败。',
|
||||
details: ['people_per_room: 必填字段缺失。', 'departure: 必须晚于 arrival。'],
|
||||
} satisfies Partial<RoomingListGenerationError>)
|
||||
})
|
||||
|
||||
it('runs the shared unauthorized handler for auth session failures', async () => {
|
||||
const unauthorizedHandler = vi.fn()
|
||||
setUnauthorizedHandler(unauthorizedHandler)
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
createJsonResponse(
|
||||
{
|
||||
error_code: 'AUTH_SESSION_INVALID',
|
||||
message: '登录已失效,请重新登录。',
|
||||
details: [],
|
||||
},
|
||||
401,
|
||||
),
|
||||
)
|
||||
|
||||
await expect(generateReservationRoomingListExcel(createInput())).rejects.toMatchObject({
|
||||
status: 401,
|
||||
errorCode: 'AUTH_SESSION_INVALID',
|
||||
} satisfies Partial<RoomingListGenerationError>)
|
||||
expect(unauthorizedHandler).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user