diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.spec.ts index a0838c6ecd31..eef74e278543 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.spec.ts @@ -1,11 +1,20 @@ -import { Spectator, byTestId, createComponentFactory, mockProvider } from '@ngneat/spectator/jest'; +import { + Spectator, + SpyObject, + byTestId, + createComponentFactory, + mockProvider +} from '@ngneat/spectator/jest'; +import { of } from 'rxjs'; import { provideHttpClient } from '@angular/common/http'; import { ControlContainer } from '@angular/forms'; import { DotMessageService } from '@dotcms/data-access'; -import { DotDropZoneComponent } from '@dotcms/ui'; +import { DotDropZoneComponent, DropZoneErrorType, DropZoneFileEvent } from '@dotcms/ui'; +import { DotFileFieldPreviewComponent } from './components/dot-file-field-preview/dot-file-field-preview.component'; +import { DotFileFieldUiMessageComponent } from './components/dot-file-field-ui-message/dot-file-field-ui-message.component'; import { DotEditContentFileFieldComponent } from './dot-edit-content-file-field.component'; import { DotFileFieldUploadService } from './services/upload-file/upload-file.service'; import { FileFieldStore } from './store/file-field.store'; @@ -14,11 +23,15 @@ import { BINARY_FIELD_MOCK, createFormGroupDirectiveMock, FILE_FIELD_MOCK, - IMAGE_FIELD_MOCK + IMAGE_FIELD_MOCK, + NEW_FILE_MOCK } from '../../utils/mocks'; describe('DotEditContentFileFieldComponent', () => { let spectator: Spectator; + let store: InstanceType; + let uploadService: SpyObject; + const createComponent = createComponentFactory({ component: DotEditContentFileFieldComponent, detectChanges: false, @@ -30,23 +43,25 @@ describe('DotEditContentFileFieldComponent', () => { }); describe('FileField', () => { - beforeEach( - () => - (spectator = createComponent({ - props: { - field: FILE_FIELD_MOCK - } as unknown - })) - ); + beforeEach(() => { + spectator = createComponent({ + props: { + field: FILE_FIELD_MOCK + } as unknown + }); + store = spectator.inject(FileFieldStore, true); + uploadService = spectator.inject(DotFileFieldUploadService, true); + }); it('should be created', () => { expect(spectator.component).toBeTruthy(); }); - it('should have a DotDropZoneComponent', () => { + it('should have a DotDropZoneComponent and DotFileFieldUiMessageComponent', () => { spectator.detectChanges(); expect(spectator.query(DotDropZoneComponent)).toBeTruthy(); + expect(spectator.query(DotFileFieldUiMessageComponent)).toBeTruthy(); }); it('should show the proper actions', () => { @@ -57,6 +72,149 @@ describe('DotEditContentFileFieldComponent', () => { expect(spectator.query(byTestId('action-new-file'))).toBeTruthy(); expect(spectator.query(byTestId('action-generate-with-ai'))).toBeFalsy(); }); + + it('should call initLoad with proper params', () => { + const spyInitLoad = jest.spyOn(store, 'initLoad'); + + spectator.detectChanges(); + + expect(spyInitLoad).toHaveBeenCalledTimes(1); + expect(spyInitLoad).toHaveBeenCalledWith({ + fieldVariable: FILE_FIELD_MOCK.variable, + inputType: FILE_FIELD_MOCK.fieldType + }); + }); + + it('should call getAssetData when an value is set', () => { + const mockContentlet = NEW_FILE_MOCK.entity; + uploadService.getContentById.mockReturnValue(of(mockContentlet)); + + const spyGetAssetData = jest.spyOn(store, 'getAssetData'); + + spectator.component.writeValue(mockContentlet.identifier); + + expect(spyGetAssetData).toHaveBeenCalledTimes(1); + expect(spyGetAssetData).toHaveBeenCalledWith(mockContentlet.identifier); + }); + + it('should does not call getAssetData when an null value', () => { + const mockContentlet = NEW_FILE_MOCK.entity; + uploadService.getContentById.mockReturnValue(of(mockContentlet)); + + const spyGetAssetData = jest.spyOn(store, 'getAssetData'); + + spectator.component.writeValue(null); + + expect(spyGetAssetData).not.toHaveBeenCalled(); + }); + + it('should have a preview with a proper content', () => { + const mockContentlet = NEW_FILE_MOCK.entity; + uploadService.getContentById.mockReturnValue(of(mockContentlet)); + + spectator.component.writeValue(mockContentlet.identifier); + + spectator.detectChanges(); + + expect(spectator.query(DotFileFieldPreviewComponent)).toBeTruthy(); + }); + + describe('fileDropped event', () => { + it('should call to handleUploadFile when and proper file', () => { + const mockContentlet = NEW_FILE_MOCK.entity; + uploadService.uploadDotAsset.mockReturnValue(of(mockContentlet)); + + const spyHandleUploadFile = jest.spyOn(store, 'handleUploadFile'); + + const mockEvent: DropZoneFileEvent = { + file: new File([''], 'filename', { type: 'text/html' }), + validity: { + fileTypeMismatch: false, + maxFileSizeExceeded: false, + multipleFilesDropped: false, + errorsType: [], + valid: true + } + }; + spectator.detectChanges(); + + spectator.triggerEventHandler(DotDropZoneComponent, 'fileDropped', mockEvent); + + expect(spyHandleUploadFile).toHaveBeenCalledTimes(1); + expect(spyHandleUploadFile).toHaveBeenCalledWith(mockEvent.file); + }); + + it('should not call to handleUploadFile when a null file', () => { + const mockContentlet = NEW_FILE_MOCK.entity; + uploadService.uploadDotAsset.mockReturnValue(of(mockContentlet)); + + const spyHandleUploadFile = jest.spyOn(store, 'handleUploadFile'); + + const mockEvent: DropZoneFileEvent = { + file: null, + validity: { + fileTypeMismatch: false, + maxFileSizeExceeded: false, + multipleFilesDropped: false, + errorsType: [], + valid: true + } + }; + spectator.detectChanges(); + + spectator.triggerEventHandler(DotDropZoneComponent, 'fileDropped', mockEvent); + + expect(spyHandleUploadFile).not.toHaveBeenCalled(); + }); + + it('should set a proper error message with a invalid file', () => { + const mockContentlet = NEW_FILE_MOCK.entity; + uploadService.uploadDotAsset.mockReturnValue(of(mockContentlet)); + + const spySetUIMessage = jest.spyOn(store, 'setUIMessage'); + + const mockEvent: DropZoneFileEvent = { + file: new File([''], 'filename', { type: 'text/html' }), + validity: { + fileTypeMismatch: true, + maxFileSizeExceeded: false, + multipleFilesDropped: false, + errorsType: [DropZoneErrorType.MAX_FILE_SIZE_EXCEEDED], + valid: false + } + }; + spectator.detectChanges(); + + spectator.triggerEventHandler(DotDropZoneComponent, 'fileDropped', mockEvent); + + expect(spySetUIMessage).toHaveBeenCalled(); + }); + }); + + describe('fileSelected event', () => { + it('should call to fileSelected with proper file', () => { + const mockContentlet = NEW_FILE_MOCK.entity; + uploadService.uploadDotAsset.mockReturnValue(of(mockContentlet)); + + const spyHandleUploadFile = jest.spyOn(store, 'handleUploadFile'); + + const file = new File([''], 'filename', { type: 'text/html' }); + spectator.component.fileSelected([file] as unknown as FileList); + + expect(spyHandleUploadFile).toHaveBeenCalledTimes(1); + expect(spyHandleUploadFile).toHaveBeenCalledWith(file); + }); + + it('should not call to fileSelected when a null file', () => { + const mockContentlet = NEW_FILE_MOCK.entity; + uploadService.uploadDotAsset.mockReturnValue(of(mockContentlet)); + + const spyHandleUploadFile = jest.spyOn(store, 'handleUploadFile'); + spectator.component.fileSelected([] as unknown as FileList); + + expect(spyHandleUploadFile).not.toHaveBeenCalled(); + }); + }); }); describe('ImageField', () => { diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.ts index d645cd0ce8fd..6a8081f554e5 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.ts @@ -72,6 +72,10 @@ export class DotEditContentFileFieldComponent implements ControlValueAccessor, O constructor() { effect(() => { + if (!this.onChange && !this.onTouched) { + return; + } + const value = this.store.value(); this.onChange(value); this.onTouched(); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/store/file-field.store.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/store/file-field.store.spec.ts new file mode 100644 index 000000000000..4904da297ec6 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/store/file-field.store.spec.ts @@ -0,0 +1,250 @@ +import { SpyObject, mockProvider } from '@ngneat/spectator/jest'; +import { patchState } from '@ngrx/signals'; +import { of, throwError } from 'rxjs'; + +import { TestBed } from '@angular/core/testing'; + +import { FileFieldStore } from './file-field.store'; + +import { NEW_FILE_MOCK, TEMP_FILE_MOCK } from '../../../utils/mocks'; +import { UIMessage } from '../models'; +import { DotFileFieldUploadService } from '../services/upload-file/upload-file.service'; +import { getUiMessage } from '../utils/messages'; + +describe('FileFieldStore', () => { + let store: InstanceType; + let service: SpyObject; + + beforeEach(() => { + store = TestBed.overrideProvider( + DotFileFieldUploadService, + mockProvider(DotFileFieldUploadService) + ).runInInjectionContext(() => new FileFieldStore()); + + service = TestBed.inject(DotFileFieldUploadService) as SpyObject; + }); + + it('should be created', () => { + expect(store).toBeTruthy(); + }); + + describe('Method: initLoad', () => { + it('should init the state properly for File input', () => { + store.initLoad({ + inputType: 'File', + fieldVariable: 'myVar' + }); + + expect(store.inputType()).toEqual('File'); + expect(store.fieldVariable()).toEqual('myVar'); + expect(store.allowExistingFile()).toBe(true); + expect(store.allowURLImport()).toBe(true); + expect(store.allowCreateFile()).toBe(true); + expect(store.allowGenerateImg()).toBe(false); + expect(store.acceptedFiles().length).toBe(0); + expect(store.maxFileSize()).toBeNull(); + }); + + it('should init the state properly for Image input', () => { + store.initLoad({ + inputType: 'Image', + fieldVariable: 'myVar' + }); + + expect(store.inputType()).toEqual('Image'); + expect(store.fieldVariable()).toEqual('myVar'); + expect(store.allowExistingFile()).toBe(true); + expect(store.allowURLImport()).toBe(true); + expect(store.allowCreateFile()).toBe(false); + expect(store.allowGenerateImg()).toBe(true); + expect(store.acceptedFiles()).toContain('image/*'); + expect(store.maxFileSize()).toBeNull(); + }); + + it('should init the state properly for Binary input', () => { + store.initLoad({ + inputType: 'Binary', + fieldVariable: 'myVar' + }); + + expect(store.inputType()).toEqual('Binary'); + expect(store.fieldVariable()).toEqual('myVar'); + expect(store.allowExistingFile()).toBe(false); + expect(store.allowURLImport()).toBe(true); + expect(store.allowCreateFile()).toBe(true); + expect(store.allowGenerateImg()).toBe(true); + expect(store.acceptedFiles().length).toBe(0); + expect(store.maxFileSize()).toBeNull(); + }); + }); + + describe('Method: setUIMessage', () => { + it('should set uiMessage with valid UIMessage object', () => { + const uiMessage: UIMessage = { + message: 'test message', + severity: 'info', + icon: 'pi pi-upload' + }; + + store.setUIMessage(uiMessage); + + expect(store.uiMessage()).toEqual({ + message: 'test message', + severity: 'info', + icon: 'pi pi-upload', + args: [`${store.maxFileSize()}`, store.acceptedFiles().join(', ')] + }); + }); + }); + + describe('Method: removeFile', () => { + it('should set the state properly when removeFile is called', () => { + patchState(store, { + contentlet: NEW_FILE_MOCK.entity, + tempFile: TEMP_FILE_MOCK, + value: 'some value', + fileStatus: 'preview', + uiMessage: getUiMessage('SERVER_ERROR') + }); + store.removeFile(); + expect(store.contentlet()).toBeNull(); + expect(store.tempFile()).toBeNull(); + expect(store.value()).toBe(''); + expect(store.fileStatus()).toBe('init'); + expect(store.uiMessage()).toBe(getUiMessage('DEFAULT')); + }); + }); + + describe('Method: setDropZoneState', () => { + it('should set dropZoneActive to true', () => { + patchState(store, { + dropZoneActive: false + }); + store.setDropZoneState(true); + expect(store.dropZoneActive()).toBe(true); + }); + + it('should set dropZoneActive to false', () => { + patchState(store, { + dropZoneActive: true + }); + store.setDropZoneState(false); + expect(store.dropZoneActive()).toBe(false); + }); + }); + + describe('Method: handleUploadFile', () => { + it('should does not call uploadService with maxFileSize exceeded', () => { + patchState(store, { + maxFileSize: 10000 + }); + + const file = new File([''], 'filename', { type: 'text/plain' }); + Object.defineProperty(file, 'size', { value: 20000 }); + + store.handleUploadFile(file); + expect(service.uploadDotAsset).not.toHaveBeenCalled(); + }); + + it('should set state properly with maxFileSize exceeded', () => { + patchState(store, { + maxFileSize: 10000 + }); + + const file = new File([''], 'filename', { type: 'text/plain' }); + Object.defineProperty(file, 'size', { value: 20000 }); + + store.handleUploadFile(file); + expect(store.dropZoneActive()).toBe(true); + expect(store.fileStatus()).toBe('init'); + expect(store.uiMessage()).toEqual({ + ...getUiMessage('MAX_FILE_SIZE_EXCEEDED'), + args: ['10000'] + }); + }); + + it('should call uploadService with maxFileSize not exceeded', () => { + service.uploadDotAsset.mockReturnValue(of(NEW_FILE_MOCK.entity)); + + patchState(store, { + maxFileSize: 10000 + }); + + const file = new File([''], 'filename', { type: 'text/plain' }); + Object.defineProperty(file, 'size', { value: 5000 }); + + store.handleUploadFile(file); + expect(service.uploadDotAsset).toHaveBeenCalledWith(file); + }); + + it('should set state properly with maxFileSize not exceeded', () => { + const mockContentlet = NEW_FILE_MOCK.entity; + + service.uploadDotAsset.mockReturnValue(of(mockContentlet)); + + patchState(store, { + maxFileSize: 10000 + }); + + const file = new File([''], 'filename', { type: 'text/plain' }); + Object.defineProperty(file, 'size', { value: 5000 }); + + store.handleUploadFile(file); + expect(store.tempFile()).toBeNull(); + expect(store.value()).toBe(mockContentlet.identifier); + expect(store.contentlet()).toEqual(mockContentlet); + expect(store.fileStatus()).toBe('preview'); + expect(store.previewFile()).toEqual({ + source: 'contentlet', + file: mockContentlet + }); + }); + + it('should set state properly with an error calling uploadDotAsset', () => { + service.uploadDotAsset.mockReturnValue(throwError('error')); + + const file = new File([''], 'filename', { type: 'text/plain' }); + store.handleUploadFile(file); + + expect(service.uploadDotAsset).toHaveBeenCalledWith(file); + expect(store.fileStatus()).toBe('init'); + expect(store.uiMessage()).toEqual(getUiMessage('SERVER_ERROR')); + }); + }); + + describe('Method: getAssetData', () => { + it('should call getContentById', () => { + const mockContentlet = NEW_FILE_MOCK.entity; + service.getContentById.mockReturnValue(of(mockContentlet)); + + store.getAssetData(mockContentlet.identifier); + expect(service.getContentById).toHaveBeenCalledWith(mockContentlet.identifier); + }); + + it('should set state properly', () => { + const mockContentlet = NEW_FILE_MOCK.entity; + service.getContentById.mockReturnValue(of(mockContentlet)); + + store.getAssetData(mockContentlet.identifier); + + expect(store.tempFile()).toBeNull(); + expect(store.value()).toBe(mockContentlet.identifier); + expect(store.contentlet()).toEqual(mockContentlet); + expect(store.fileStatus()).toBe('preview'); + expect(store.previewFile()).toEqual({ + source: 'contentlet', + file: mockContentlet + }); + }); + + it('should set state properly with an error calling getAssetData', () => { + service.getContentById.mockReturnValue(throwError('error')); + + store.getAssetData('id'); + + expect(service.getContentById).toHaveBeenCalledWith('id'); + expect(store.fileStatus()).toBe('init'); + expect(store.uiMessage()).toEqual(getUiMessage('SERVER_ERROR')); + }); + }); +}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/utils/messages.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/utils/messages.spec.ts new file mode 100644 index 000000000000..f4406a10887c --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/utils/messages.spec.ts @@ -0,0 +1,23 @@ +import { getUiMessage, UiMessageMap } from './messages'; + +import { MESSAGES_TYPES } from '../models'; + +describe('getUiMessage function', () => { + it('should return the correct uiMessage for a valid key', () => { + const key: MESSAGES_TYPES = 'DEFAULT'; + const expectedUiMessage = UiMessageMap[key]; + expect(getUiMessage(key)).toEqual(expectedUiMessage); + }); + + it('should throw an error for an invalid key', () => { + const key = 'INVALID_KEY' as MESSAGES_TYPES; + expect(() => getUiMessage(key)).toThrowError(`Key ${key} not found in UiMessageMap`); + }); + + it('should throw an error for a null or undefined key', () => { + const key = null as unknown as MESSAGES_TYPES; + expect(() => getUiMessage(key)).toThrowError(`Key ${key} not found in UiMessageMap`); + const key2 = undefined as unknown as MESSAGES_TYPES; + expect(() => getUiMessage(key2)).toThrowError(`Key ${key2} not found in UiMessageMap`); + }); +}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/utils/messages.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/utils/messages.ts index 30f188a17dc4..b2f3d5fa491b 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/utils/messages.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/utils/messages.ts @@ -1,4 +1,4 @@ -import { MESSAGES_TYPES, UIMessagesMap } from '../models'; +import { MESSAGES_TYPES, UIMessage, UIMessagesMap } from '../models'; export const UiMessageMap: UIMessagesMap = { DEFAULT: { @@ -28,6 +28,17 @@ export const UiMessageMap: UIMessagesMap = { } }; -export function getUiMessage(key: MESSAGES_TYPES) { - return UiMessageMap[key]; +/** + * Returns a uiMessage given its key + * @param key The key of the uiMessage + * @returns The uiMessage + */ +export function getUiMessage(key: MESSAGES_TYPES): UIMessage { + const uiMessage = UiMessageMap[key]; + + if (!uiMessage) { + throw new Error(`Key ${key} not found in UiMessageMap`); + } + + return uiMessage; }