diff --git a/cypress/local-only/tests/integration/fileAPetition/petitions-clerk-creates-paper-case.cy.ts b/cypress/local-only/tests/integration/fileAPetition/petitions-clerk-creates-paper-case.cy.ts index b06406e85cb..a96ccb6f43a 100644 --- a/cypress/local-only/tests/integration/fileAPetition/petitions-clerk-creates-paper-case.cy.ts +++ b/cypress/local-only/tests/integration/fileAPetition/petitions-clerk-creates-paper-case.cy.ts @@ -19,6 +19,11 @@ import { unchecksOrdersAndNoticesBoxesInCase } from '../../../support/pages/unch describe('Petition clerk creates a paper filing', function () { describe('Create and submit a paper petition', () => { + beforeEach(() => { + cy.window().then(win => { + cy.stub(win, 'open').as('windowOpen'); + }); + }); it('should create a paper petition', () => { navigateToDocumentQC('petitionsclerk'); @@ -43,8 +48,6 @@ describe('Petition clerk creates a paper filing', function () { it('should display attachment links in the attachment section', () => { cy.get('[data-testid="petitionFileButton"]').should('be.visible'); cy.get('[data-testid="petitionFileButton"]').click(); - cy.get('[data-testid="modal-dialog-header"]').should('be.visible'); - cy.get('[data-testid="close-modal-button"]').click(); cy.get('[data-testid="stinFileDisplay"]').should('be.visible'); cy.get('[data-testid="stinFileDisplay"]').should('not.be.enabled'); @@ -52,14 +55,10 @@ describe('Petition clerk creates a paper filing', function () { 'be.visible', ); cy.get('[data-testid="requestForPlaceOfTrialFileButton"]').click(); - cy.get('[data-testid="modal-dialog-header"]').should('be.visible'); - cy.get('[data-testid="close-modal-button"]').click(); cy.get('[data-testid="attachmentToPetitionFileButton"]').should( 'be.visible', ); cy.get('[data-testid="attachmentToPetitionFileButton"]').click(); - cy.get('[data-testid="modal-dialog-header"]').should('be.visible'); - cy.get('[data-testid="close-modal-button"]').click(); }); it('should display Orders/Notices Automatically Created notification', () => { diff --git a/web-client/src/presenter/actions/PDFPreviewModal/loadPdfAction.test.ts b/web-client/src/presenter/actions/PDFPreviewModal/loadPdfAction.test.ts index 61fd51c3bdf..f23482d392a 100644 --- a/web-client/src/presenter/actions/PDFPreviewModal/loadPdfAction.test.ts +++ b/web-client/src/presenter/actions/PDFPreviewModal/loadPdfAction.test.ts @@ -5,32 +5,35 @@ import { runAction } from '@web-client/presenter/test.cerebral'; import { testPdfDoc } from '../../../../../shared/src/business/test/getFakeFile'; describe('loadPdfAction', () => { - global.Blob = function () {}; - const fakeFile = testPdfDoc; const b64File = `data:application/pdf;base64,${Buffer.from( - String.fromCharCode.apply(null, fakeFile), + String.fromCharCode(...fakeFile), ).toString('base64')}`; const mocks = { - readAsArrayBufferMock: jest.fn().mockImplementation(async function () { + readAsArrayBufferMock: jest.fn().mockImplementation(async function (this: { + result: any; + onload: any; + }) { this.result = fakeFile; await this.onload(); }), - readAsDataURLMock: jest.fn().mockImplementation(async function () { + readAsDataURLMock: jest.fn().mockImplementation(async function (this: { + result: any; + onload: any; + }) { this.result = b64File; await this.onload(); }), }; - /** - * Mock FileReader Implementation - */ - function MockFileReader() { - this.onload = null; - this.onerror = null; - this.readAsDataURL = mocks.readAsDataURLMock; - this.readAsArrayBuffer = mocks.readAsArrayBufferMock; + class MockFileReader { + public result: unknown = null; + public onload: any; + public onerror: any; + + readAsDataURL = mocks.readAsDataURLMock; + readAsArrayBuffer = mocks.readAsArrayBufferMock; } beforeAll(() => { @@ -100,7 +103,10 @@ describe('loadPdfAction', () => { }); it('should error out when the FileReader fails', async () => { - mocks.readAsArrayBufferMock.mockImplementationOnce(function () { + mocks.readAsArrayBufferMock.mockImplementationOnce(function (this: { + result: any; + onerror: any; + }) { this.result = 'abc'; this.onerror(new Error('An error called via reader.onerror.')); }); diff --git a/web-client/src/presenter/actions/PDFPreviewModal/loadPdfAction.ts b/web-client/src/presenter/actions/PDFPreviewModal/loadPdfAction.ts index 629d3ac3e5b..dcfa993d2b6 100644 --- a/web-client/src/presenter/actions/PDFPreviewModal/loadPdfAction.ts +++ b/web-client/src/presenter/actions/PDFPreviewModal/loadPdfAction.ts @@ -19,7 +19,7 @@ export const loadPdfAction = ({ store.set(state.modal.pdfPreviewModal, {}); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const reader = applicationContext.getFileReaderInstance(); reader.onload = () => { diff --git a/web-client/src/presenter/actions/PDFPreviewTab/loadPdfForTabAction.test.ts b/web-client/src/presenter/actions/PDFPreviewTab/loadPdfForTabAction.test.ts new file mode 100644 index 00000000000..9eaca35b931 --- /dev/null +++ b/web-client/src/presenter/actions/PDFPreviewTab/loadPdfForTabAction.test.ts @@ -0,0 +1,122 @@ +import { applicationContextForClient as applicationContext } from '@web-client/test/createClientTestApplicationContext'; +import { loadPdfForTabAction } from '../PDFPreviewTab/loadPdfForTabAction'; +import { presenter } from '../../presenter-mock'; +import { runAction } from '@web-client/presenter/test.cerebral'; +import { testPdfDoc } from '../../../../../shared/src/business/test/getFakeFile'; + +describe('loadPdfForTabAction', () => { + let originalWindowOpen; + + const fakeFile = testPdfDoc; + const b64File = `data:application/pdf;base64,${Buffer.from( + String.fromCharCode(...fakeFile), + ).toString('base64')}`; + + const mocks = { + readAsArrayBufferMock: jest.fn().mockImplementation(async function (this: { + result: any; + onload: any; + }) { + this.result = fakeFile; + await this.onload(); + }), + readAsDataURLMock: jest.fn().mockImplementation(async function (this: { + result: any; + onload: any; + }) { + this.result = b64File; + await this.onload(); + }), + }; + + class MockFileReader { + public result: unknown = null; + public onload: any; + public onerror: any; + + readAsDataURL = mocks.readAsDataURLMock; + readAsArrayBuffer = mocks.readAsArrayBufferMock; + } + + beforeAll(() => { + global.atob = x => x; + presenter.providers.path = { + error: jest.fn(), + success: jest.fn(), + }; + applicationContext.getFileReaderInstance.mockReturnValue( + new MockFileReader(), + ); + presenter.providers.applicationContext = applicationContext; + presenter.providers.router = { + createObjectURL: jest.fn().mockReturnValue('some url'), + }; + originalWindowOpen = window.open; + window.open = jest.fn(); + }); + + afterAll(() => { + window.open = originalWindowOpen; + }); + + it('should call window.open with correcturl for pdf file', async () => { + await runAction(loadPdfForTabAction, { + modules: { + presenter, + }, + props: { file: fakeFile }, + }); + + expect(window.open).toHaveBeenCalledWith('some url', '_blank'); + }); + + it('should detect binary (not base64-encoded) pdf data and read it successfully', async () => { + await runAction(loadPdfForTabAction, { + modules: { + presenter, + }, + props: { + file: fakeFile, + }, + }); + + expect(mocks.readAsArrayBufferMock).toHaveBeenCalled(); + }); + + it('should return an error when given an invalid pdf', async () => { + presenter.providers.router.createObjectURL.mockImplementationOnce(() => { + throw new Error('bad pdf data'); + }); + await expect( + runAction(loadPdfForTabAction, { + modules: { + presenter, + }, + props: { + file: 'data:binary/pdf,INVALID-BYTES', + }, + }), + ).rejects.toThrow('bad pdf data'); + }); + + it('should error out when the FileReader fails', async () => { + mocks.readAsArrayBufferMock.mockImplementationOnce(function (this: { + result: any; + onerror: any; + }) { + this.result = 'abc'; + this.onerror(new Error('An error called via reader.onerror.')); + }); + + await expect( + runAction(loadPdfForTabAction, { + modules: { + presenter, + }, + props: { + file: 'this my file', + }, + }), + ).rejects.toThrow('An error called via reader.onerror.'); + }); +}); diff --git a/web-client/src/presenter/actions/PDFPreviewTab/loadPdfForTabAction.ts b/web-client/src/presenter/actions/PDFPreviewTab/loadPdfForTabAction.ts new file mode 100644 index 00000000000..232b6246ffc --- /dev/null +++ b/web-client/src/presenter/actions/PDFPreviewTab/loadPdfForTabAction.ts @@ -0,0 +1,44 @@ +export const loadPdfForTabAction = ({ + applicationContext, + props, + router, +}: ActionProps) => { + const { file } = props; + const isBase64Encoded = typeof file === 'string' && file.startsWith('data'); + + return new Promise((resolve, reject) => { + const reader = applicationContext.getFileReaderInstance(); + + reader.onload = () => { + let binaryFile: string | ArrayBuffer | null; + if (isBase64Encoded && reader.result === 'string') { + const base64File = reader.result.replace(/[^,]+,/, ''); + binaryFile = atob(base64File); + } else { + binaryFile = reader.result; + } + + try { + const pdfDataUri = router.createObjectURL( + // @ts-ignore + new Blob([binaryFile], { type: 'application/pdf' }), + ); + window.open(pdfDataUri, '_blank'); + resolve(); + } catch (err) { + reject(err); + } + }; + + reader.onerror = function (err) { + reject(err); + }; + + if (isBase64Encoded) { + // @ts-ignore + reader.readAsDataURL(file); + } else { + reader.readAsArrayBuffer(file); + } + }); +}; diff --git a/web-client/src/presenter/actions/getPDFForPreviewTabAction.test.ts b/web-client/src/presenter/actions/getPDFForPreviewTabAction.test.ts new file mode 100644 index 00000000000..4ec450b8b97 --- /dev/null +++ b/web-client/src/presenter/actions/getPDFForPreviewTabAction.test.ts @@ -0,0 +1,50 @@ +import { applicationContextForClient as applicationContext } from '@web-client/test/createClientTestApplicationContext'; +import { getPDFForPreviewTabAction } from './getPDFForPreviewTabAction'; +import { presenter } from '../presenter-mock'; +import { runAction } from '@web-client/presenter/test.cerebral'; + +describe('getPDFForPreviewTabAction', () => { + beforeAll(() => { + presenter.providers.applicationContext = applicationContext; + applicationContext + .getUseCases() + .loadPDFForPreviewInteractor.mockResolvedValue('fake file data'); + }); + + it('returns original props if we already have what appears to be an actual file', async () => { + const props = { file: { name: 'name of a file on a real file object' } }; + const result = await runAction(getPDFForPreviewTabAction, { + modules: { + presenter, + }, + props, + state: {}, + }); + expect(result.props).toEqual(props); + expect( + applicationContext.getUseCases().loadPDFForPreviewInteractor, + ).not.toHaveBeenCalled(); + }); + + it('returns results from loadPDFForPreviewInteractor if provided a docketNumber and docketEntryId', async () => { + const props = { file: { docketEntryId: '456' } }; + await runAction(getPDFForPreviewTabAction, { + modules: { + presenter, + }, + props, + state: { + caseDetail: { + docketNumber: '123-20', + }, + }, + }); + expect( + applicationContext.getUseCases().loadPDFForPreviewInteractor.mock + .calls[0][1], + ).toMatchObject({ + docketEntryId: '456', + docketNumber: '123-20', + }); + }); +}); diff --git a/web-client/src/presenter/actions/getPDFForPreviewTabAction.ts b/web-client/src/presenter/actions/getPDFForPreviewTabAction.ts new file mode 100644 index 00000000000..cdce9fbd7f0 --- /dev/null +++ b/web-client/src/presenter/actions/getPDFForPreviewTabAction.ts @@ -0,0 +1,21 @@ +import { state } from '@web-client/presenter/app.cerebral'; + +export const getPDFForPreviewTabAction = async ({ + applicationContext, + get, + props, +}: ActionProps) => { + if (props.file.name) { + return props; + } + const { docketEntryId } = props.file; + const docketNumber = get(state.caseDetail.docketNumber); + + const pdfObj = await applicationContext + .getUseCases() + .loadPDFForPreviewInteractor(applicationContext, { + docketEntryId, + docketNumber, + }); + return { file: pdfObj }; +}; diff --git a/web-client/src/presenter/actions/openCaseDocumentDownloadUrlAction.ts b/web-client/src/presenter/actions/openCaseDocumentDownloadUrlAction.ts index 85c2756b40b..82fbea29075 100644 --- a/web-client/src/presenter/actions/openCaseDocumentDownloadUrlAction.ts +++ b/web-client/src/presenter/actions/openCaseDocumentDownloadUrlAction.ts @@ -1,11 +1,5 @@ import { state } from '@web-client/presenter/app.cerebral'; -/** - * opens the document in a new tab - * @param {object} providers the providers object - * @param {object} providers.props the cerebral props object - * @param {object} providers.store the cerebral store object used for clearing alertError, alertSuccess - */ export const openCaseDocumentDownloadUrlAction = async ({ applicationContext, props, diff --git a/web-client/src/presenter/computeds/fileDocumentHelper.ts b/web-client/src/presenter/computeds/fileDocumentHelper.ts index 14978154883..28cac600bbd 100644 --- a/web-client/src/presenter/computeds/fileDocumentHelper.ts +++ b/web-client/src/presenter/computeds/fileDocumentHelper.ts @@ -119,6 +119,7 @@ export const fileDocumentHelper = ( supportingDocumentTypeList, ...showSecondaryProperties, ...supportingDocumentFlags, + docketNumber: caseDetail.docketNumber, }; if (form.hasSupportingDocuments) { diff --git a/web-client/src/presenter/presenter.ts b/web-client/src/presenter/presenter.ts index 57c2751f0d1..a931dad7115 100644 --- a/web-client/src/presenter/presenter.ts +++ b/web-client/src/presenter/presenter.ts @@ -234,6 +234,7 @@ import { loadDefaultDraftViewerDocumentToDisplaySequence } from './sequences/Doc import { loadDefaultViewerCorrespondenceSequence } from './sequences/loadDefaultViewerCorrespondenceSequence'; import { loadMoreCaseDeadlinesSequence } from './sequences/loadMoreCaseDeadlinesSequence'; import { loadMorePendingItemsSequence } from './sequences/loadMorePendingItemsSequence'; +import { loadPdfForTabSequence } from './sequences/PDFPreviewTab/loadPdfForTabSequence'; import { loadPdfSequence } from './sequences/PDFPreviewModal/loadPdfSequence'; import { navigateBackSequence } from './sequences/navigateBackSequence'; import { navigateToCaseDetailFromPaperServiceSequence } from './sequences/navigateToCaseDetailFromPaperServiceSequence'; @@ -976,6 +977,7 @@ export const presenterSequences = { loadMoreCaseDeadlinesSequence as unknown as Function, loadMorePendingItemsSequence: loadMorePendingItemsSequence as unknown as Function, + loadPdfForTabSequence: loadPdfForTabSequence as unknown as Function, loadPdfSequence: loadPdfSequence as unknown as Function, navigateBackSequence: navigateBackSequence as unknown as Function, navigateToCaseDetailFromPaperServiceSequence: diff --git a/web-client/src/presenter/sequences/PDFPreviewTab/loadPdfForTabSequence.ts b/web-client/src/presenter/sequences/PDFPreviewTab/loadPdfForTabSequence.ts new file mode 100644 index 00000000000..cd4f245ca7a --- /dev/null +++ b/web-client/src/presenter/sequences/PDFPreviewTab/loadPdfForTabSequence.ts @@ -0,0 +1,9 @@ +import { getPDFForPreviewTabAction } from '../../actions/getPDFForPreviewTabAction'; +import { loadPdfForTabAction } from '../../actions/PDFPreviewTab/loadPdfForTabAction'; + +import { showProgressSequenceDecorator } from '../../utilities/showProgressSequenceDecorator'; + +export const loadPdfForTabSequence = showProgressSequenceDecorator([ + getPDFForPreviewTabAction, + loadPdfForTabAction, +]); diff --git a/web-client/src/utilities/fileReader.ts b/web-client/src/utilities/fileReader.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/web-client/src/views/DocketRecord/DocumentViewerDocument.tsx b/web-client/src/views/DocketRecord/DocumentViewerDocument.tsx index 3c8a79705a8..fa7b4f0eb2e 100644 --- a/web-client/src/views/DocketRecord/DocumentViewerDocument.tsx +++ b/web-client/src/views/DocketRecord/DocumentViewerDocument.tsx @@ -207,12 +207,12 @@ export const DocumentViewerDocument = connect( link icon="file-pdf" iconColor="white" - onClick={() => - openCaseDocumentDownloadUrlSequence({ + onClick={() => { + return openCaseDocumentDownloadUrlSequence({ docketEntryId: viewerDocumentToDisplay.docketEntryId, docketNumber: caseDetail.docketNumber, - }) - } + }); + }} > View Full PDF diff --git a/web-client/src/views/PDFPreviewButton.tsx b/web-client/src/views/PDFPreviewButton.tsx index 1bf1f948a47..9132d046a41 100644 --- a/web-client/src/views/PDFPreviewButton.tsx +++ b/web-client/src/views/PDFPreviewButton.tsx @@ -9,7 +9,9 @@ import { state } from '@web-client/presenter/app.cerebral'; import React from 'react'; const pdfPreviewButtonDeps = { - openPdfPreviewModalSequence: sequences.openPdfPreviewModalSequence, + loadPdfForTabSequence: sequences.loadPdfForTabSequence, + openCaseDocumentDownloadUrlSequence: + sequences.openCaseDocumentDownloadUrlSequence, pdfPreviewModalHelper: state.pdfPreviewModalHelper, showModal: state.modal.showModal, }; @@ -30,7 +32,7 @@ export const PDFPreviewButton = connect< function PDFPreviewButton({ file, id, - openPdfPreviewModalSequence, + loadPdfForTabSequence, pdfPreviewModalHelper, shouldAbbreviateTitle = false, shouldWrapText = true, @@ -72,7 +74,7 @@ export const PDFPreviewButton = connect<