Skip to content

Commit

Permalink
WEBREL-1132 / refactor file uploader (deriv-com#10457)
Browse files Browse the repository at this point in the history
* chore: useFileUploader hook

* chore: update types for file uploader

* feat: 🎨 migrate File upload to TS

* chore: update types and cleanup proof-of-address-form

* fix: ts types and promise resolution

* chore: update types, hook and test cases for poa address form

* fix: update types & removed unused imports

* chore: test case for useFileUploader

* chore: add apiprovider in cfd

* chore: update cfd-poa test case

* chore: removed unused import

* fix: exported required type

* chore: refactored deprecated isMobile & updated test cases

* fix: update file uploader to pass eslint in cfd

* chore: removed unused import

* chore: declare module for binary file uploader to fix failing trader build

* chore: update types and fix the file selection in poa

* fix: getChangeableFields when account_settings is not yet populated

---------

Co-authored-by: Likhith Kolayari <likhith@regentmarkets.com>
  • Loading branch information
shahzaib-deriv and likhith-deriv authored Oct 25, 2023
1 parent 9ff6169 commit d99a695
Show file tree
Hide file tree
Showing 21 changed files with 422 additions and 321 deletions.
18 changes: 18 additions & 0 deletions packages/account/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
declare module '@binary-com/binary-document-uploader' {
import { DocumentUploadRequest } from '@deriv/api-types';

class DocumentUploader {
constructor(settings: { connection: any });
upload(file: {
documentType?: DocumentUploadRequest['document_type'];
documentFormat?: string | Omit<string, DocumentUploadRequest['document_format']>;
documentId?: DocumentUploadRequest['document_id'];
expirationDate?: string;
lifetimeValid?: DocumentUploadRequest['lifetime_valid'];
pageType?: DocumentUploadRequest['page_type'];
[key: string]: unknown;
}): Promise<{ message?: string; warning?: string; [key: string]: any }>;
}

export default DocumentUploader; // Export the class as default
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { isDesktop, isMobile } from '@deriv/shared';
import { isDesktop } from '@deriv/shared';
import FileUploaderContainer from '../file-uploader-container';
import { StoreProvider, mockStore } from '@deriv/stores';

jest.mock('@deriv/components', () => {
const original_module = jest.requireActual('@deriv/components');
Expand All @@ -13,7 +14,6 @@ jest.mock('@deriv/components', () => {
jest.mock('@deriv/shared', () => ({
...jest.requireActual('@deriv/shared'),
isDesktop: jest.fn(),
isMobile: jest.fn(),
WS: {
getSocket: jest.fn(),
},
Expand All @@ -26,17 +26,18 @@ describe('<FileUploaderContainer />', () => {
mock_props = {
examples: '',
files_description: '',
getSocket: jest.fn(),
onFileDrop: jest.fn(),
onRef: jest.fn(),
settings: {},
};

(isDesktop as jest.Mock).mockReturnValue(true);
(isMobile as jest.Mock).mockReturnValue(false);
jest.clearAllMocks();
});

const mock_store = mockStore({});
const wrapper = ({ children }: { children: React.ReactNode }) => (
<StoreProvider store={mock_store}>{children}</StoreProvider>
);

const file_size_msg = /maximum size: 8MB/i;
const file_type_msg = /supported formats: JPEG, JPG, PNG, PDF and GIF only/i;
const file_warning_msg = /remember, selfies, pictures of houses, or non-related images will be rejected./i;
Expand All @@ -52,43 +53,43 @@ describe('<FileUploaderContainer />', () => {
};

it('should render FileUploaderContainer component and show descriptions', () => {
render(<FileUploaderContainer {...mock_props} />);
render(<FileUploaderContainer {...mock_props} />, { wrapper });
runCommonTests();
});

it('should render FileUploaderContainer component if getSocket is not passed as prop', () => {
delete mock_props.getSocket;
render(<FileUploaderContainer {...mock_props} />);
render(<FileUploaderContainer {...mock_props} />, { wrapper });
runCommonTests();
});

it('files description and examples should be shown when passed', () => {
mock_props.files_description = <div>Files description</div>;
mock_props.examples = <div>Files failure examples</div>;

render(<FileUploaderContainer {...mock_props} />);
render(<FileUploaderContainer {...mock_props} />, { wrapper });
expect(screen.getByText('Files description')).toBeInTheDocument();
expect(screen.getByText('Files failure examples')).toBeInTheDocument();
});

it('should show hint message for desktop', () => {
render(<FileUploaderContainer {...mock_props} />);
render(<FileUploaderContainer {...mock_props} />, { wrapper });
expect(screen.getByText(hint_msg_desktop)).toBeInTheDocument();
expect(screen.queryByText(hint_msg_mobile)).not.toBeInTheDocument();
});

it('should show hint message for mobile', () => {
(isMobile as jest.Mock).mockReturnValue(true);
(isDesktop as jest.Mock).mockReturnValue(false);

render(<FileUploaderContainer {...mock_props} />);
const mock_store = mockStore({
ui: {
is_mobile: true,
},
});

render(<FileUploaderContainer {...mock_props} />, {
wrapper: ({ children }) => <StoreProvider store={mock_store}>{children}</StoreProvider>,
});
expect(screen.getByText(hint_msg_mobile)).toBeInTheDocument();
expect(screen.queryByText(hint_msg_desktop)).not.toBeInTheDocument();
});

it('should call ref function on rendering the component', () => {
render(<FileUploaderContainer {...mock_props} />);

expect(mock_props.onRef).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -1,31 +1,29 @@
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { DocumentUploadResponse } from '@deriv/api-types';
import { compressImageFiles, isMobile, isDesktop, readFiles } from '@deriv/shared';
import { isDesktop, readFiles } from '@deriv/shared';
import FileUploader from '../file-uploader';
import { StoreProvider, mockStore } from '@deriv/stores';

jest.mock('@deriv/shared', () => ({
...jest.requireActual('@deriv/shared'),
isDesktop: jest.fn(),
isMobile: jest.fn(),
compressImageFiles: jest.fn(() => Promise.resolve([{ path: 'hello.pdf' }])),
readFiles: jest.fn(),
}));

jest.mock('@binary-com/binary-document-uploader');

describe('<FileUploader />', () => {
beforeEach(() => {
(isDesktop as jest.Mock).mockReturnValue(true);
(isMobile as jest.Mock).mockReturnValue(false);
jest.clearAllMocks();
});

const mock_store = mockStore({});
const wrapper = ({ children }: { children: React.ReactNode }) => (
<StoreProvider store={mock_store}>{children}</StoreProvider>
);

const props: React.ComponentProps<typeof FileUploader> = {
onFileDrop: jest.fn(),
getSocket: jest.fn(),
ref: React.createRef<HTMLElement>(),
settings: {},
};

const large_file_error_msg = /file size should be 8mb or less/i;
Expand All @@ -34,19 +32,25 @@ describe('<FileUploader />', () => {
const click_msg = /click here to upload/i;

it('should render FileUploader component in desktop mode', () => {
render(<FileUploader {...props} />);
render(<FileUploader {...props} />, { wrapper });
expect(screen.getByText(drop_click_msg)).toBeInTheDocument();
});

it('should render FileUploader component in mobile mode', () => {
(isMobile as jest.Mock).mockReturnValue(true);
(isDesktop as jest.Mock).mockReturnValue(false);
render(<FileUploader {...props} />);
const mock_store = mockStore({
ui: {
is_mobile: true,
},
});
render(<FileUploader {...props} />, {
wrapper: ({ children }) => <StoreProvider store={mock_store}>{children}</StoreProvider>,
});
expect(screen.getByText(click_msg)).toBeInTheDocument();
});

it('should upload supported file', async () => {
render(<FileUploader {...props} />);
render(<FileUploader {...props} />, { wrapper });

const file = new File(['hello'], 'hello.png', { type: 'image/png' });

Expand All @@ -60,7 +64,7 @@ describe('<FileUploader />', () => {
});

it('should show error message when unsupported file is uploaded', async () => {
render(<FileUploader {...props} />);
render(<FileUploader {...props} />, { wrapper });

const file = new File(['hello'], 'hello.html', { type: 'html' });
const input = screen.getByTestId('dt_file_upload_input');
Expand All @@ -72,7 +76,7 @@ describe('<FileUploader />', () => {
});

it('should show error message when multiple files are uploaded', async () => {
render(<FileUploader {...props} />);
render(<FileUploader {...props} />, { wrapper });

const files = [
new File(['hello'], 'hello.png', { type: 'image/png' }),
Expand All @@ -87,7 +91,7 @@ describe('<FileUploader />', () => {
});

it('should show error message when larger files are uploaded', async () => {
render(<FileUploader {...props} />);
render(<FileUploader {...props} />, { wrapper });
const file = new File(['hello'], 'hello.png', { type: 'image/png' });
Object.defineProperty(file, 'size', { value: 1024 * 1024 * 10 });

Expand All @@ -100,7 +104,7 @@ describe('<FileUploader />', () => {
});

it('should remove the file when close icon is clicked', async () => {
render(<FileUploader {...props} />);
render(<FileUploader {...props} />, { wrapper });
const file = new File(['hello'], 'hello.png', { type: 'image/png' });

const input: HTMLInputElement = screen.getByTestId('dt_file_upload_input');
Expand All @@ -121,19 +125,10 @@ describe('<FileUploader />', () => {
});
});

it('upload function should return 0 if document is not selected', () => {
render(<FileUploader {...props} />);

const uploadFn = (
props?.ref as React.RefObject<HTMLElement & { upload: () => Promise<DocumentUploadResponse> }>
).current?.upload();
expect(uploadFn).toBe(0);
});

it('upload methods should reject if readFile returns empty array ', async () => {
(readFiles as jest.Mock).mockResolvedValue([]);

render(<FileUploader {...props} />);
render(<FileUploader {...props} />, { wrapper });
const blob = new Blob(['sample_data']);
const file = new File([blob], 'hello.pdf', { type: 'application/pdf' });

Expand All @@ -143,10 +138,7 @@ describe('<FileUploader />', () => {
expect(screen.getByText(/hello\.pdf/i)).toBeInTheDocument();
expect(input?.files?.[0]).toBe(file);
});
(
props?.ref as React.RefObject<HTMLElement & { upload: () => Promise<DocumentUploadResponse> }>
).current?.upload();
expect(compressImageFiles).toBeCalled();

expect(props.onFileDrop).toBeCalled();
});
});
Original file line number Diff line number Diff line change
@@ -1,59 +1,42 @@
import React from 'react';
import { Text } from '@deriv/components';
import { isMobile, WS } from '@deriv/shared';
import type { TSettings } from '@deriv/shared/src/utils/files/file-uploader-utils';
import { Localize } from '@deriv/translations';
import FileUploader from './file-uploader';
import { TFile } from '../../Types';
import { observer, useStore } from '@deriv/stores';

type TFileUploaderContainer = {
getSocket?: () => WebSocket;
onFileDrop: (file: TFile | undefined) => void;
onRef: (ref: React.RefObject<null | { upload: () => void }> | undefined) => void;
settings?: Partial<TSettings>;
onFileDrop: (files: File[]) => void;
files_description: React.ReactNode;
examples: React.ReactNode;
onError?: (error_message: string) => void;
};

const FileUploaderContainer = ({
examples,
files_description,
getSocket,
onFileDrop,
onRef,
settings,
}: TFileUploaderContainer) => {
const ref = React.useRef(null);

const getSocketFunc = getSocket ?? WS.getSocket;

React.useEffect(() => {
if (ref) {
if (typeof onRef === 'function') onRef(ref);
}
return () => onRef(undefined);
}, [onRef, ref]);

return (
<div className='file-uploader__container' data-testid='dt_file_uploader_container'>
{files_description}
<Text size={isMobile() ? 'xxs' : 'xs'} as='div' className='file-uploader__file-title' weight='bold'>
<Localize i18n_default_text='Upload file' />
</Text>
<div className='file-uploader__file-dropzone-wrapper'>
<FileUploader getSocket={getSocketFunc} ref={ref} onFileDrop={onFileDrop} settings={settings} />
</div>
<div className='file-uploader__file-supported-formats'>
<Text size={isMobile() ? 'xxxs' : 'xxs'}>
<Localize i18n_default_text='Supported formats: JPEG, JPG, PNG, PDF and GIF only' />
</Text>
<Text size={isMobile() ? 'xxxs' : 'xxs'}>
<Localize i18n_default_text='Maximum size: 8MB' />
const FileUploaderContainer = observer(
({ examples, files_description, onFileDrop, onError }: TFileUploaderContainer) => {
const {
ui: { is_mobile },
} = useStore();
return (
<div className='file-uploader__container' data-testid='dt_file_uploader_container'>
{files_description}
<Text size={is_mobile ? 'xxs' : 'xs'} as='div' className='file-uploader__file-title' weight='bold'>
<Localize i18n_default_text='Upload file' />
</Text>
<div className='file-uploader__file-dropzone-wrapper'>
<FileUploader onError={onError} onFileDrop={onFileDrop} />
</div>
<div className='file-uploader__file-supported-formats'>
<Text size={is_mobile ? 'xxxs' : 'xxs'}>
<Localize i18n_default_text='Supported formats: JPEG, JPG, PNG, PDF and GIF only' />
</Text>
<Text size={is_mobile ? 'xxxs' : 'xxs'}>
<Localize i18n_default_text='Maximum size: 8MB' />
</Text>
</div>
{examples}
</div>
{examples}
</div>
);
};
);
}
);

export default FileUploaderContainer;
Loading

0 comments on commit d99a695

Please sign in to comment.