import ElementPlus from 'element-plus' import { flushPromises, mount } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import OceanBookingPrintDialog from './OceanBookingPrintDialog.vue' import { downloadOceanBookingPrint } from '../api/platformOceanBookings' import { ApiResponseError } from '@/shared/api/http' vi.mock('../api/platformOceanBookings', () => ({ downloadOceanBookingPrint: vi.fn() })) afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); document.body.innerHTML = '' }) describe('OceanBookingPrintDialog', () => { beforeEach(() => { vi.clearAllMocks() document.body.innerHTML = '' vi.stubGlobal('URL', { createObjectURL: vi.fn(() => 'blob:booking-print'), revokeObjectURL: vi.fn() }) }) it('offers the two confirmed documents and previews the selected PDF', async () => { vi.mocked(downloadOceanBookingPrint).mockResolvedValue(new Blob(['pdf'], { type: 'application/pdf' })) const wrapper = mount(OceanBookingPrintDialog, { props: { modelValue: true, bookingId: '123', bookingNo: 'OB-123', allowed: true }, global: { plugins: [ElementPlus] }, attachTo: document.body, }) await flushPromises() expect(document.body.textContent).toContain('出口货物托运单') expect(document.body.textContent).toContain('提单确认件') await wrapper.findAll('.booking-print-option')[0]!.trigger('click') await flushPromises() await Array.from(document.body.querySelectorAll('button')).find((button) => button.textContent?.includes('预览 PDF'))?.click() await flushPromises() expect(downloadOceanBookingPrint).toHaveBeenCalledWith('123', 'SHIPPING_INSTRUCTION') expect(wrapper.emitted('previewed')).toEqual([[{ bookingId: '123', type: 'SHIPPING_INSTRUCTION' }]]) expect(document.body.querySelector('iframe')?.getAttribute('src')).toBe('blob:booking-print') expect(document.body.querySelector('.el-dialog__title')?.textContent).toBe('出口货物托运单 · OB-123') document.querySelector('[aria-label="展开全屏"]')!.click() await flushPromises() expect(document.querySelector('.el-dialog.is-fullscreen')).not.toBeNull() expect(document.querySelector('iframe')?.getAttribute('src')).toBe('blob:booking-print') document.querySelector('[aria-label="退出全屏"]')!.click() await flushPromises() Array.from(document.querySelectorAll('button')).find(button => button.textContent?.trim() === '返回选择')!.click() await flushPromises() expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:booking-print') expect(wrapper.findComponent({ name: 'ElRadioGroup' }).props('modelValue')).toBe('SHIPPING_INSTRUCTION') expect(downloadOceanBookingPrint).toHaveBeenCalledTimes(1) expect(document.querySelector('[aria-label="展开全屏"]')).toBeNull() wrapper.unmount() }) it('shows permission and API errors without attempting a download', async () => { const denied = mount(OceanBookingPrintDialog, { props: { modelValue: true, bookingId: '123', allowed: false }, global: { plugins: [ElementPlus] }, attachTo: document.body }) await flushPromises() expect(document.body.textContent).toContain('暂无订舱单据打印权限') expect(downloadOceanBookingPrint).not.toHaveBeenCalled() denied.unmount() vi.mocked(downloadOceanBookingPrint).mockRejectedValue(new ApiResponseError('缺少提单号', 'B0409', 200, undefined, 'req-print', 'trace-print')) const failed = mount(OceanBookingPrintDialog, { props: { modelValue: true, bookingId: '123', allowed: true }, global: { plugins: [ElementPlus] }, attachTo: document.body }) await flushPromises() await failed.findAll('.booking-print-option')[1]!.trigger('click') await flushPromises() await Array.from(document.body.querySelectorAll('button')).find((button) => button.textContent?.includes('预览 PDF'))?.click() await flushPromises() expect(document.body.textContent).toContain('缺少提单号') expect(document.body.textContent).toContain('req-print') expect(document.body.textContent).toContain('trace-print') failed.unmount() }) it('suppresses duplicate preview clicks and restores loading after completion', async () => { let resolvePrint!: (blob: Blob) => void vi.mocked(downloadOceanBookingPrint).mockImplementation(() => new Promise((resolve) => { resolvePrint = resolve })) const wrapper = mount(OceanBookingPrintDialog, { props: { modelValue: true, bookingId: '123', bookingNo: 'OB-123', allowed: true }, global: { plugins: [ElementPlus] }, attachTo: document.body, }) await flushPromises() await wrapper.findAll('.booking-print-option')[0]!.trigger('click') const preview = Array.from(document.body.querySelectorAll('button')).find((button) => button.textContent?.includes('预览 PDF'))! preview.click() preview.click() await flushPromises() expect(downloadOceanBookingPrint).toHaveBeenCalledTimes(1) expect(preview.disabled).toBe(true) resolvePrint(new Blob(['pdf'], { type: 'application/pdf' })) await flushPromises() expect(document.body.querySelector('iframe')).not.toBeNull() wrapper.unmount() }) }) describe('OceanBookingPrintDialog identity and resource lifecycle', () => { beforeEach(() => { vi.clearAllMocks() vi.stubGlobal('URL', { createObjectURL: vi.fn(() => 'blob:current-print'), revokeObjectURL: vi.fn() }) }) it('uses a neutral title and filename when the booking business number is missing', async () => { vi.mocked(downloadOceanBookingPrint).mockResolvedValue(new Blob(['pdf'], { type: 'application/pdf' })) const downloads: string[] = [] vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(function (this: HTMLAnchorElement) { downloads.push(this.download) }) const wrapper = mount(OceanBookingPrintDialog, { props: { modelValue: true, bookingId: 'internal-booking-id', bookingNo: ' ', allowed: true }, global: { plugins: [ElementPlus] }, attachTo: document.body }) await flushPromises() expect(document.querySelector('.el-dialog__title')?.textContent).toBe('选择打印单据') expect(document.body.textContent).not.toContain('internal-booking-id') await wrapper.findAll('.booking-print-option')[0]!.trigger('click') Array.from(document.querySelectorAll('button')).find(button => button.textContent?.trim() === '预览 PDF')!.click() await flushPromises() Array.from(document.querySelectorAll('button')).find(button => button.textContent?.trim() === '下载 PDF')!.click() expect(downloads).toEqual(['订舱单据-出口货物托运单.pdf']) expect(downloadOceanBookingPrint).toHaveBeenCalledExactlyOnceWith('internal-booking-id', 'SHIPPING_INSTRUCTION') expect(document.querySelector('iframe')?.title).not.toContain('internal-booking-id') wrapper.unmount() }) it('ignores a closed request that resolves after a new selection is previewed and releases the active URL', async () => { let resolveFirst!: (value: Blob) => void const current = new Blob(['current'], { type: 'application/pdf' }) vi.mocked(downloadOceanBookingPrint).mockImplementationOnce(() => new Promise(resolve => { resolveFirst = resolve })).mockResolvedValueOnce(current) const wrapper = mount(OceanBookingPrintDialog, { props: { modelValue: true, bookingId: '123', bookingNo: 'OB-123', allowed: true }, global: { plugins: [ElementPlus] }, attachTo: document.body }) await flushPromises() await wrapper.findAll('.booking-print-option')[0]!.trigger('click') Array.from(document.querySelectorAll('button')).find(button => button.textContent?.trim() === '预览 PDF')!.click() await flushPromises() await wrapper.setProps({ modelValue: false }) await flushPromises() await wrapper.setProps({ modelValue: true }) await flushPromises() await wrapper.findAll('.booking-print-option')[1]!.trigger('click') Array.from(document.querySelectorAll('button')).find(button => button.textContent?.trim() === '预览 PDF')!.click() await flushPromises() resolveFirst(new Blob(['stale'], { type: 'application/pdf' })) await flushPromises() expect(URL.createObjectURL).toHaveBeenCalledTimes(1) expect(vi.mocked(URL.createObjectURL).mock.calls[0]?.[0]).toBe(current) expect(document.querySelector('.el-dialog__title')?.textContent).toBe('提单确认件 · OB-123') expect(document.querySelector('iframe')?.getAttribute('src')).toBe('blob:current-print') expect(downloadOceanBookingPrint).toHaveBeenNthCalledWith(1, '123', 'SHIPPING_INSTRUCTION') expect(downloadOceanBookingPrint).toHaveBeenNthCalledWith(2, '123', 'BILL_OF_LADING_CONFIRMATION') wrapper.unmount() expect(URL.revokeObjectURL).toHaveBeenCalledExactlyOnceWith('blob:current-print') }) it('opens the selected document directly and reloads the other type after closing', async () => { vi.stubGlobal('URL', { createObjectURL: vi.fn(() => 'blob:booking-print'), revokeObjectURL: vi.fn() }) vi.mocked(downloadOceanBookingPrint).mockResolvedValue(new Blob(['pdf'], { type: 'application/pdf' })) const wrapper = mount(OceanBookingPrintDialog, { props: { modelValue: true, bookingId: '123', bookingNo: 'OB-123', allowed: true, initialType: 'SHIPPING_INSTRUCTION' }, global: { plugins: [ElementPlus] }, attachTo: document.body, }) await flushPromises() expect(downloadOceanBookingPrint).toHaveBeenLastCalledWith('123', 'SHIPPING_INSTRUCTION') expect(document.body.querySelector('iframe')).not.toBeNull() await wrapper.setProps({ modelValue: false }) await flushPromises() expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:booking-print') await wrapper.setProps({ initialType: 'BILL_OF_LADING_CONFIRMATION', modelValue: true }) await flushPromises() expect(downloadOceanBookingPrint).toHaveBeenLastCalledWith('123', 'BILL_OF_LADING_CONFIRMATION') expect(document.body.querySelector('.el-dialog__title')?.textContent).toBe('提单确认件 · OB-123') wrapper.unmount() }) })