refactor: add service for chains settings
This commit is contained in:
@@ -1,46 +1,28 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ChainAdd } from './chain-add';
|
||||
import { ChainDAO } from '../../../../dao/ChainDAO';
|
||||
import { vi } from 'vitest';
|
||||
import { provideTranslateService } from '@ngx-translate/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ChainFormGroup } from '../chain-formgroup';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { ImageStorage } from '../../../../services/image-storage';
|
||||
import { ChainSettings } from '../../../../services/chain-settings';
|
||||
import { Chain } from '../../../../models/Chain';
|
||||
|
||||
describe('ChainAdd', () => {
|
||||
let component: ChainAdd;
|
||||
let fixture: ComponentFixture<ChainAdd>;
|
||||
|
||||
let chainDAO: Partial<ChainDAO>;
|
||||
let imageStorage: Partial<ImageStorage>;
|
||||
let router: Partial<Router>;
|
||||
let chainSettings: Partial<ChainSettings>;
|
||||
|
||||
beforeEach(async () => {
|
||||
chainDAO = {
|
||||
insert: vi.fn().mockResolvedValue({
|
||||
rows: [],
|
||||
}),
|
||||
};
|
||||
|
||||
imageStorage = {
|
||||
saveImage: vi.fn().mockResolvedValue('mock.jpg'),
|
||||
};
|
||||
|
||||
router = {
|
||||
navigate: vi.fn(),
|
||||
chainSettings = {
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ChainAdd],
|
||||
providers: [
|
||||
provideTranslateService(),
|
||||
{ provide: ChainDAO, useValue: chainDAO },
|
||||
{ provide: ImageStorage, useValue: imageStorage },
|
||||
{ provide: Router, useValue: router },
|
||||
],
|
||||
providers: [provideTranslateService(), { provide: ChainSettings, useValue: chainSettings }],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ChainAdd);
|
||||
@@ -53,7 +35,6 @@ describe('ChainAdd', () => {
|
||||
});
|
||||
|
||||
it('should insert chain and store image', async () => {
|
||||
const saveSpy = vi.spyOn(component, 'save');
|
||||
(<any>component).form = new ChainFormGroup({
|
||||
name: new FormControl('Mock'),
|
||||
image: new FormControl(new File([], 'mock')),
|
||||
@@ -62,8 +43,9 @@ describe('ChainAdd', () => {
|
||||
const actionBtn = fixture.debugElement.query(By.css('app-action-btn'));
|
||||
actionBtn.triggerEventHandler('click');
|
||||
await fixture.whenStable();
|
||||
expect(saveSpy).toHaveBeenCalled();
|
||||
expect(imageStorage.saveImage).toHaveBeenCalledOnce();
|
||||
expect(chainDAO.insert).toHaveBeenCalledOnce();
|
||||
expect(chainSettings.save).toHaveBeenCalledExactlyOnceWith(
|
||||
new Chain(component.form.controls.name.value, ''),
|
||||
component.form.controls.image.value,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,44 +2,37 @@ import { Component, inject } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { ImageStorage } from '../../../../services/image-storage';
|
||||
import { ChainDAO } from '../../../../dao/ChainDAO';
|
||||
import { Chain } from '../../../../models/Chain';
|
||||
import { ChainFormGroup } from '../chain-formgroup';
|
||||
import { ChainForm } from "../chain-form/chain-form";
|
||||
import { Router } from '@angular/router';
|
||||
import { ActionBtn } from "../../../../components/action-btn/action-btn";
|
||||
import { ChainForm } from '../chain-form/chain-form';
|
||||
import { ActionBtn } from '../../../../components/action-btn/action-btn';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { UpperfirstPipe } from "../../../../pipes/upperfirst-pipe";
|
||||
import { UpperfirstPipe } from '../../../../pipes/upperfirst-pipe';
|
||||
import { ChainSettings } from '../../../../services/chain-settings';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chain-add',
|
||||
imports: [MatFormFieldModule, MatInputModule, ReactiveFormsModule, ChainForm, ActionBtn, TranslatePipe, UpperfirstPipe],
|
||||
imports: [
|
||||
ActionBtn,
|
||||
ChainForm,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule,
|
||||
TranslatePipe,
|
||||
UpperfirstPipe,
|
||||
],
|
||||
templateUrl: './chain-add.html',
|
||||
styleUrl: './chain-add.scss',
|
||||
})
|
||||
export class ChainAdd {
|
||||
private readonly chainDAO = inject(ChainDAO);
|
||||
private readonly imageStorage = inject(ImageStorage);
|
||||
private readonly router = inject(Router);
|
||||
readonly form = new ChainFormGroup
|
||||
private readonly chainSettings = inject(ChainSettings);
|
||||
readonly form = new ChainFormGroup();
|
||||
|
||||
async save() {
|
||||
const name = this.form.controls.name.value;
|
||||
const img = this.form.controls.image.value;
|
||||
try {
|
||||
//TODO: the sqlite bridge can't handle null as param
|
||||
const chain = new Chain(name, '');
|
||||
if (img) {
|
||||
const imgFileName = await this.imageStorage.saveImage(img);
|
||||
chain.image = imgFileName;
|
||||
}
|
||||
await this.chainDAO.insert(chain);
|
||||
this.router.navigate(['settings', 'chains'])
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
//TODO: report problem
|
||||
}
|
||||
//TODO: the sqlite bridge can't handle null as param
|
||||
const chain = new Chain(name, '');
|
||||
await this.chainSettings.save(chain, img);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,52 +1,44 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ChainEdit } from './chain-edit';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { Chain } from '../../../../models/Chain';
|
||||
import { provideTranslateService } from '@ngx-translate/core';
|
||||
import { ChainDAO } from '../../../../dao/ChainDAO';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { ImageStorage } from '../../../../services/image-storage';
|
||||
import { ChainSettings } from '../../../../services/chain-settings';
|
||||
|
||||
const CHAINS_PATH = ['settings', 'chains'];
|
||||
const CHAIN_MOCK = new Chain('Mock', '', 1);
|
||||
describe('ChainEdit', () => {
|
||||
let component: ChainEdit;
|
||||
let fixture: ComponentFixture<ChainEdit>;
|
||||
|
||||
let activatedRoute: Partial<ActivatedRoute>;
|
||||
let chainDAO: Partial<ChainDAO>;
|
||||
let chainSettings: Partial<ChainSettings>;
|
||||
let imageStorage: Partial<ImageStorage>;
|
||||
let router: Partial<Router>;
|
||||
|
||||
const dataSubject = new BehaviorSubject({ chain: CHAIN_MOCK });
|
||||
beforeEach(async () => {
|
||||
activatedRoute = {
|
||||
data: dataSubject,
|
||||
};
|
||||
chainDAO = {
|
||||
update: vi.fn().mockResolvedValue({
|
||||
rows: [],
|
||||
}),
|
||||
chainSettings = {
|
||||
update: vi.fn(),
|
||||
};
|
||||
imageStorage = {
|
||||
getImage: vi.fn(),
|
||||
saveImage: vi.fn(),
|
||||
deleteImage: vi.fn(),
|
||||
};
|
||||
router = {
|
||||
navigate: vi.fn(),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ChainEdit],
|
||||
providers: [
|
||||
provideTranslateService(),
|
||||
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||
{ provide: ChainDAO, useValue: chainDAO },
|
||||
{ provide: ChainSettings, useValue: chainSettings },
|
||||
{ provide: ImageStorage, useValue: imageStorage },
|
||||
{ provide: Router, useValue: router },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
@@ -59,44 +51,29 @@ describe('ChainEdit', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('on form update', () => {
|
||||
it('should update chain name after modifying name', async () => {
|
||||
await fixture.whenStable();
|
||||
const chainNameInput = fixture.debugElement.query(By.css('[formControlName="name"]'));
|
||||
chainNameInput.triggerEventHandler('input', { target: { value: 'name update mock' } });
|
||||
fixture.whenStable();
|
||||
const actionBtn = fixture.debugElement.query(By.css('app-action-btn'));
|
||||
actionBtn.triggerEventHandler('click');
|
||||
await fixture.whenStable();
|
||||
expect(chainDAO.update).toHaveBeenCalledExactlyOnceWith(component['chain'], {
|
||||
id: CHAIN_MOCK.id,
|
||||
});
|
||||
expect(router.navigate).toHaveBeenCalledExactlyOnceWith(CHAINS_PATH);
|
||||
});
|
||||
it('should call chainSettings update on chain update', async () => {
|
||||
const CHAIN_NAME_UPDATE_MOCK = 'name update mock';
|
||||
await fixture.whenStable();
|
||||
const chainNameInput = fixture.debugElement.query(By.css('[formControlName="name"]'));
|
||||
chainNameInput.triggerEventHandler('input', { target: { value: CHAIN_NAME_UPDATE_MOCK } });
|
||||
fixture.whenStable();
|
||||
const actionBtn = fixture.debugElement.query(By.css('app-action-btn'));
|
||||
actionBtn.triggerEventHandler('click');
|
||||
await fixture.whenStable();
|
||||
expect(chainSettings.update).toHaveBeenCalledExactlyOnceWith(
|
||||
CHAIN_MOCK,
|
||||
new Chain(CHAIN_NAME_UPDATE_MOCK, component['chain']!.image, component['chain']!.id),
|
||||
component.form.controls.image.value,
|
||||
);
|
||||
});
|
||||
|
||||
it('should update chain image after modifying image', async () => {
|
||||
const NEW_IMG_MOCK = new File([], 'new_img_mock.png', { type: 'image/png' });
|
||||
const CHAIN_MOCK = new Chain('Mock', 'img_mock.jpeg', 1);
|
||||
imageStorage.getImage = vi
|
||||
.fn()
|
||||
.mockResolvedValue(new File([], CHAIN_MOCK.image!, { type: 'image/jpeg' }));
|
||||
imageStorage.saveImage = vi.fn().mockResolvedValue(NEW_IMG_MOCK.name);
|
||||
|
||||
dataSubject.next({ chain: CHAIN_MOCK });
|
||||
await fixture.whenStable();
|
||||
//User's select a new image
|
||||
component.form.controls.image.patchValue(NEW_IMG_MOCK);
|
||||
await fixture.whenStable();
|
||||
const actionBtn = fixture.debugElement.query(By.css('app-action-btn'));
|
||||
actionBtn.triggerEventHandler('click');
|
||||
await fixture.whenStable();
|
||||
expect(imageStorage.getImage).toHaveBeenCalledWith(CHAIN_MOCK.image);
|
||||
expect(imageStorage.saveImage).toHaveBeenCalledExactlyOnceWith(NEW_IMG_MOCK);
|
||||
expect(imageStorage.deleteImage).toHaveBeenCalledExactlyOnceWith(CHAIN_MOCK.image);
|
||||
expect(chainDAO.update).toHaveBeenCalledExactlyOnceWith(component['chain'], {
|
||||
id: CHAIN_MOCK.id,
|
||||
});
|
||||
expect(router.navigate).toHaveBeenCalledExactlyOnceWith(CHAINS_PATH);
|
||||
});
|
||||
it('should patch form with chain data', async () => {
|
||||
const IMAGE_FILE_MOCK = new Blob([], { type: 'image/png' });
|
||||
imageStorage.getImage = vi.fn().mockResolvedValue(IMAGE_FILE_MOCK);
|
||||
const CHAIN_MOCK = new Chain('name', 'image.png', 1);
|
||||
dataSubject.next({ chain: CHAIN_MOCK });
|
||||
await fixture.whenStable();
|
||||
expect(component.form.controls.name.value).toEqual(CHAIN_MOCK.name);
|
||||
expect(component.form.controls.image.value?.type).toEqual(IMAGE_FILE_MOCK.type);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { ChangeDetectorRef, Component, inject, OnInit } from '@angular/core';
|
||||
import { ChainFormGroup } from '../chain-formgroup';
|
||||
import { ChainForm } from '../chain-form/chain-form';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable, take, tap } from 'rxjs';
|
||||
import { Chain } from '../../../../models/Chain';
|
||||
import { ImageStorage } from '../../../../services/image-storage';
|
||||
import { ActionBtn } from '../../../../components/action-btn/action-btn';
|
||||
import { ChainDAO } from '../../../../dao/ChainDAO';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { UpperfirstPipe } from "../../../../pipes/upperfirst-pipe";
|
||||
import { UpperfirstPipe } from '../../../../pipes/upperfirst-pipe';
|
||||
import { ChainSettings } from '../../../../services/chain-settings';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chain-edit',
|
||||
@@ -21,9 +21,8 @@ export class ChainEdit implements OnInit {
|
||||
protected readonly activatedRoute = inject(ActivatedRoute);
|
||||
private readonly cd = inject(ChangeDetectorRef);
|
||||
private chain?: Chain;
|
||||
private readonly chainDAO = inject(ChainDAO);
|
||||
private readonly imageStorage = inject(ImageStorage);
|
||||
private readonly router = inject(Router);
|
||||
private readonly chainSettings = inject(ChainSettings);
|
||||
|
||||
ngOnInit() {
|
||||
(<Observable<{ chain: Chain }>>this.activatedRoute.data)
|
||||
@@ -36,7 +35,7 @@ export class ChainEdit implements OnInit {
|
||||
}
|
||||
|
||||
get disabled() {
|
||||
return (this.form.invalid || this.form.pristine)
|
||||
return this.form.invalid || this.form.pristine;
|
||||
}
|
||||
|
||||
async patchForm(chain: Chain) {
|
||||
@@ -51,31 +50,16 @@ export class ChainEdit implements OnInit {
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
//TODO: reportar error
|
||||
//TODO: reportar error
|
||||
}
|
||||
}
|
||||
|
||||
async update() {
|
||||
try {
|
||||
if (this.chain) {
|
||||
this.chain.name = this.form.controls.name.value;
|
||||
const formImgFile = this.form.controls.image.value;
|
||||
let imgName = this.chain.name;
|
||||
if (formImgFile) {
|
||||
if (formImgFile.name !== imgName) {
|
||||
imgName = await this.imageStorage.saveImage(formImgFile);
|
||||
if (this.chain.image) {
|
||||
this.imageStorage.deleteImage(this.chain.image);
|
||||
}
|
||||
this.chain.image = imgName;
|
||||
}
|
||||
}
|
||||
await this.chainDAO.update(this.chain, { id: this.chain.id });
|
||||
this.router.navigate(['settings', 'chains']);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
//TODO: reportar error
|
||||
if (this.chain) {
|
||||
const updatedName = this.form.controls.name.value;
|
||||
const updatedImg = this.form.controls.image.value;
|
||||
const updatedChain = new Chain(updatedName, '', this.chain.id);
|
||||
await this.chainSettings.update(this.chain, updatedChain, updatedImg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
74
src/app/services/chain-settings.spec.ts
Normal file
74
src/app/services/chain-settings.spec.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ChainSettings } from './chain-settings';
|
||||
import { ImageStorage } from './image-storage';
|
||||
import { ChainDAO } from '../dao/ChainDAO';
|
||||
import { Router } from '@angular/router';
|
||||
import { Chain } from '../models/Chain';
|
||||
|
||||
const CHAIN_SETTINGS_PATH = ['settings', 'chains'];
|
||||
describe('ChainSettings', () => {
|
||||
let service: ChainSettings;
|
||||
|
||||
let chainDAO: Partial<ChainDAO>;
|
||||
let imageStorage: Partial<ImageStorage>;
|
||||
let router: Partial<Router>;
|
||||
|
||||
beforeEach(() => {
|
||||
chainDAO = {
|
||||
update: vi.fn().mockResolvedValue({
|
||||
rows: [],
|
||||
}),
|
||||
insert: vi.fn().mockResolvedValue({
|
||||
rows: [],
|
||||
}),
|
||||
};
|
||||
|
||||
imageStorage = {
|
||||
saveImage: vi.fn().mockResolvedValue('mock.jpg'),
|
||||
deleteImage: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
router = {
|
||||
navigate: vi.fn(),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: ChainDAO, useValue: chainDAO },
|
||||
{ provide: ImageStorage, useValue: imageStorage },
|
||||
{ provide: Router, useValue: router },
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(ChainSettings);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should save chain and store image', async () => {
|
||||
const CHAIN_IMG_MOCK = new File([], 'image.jpg', { type: 'image/jpeg' });
|
||||
const CHAIN_MOCK = new Chain('mock', '');
|
||||
imageStorage.saveImage = vi.fn().mockResolvedValue(CHAIN_IMG_MOCK.name);
|
||||
|
||||
await service.save(CHAIN_MOCK, CHAIN_IMG_MOCK);
|
||||
expect(imageStorage.saveImage).toHaveBeenCalledExactlyOnceWith(CHAIN_IMG_MOCK);
|
||||
expect(chainDAO.insert).toHaveBeenCalledExactlyOnceWith(CHAIN_MOCK);
|
||||
expect(router.navigate).toHaveBeenCalledExactlyOnceWith(CHAIN_SETTINGS_PATH);
|
||||
});
|
||||
|
||||
it('should update chain and its image', async () => {
|
||||
const MODIFIED_IMG_MOCK = new File([], 'new_image.jpg', { type: 'image/jpeg' });
|
||||
const PREV_CHAIN_MOCK = new Chain('prev chain name', 'prev_img.png');
|
||||
const MODIFIED_CHAIN_MOCK = new Chain('new chain name', '');
|
||||
await service.update(PREV_CHAIN_MOCK, MODIFIED_CHAIN_MOCK, MODIFIED_IMG_MOCK);
|
||||
expect(imageStorage.saveImage).toHaveBeenCalledExactlyOnceWith(MODIFIED_IMG_MOCK);
|
||||
expect(imageStorage.deleteImage).toHaveBeenCalledExactlyOnceWith(PREV_CHAIN_MOCK.image);
|
||||
expect(MODIFIED_CHAIN_MOCK.image).toEqual('mock.jpg');
|
||||
expect(chainDAO.update).toHaveBeenCalledExactlyOnceWith(MODIFIED_CHAIN_MOCK, {
|
||||
id: MODIFIED_CHAIN_MOCK.id,
|
||||
});
|
||||
expect(router.navigate).toHaveBeenCalledExactlyOnceWith(CHAIN_SETTINGS_PATH);
|
||||
});
|
||||
});
|
||||
48
src/app/services/chain-settings.ts
Normal file
48
src/app/services/chain-settings.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { ChainDAO } from '../dao/ChainDAO';
|
||||
import { ImageStorage } from './image-storage';
|
||||
import { Router } from '@angular/router';
|
||||
import { Chain } from '../models/Chain';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ChainSettings {
|
||||
private readonly chainDAO = inject(ChainDAO);
|
||||
private readonly imageStorage = inject(ImageStorage);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
async save(chain: Chain, img: File | null) {
|
||||
try {
|
||||
if (img) {
|
||||
const imgFileName = await this.imageStorage.saveImage(img);
|
||||
chain.image = imgFileName;
|
||||
}
|
||||
await this.chainDAO.insert(chain);
|
||||
this.router.navigate(['settings', 'chains']);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
//TODO: report problem
|
||||
}
|
||||
}
|
||||
|
||||
async update(prevChain: Chain, updatedChain: Chain, img: File | null) {
|
||||
try {
|
||||
let imgName = prevChain.name;
|
||||
if (img) {
|
||||
if (img.name !== imgName) {
|
||||
imgName = await this.imageStorage.saveImage(img);
|
||||
if (prevChain.image) {
|
||||
this.imageStorage.deleteImage(prevChain.image);
|
||||
}
|
||||
updatedChain.image = imgName;
|
||||
}
|
||||
}
|
||||
await this.chainDAO.update(updatedChain, { id: updatedChain.id });
|
||||
this.router.navigate(['settings', 'chains']);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
//TODO: report problem
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user