mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-11-11 09:06:43 -05:00
Fix: delay iframe DOM removal, handle onafterprint error for print in FF (#11237)
This commit is contained in:
commit
8b9ca75a90
@ -1489,6 +1489,8 @@ describe('DocumentDetailComponent', () => {
|
|||||||
mockContentWindow.onafterprint(new Event('afterprint'))
|
mockContentWindow.onafterprint(new Event('afterprint'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tick(500)
|
||||||
|
|
||||||
expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
|
expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
|
||||||
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
|
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
|
||||||
|
|
||||||
@ -1512,65 +1514,97 @@ describe('DocumentDetailComponent', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show error toast if printing throws inside iframe', fakeAsync(() => {
|
const iframePrintErrorCases: Array<{
|
||||||
initNormally()
|
description: string
|
||||||
|
thrownError: Error
|
||||||
|
expectToast: boolean
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
description: 'should show error toast if printing throws inside iframe',
|
||||||
|
thrownError: new Error('focus failed'),
|
||||||
|
expectToast: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
'should suppress toast if cross-origin afterprint error occurs',
|
||||||
|
thrownError: new DOMException(
|
||||||
|
'Accessing onafterprint triggered a cross-origin violation',
|
||||||
|
'SecurityError'
|
||||||
|
),
|
||||||
|
expectToast: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
const appendChildSpy = jest
|
iframePrintErrorCases.forEach(({ description, thrownError, expectToast }) => {
|
||||||
.spyOn(document.body, 'appendChild')
|
it(
|
||||||
.mockImplementation((node: Node) => node)
|
description,
|
||||||
const removeChildSpy = jest
|
fakeAsync(() => {
|
||||||
.spyOn(document.body, 'removeChild')
|
initNormally()
|
||||||
.mockImplementation((node: Node) => node)
|
|
||||||
const createObjectURLSpy = jest
|
|
||||||
.spyOn(URL, 'createObjectURL')
|
|
||||||
.mockReturnValue('blob:mock-url')
|
|
||||||
const revokeObjectURLSpy = jest
|
|
||||||
.spyOn(URL, 'revokeObjectURL')
|
|
||||||
.mockImplementation(() => {})
|
|
||||||
|
|
||||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
const appendChildSpy = jest
|
||||||
|
.spyOn(document.body, 'appendChild')
|
||||||
|
.mockImplementation((node: Node) => node)
|
||||||
|
const removeChildSpy = jest
|
||||||
|
.spyOn(document.body, 'removeChild')
|
||||||
|
.mockImplementation((node: Node) => node)
|
||||||
|
const createObjectURLSpy = jest
|
||||||
|
.spyOn(URL, 'createObjectURL')
|
||||||
|
.mockReturnValue('blob:mock-url')
|
||||||
|
const revokeObjectURLSpy = jest
|
||||||
|
.spyOn(URL, 'revokeObjectURL')
|
||||||
|
.mockImplementation(() => {})
|
||||||
|
|
||||||
const mockContentWindow = {
|
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||||
focus: jest.fn().mockImplementation(() => {
|
|
||||||
throw new Error('focus failed')
|
|
||||||
}),
|
|
||||||
print: jest.fn(),
|
|
||||||
onafterprint: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockIframe: any = {
|
const mockContentWindow = {
|
||||||
style: {},
|
focus: jest.fn().mockImplementation(() => {
|
||||||
src: '',
|
throw thrownError
|
||||||
onload: null,
|
}),
|
||||||
contentWindow: mockContentWindow,
|
print: jest.fn(),
|
||||||
}
|
onafterprint: null,
|
||||||
|
}
|
||||||
|
|
||||||
const createElementSpy = jest
|
const mockIframe: any = {
|
||||||
.spyOn(document, 'createElement')
|
style: {},
|
||||||
.mockReturnValue(mockIframe as any)
|
src: '',
|
||||||
|
onload: null,
|
||||||
|
contentWindow: mockContentWindow,
|
||||||
|
}
|
||||||
|
|
||||||
const blob = new Blob(['test'], { type: 'application/pdf' })
|
const createElementSpy = jest
|
||||||
component.printDocument()
|
.spyOn(document, 'createElement')
|
||||||
|
.mockReturnValue(mockIframe as any)
|
||||||
|
|
||||||
const req = httpTestingController.expectOne(
|
const blob = new Blob(['test'], { type: 'application/pdf' })
|
||||||
`${environment.apiBaseUrl}documents/${doc.id}/download/`
|
component.printDocument()
|
||||||
|
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}documents/${doc.id}/download/`
|
||||||
|
)
|
||||||
|
req.flush(blob)
|
||||||
|
|
||||||
|
tick()
|
||||||
|
|
||||||
|
if (mockIframe.onload) {
|
||||||
|
mockIframe.onload(new Event('load'))
|
||||||
|
}
|
||||||
|
|
||||||
|
tick(200)
|
||||||
|
|
||||||
|
if (expectToast) {
|
||||||
|
expect(toastSpy).toHaveBeenCalled()
|
||||||
|
} else {
|
||||||
|
expect(toastSpy).not.toHaveBeenCalled()
|
||||||
|
}
|
||||||
|
expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
|
||||||
|
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
|
||||||
|
|
||||||
|
createElementSpy.mockRestore()
|
||||||
|
appendChildSpy.mockRestore()
|
||||||
|
removeChildSpy.mockRestore()
|
||||||
|
createObjectURLSpy.mockRestore()
|
||||||
|
revokeObjectURLSpy.mockRestore()
|
||||||
|
})
|
||||||
)
|
)
|
||||||
req.flush(blob)
|
})
|
||||||
|
|
||||||
tick()
|
|
||||||
|
|
||||||
if (mockIframe.onload) {
|
|
||||||
mockIframe.onload(new Event('load'))
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(toastSpy).toHaveBeenCalled()
|
|
||||||
expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
|
|
||||||
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
|
|
||||||
|
|
||||||
createElementSpy.mockRestore()
|
|
||||||
appendChildSpy.mockRestore()
|
|
||||||
removeChildSpy.mockRestore()
|
|
||||||
createObjectURLSpy.mockRestore()
|
|
||||||
revokeObjectURLSpy.mockRestore()
|
|
||||||
}))
|
|
||||||
})
|
})
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
|
|||||||
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
|
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector'
|
import { DeviceDetectorService } from 'ngx-device-detector'
|
||||||
import { BehaviorSubject, Observable, of, Subject } from 'rxjs'
|
import { BehaviorSubject, Observable, of, Subject, timer } from 'rxjs'
|
||||||
import {
|
import {
|
||||||
catchError,
|
catchError,
|
||||||
debounceTime,
|
debounceTime,
|
||||||
@ -1452,9 +1452,18 @@ export class DocumentDetailComponent
|
|||||||
URL.revokeObjectURL(blobUrl)
|
URL.revokeObjectURL(blobUrl)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.toastService.showError($localize`Print failed.`, err)
|
// FF throws cross-origin error on onafterprint
|
||||||
document.body.removeChild(iframe)
|
const isCrossOriginAfterPrintError =
|
||||||
URL.revokeObjectURL(blobUrl)
|
err instanceof DOMException &&
|
||||||
|
err.message.includes('onafterprint')
|
||||||
|
if (!isCrossOriginAfterPrintError) {
|
||||||
|
this.toastService.showError($localize`Print failed.`, err)
|
||||||
|
}
|
||||||
|
timer(100).subscribe(() => {
|
||||||
|
// delay to avoid FF print failure
|
||||||
|
document.body.removeChild(iframe)
|
||||||
|
URL.revokeObjectURL(blobUrl)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user