-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
- Paleo
+
+
+
-
-
-
+
+
+
-
-
-
-
\ No newline at end of file
+
diff --git a/libs/app/create/feature/src/create.page.spec.ts b/libs/app/create/feature/src/create.page.spec.ts
index 83b4de87..8e5bd75d 100644
--- a/libs/app/create/feature/src/create.page.spec.ts
+++ b/libs/app/create/feature/src/create.page.spec.ts
@@ -1,45 +1,30 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { FormArray, FormBuilder, FormControl, FormGroup, ReactiveFormsModule, UntypedFormBuilder} from '@angular/forms';
+import { FormArray, FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import { CreatePagComponent } from './create.page';
import { IonicModule } from '@ionic/angular';
import {HttpClientModule } from '@angular/common/http';
import { NavigationBarModule } from '@fridge-to-plate/app/navigation/feature'
import { IIngredient } from '@fridge-to-plate/app/ingredient/utils';
-import { IRecipe, IRecipeStep } from '@fridge-to-plate/app/recipe/utils';
-import { NEVER, of } from "rxjs";
-import { CreateAPI } from '../../data-access/src/api/create.api';
+import { IRecipe } from '@fridge-to-plate/app/recipe/utils';
+import { BehaviorSubject, take } from "rxjs";
+import { Injectable } from '@angular/core';
+import { NgxsModule, State, Store } from '@ngxs/store';
+import { IProfile } from '@fridge-to-plate/app/profile/utils';
+import { CreateRecipe } from '@fridge-to-plate/app/recipe/utils';
+import { ShowError } from '@fridge-to-plate/app/error/utils';
+
+
+@State({
+ name: 'create',
+ defaults: {
+ recipe: null,
+ }
+})
-describe('CreatePage', () => {
- let component: CreatePagComponent;
- let fixture: ComponentFixture
;
+@Injectable()
+class MockCreateState {}
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- declarations: [ CreatePagComponent ],
- imports: [
- ReactiveFormsModule,
- IonicModule,
- HttpClientModule,
- NavigationBarModule
- ],
- providers: [ FormBuilder ]
- })
- .compileComponents();
- });
- beforeEach(() => {
- fixture = TestBed.createComponent(CreatePagComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should add a new instruction control to the form', () => {
- const initialLength = component.instructionControls.length;
- component.addInstruction();
- const newLength = component.instructionControls.length;
- expect(newLength).toBe(initialLength + 1);
- });
-});
describe('CreatePagComponent', () => {
let createPage: CreatePagComponent;
@@ -52,7 +37,8 @@ describe('CreatePagComponent', () => {
ReactiveFormsModule,
IonicModule,
HttpClientModule,
- NavigationBarModule
+ NavigationBarModule,
+ NgxsModule.forRoot([MockCreateState])
],
providers: [ FormBuilder ]
})
@@ -65,18 +51,6 @@ describe('CreatePagComponent', () => {
fixture.detectChanges();
});
- it('should create a recipe form with the correct fields', () => {
- createPage.createForm();
-
- expect(createPage.recipeForm.contains('name')).toBe(true);
- expect(createPage.recipeForm.contains('description')).toBe(true);
- expect(createPage.recipeForm.contains('servings')).toBe(true);
- expect(createPage.recipeForm.contains('preparationTime')).toBe(true);
- expect(createPage.recipeForm.contains('ingredients')).toBe(true);
- expect(createPage.recipeForm.contains('instructions')).toBe(true);
- expect(createPage.recipeForm.contains('dietaryPlans')).toBe(true);
- });
-
it('should set the name, description, servings, and preparationTime fields as required', () => {
createPage.createForm();
@@ -101,15 +75,6 @@ describe('CreatePagComponent', () => {
expect(instructionsArray?.value).toEqual([]);
});
- it('should create an empty array for the dietaryPlans field', () => {
- createPage.createForm();
-
- const dietaryPlansArray = createPage.recipeForm.get('dietaryPlans');
-
- expect(dietaryPlansArray?.value).toEqual([]);
- });
-
-
it('should add a new ingredient control to the form', () => {
const initialLength = createPage.ingredientControls.length;
createPage.addIngredient();
@@ -139,40 +104,60 @@ describe('CreatePagComponent', () => {
);
+ it('get instruction steps as String[]', () => {
+ const formArray = new FormArray([
+ new FormControl('Step 1'),
+ new FormControl('Step 2'),
+ new FormControl('Step 3'),
+ ]);
+
+ // create a new recipe form using the form array
+ const recipeForm = new FormGroup({
+ instructions: formArray,
+ });
+
+ createPage.recipeForm = recipeForm;
+
+ const instructions = createPage.getInstructions();
+
+ expect(instructions[0]).toBe('Step 1');
+ expect(instructions[1]).toBe('Step 2');
+ expect(instructions[2]).toBe('Step 3');
+ })
+
+
it('should remove an instruction control from the form', () => {
+
+ const formArray = new FormArray([
+ new FormControl('Step 1'),
+ new FormControl('Step 2'),
+ new FormControl('Step 3'),
+ ]);
+
+ // create a new recipe form using the form array
+ const recipeForm = new FormGroup({
+ instructions: formArray,
+ });
+
+ createPage.recipeForm = recipeForm;
+
const initialLength = createPage.instructionControls.length;
- if(initialLength == 0) {
- expect(initialLength).toBe(0)
- return
- }
createPage.removeInstruction(0);
const newLength = createPage.instructionControls.length;
expect(newLength).toBe(initialLength - 1);
- }
- );
-
- it('should add a new dietary plan to the form', () => {
- const initialLength = createPage.dietaryPlans.length;
- createPage.toggleDietaryPlan('Vegan');
- const newLength = createPage.dietaryPlans.length;
- expect(newLength).toBe(initialLength + 1);
- }
- );
-
- it('should remove a dietary plan from the form', () => {
- const initialLength = createPage.dietaryPlans.length;
- createPage.toggleDietaryPlan('Vegan');
- createPage.toggleDietaryPlan('Vegan');
- const newLength = createPage.dietaryPlans.length;
- expect(newLength).toBe(initialLength);
+ expect(createPage.getInstructions()).toEqual(['Step 2', 'Step 3'])
}
);
});
-describe('toggleDietaryPlan', () => {
+
+describe('Testing Tags', () => {
let component: CreatePagComponent;
let fb: FormBuilder;
+ let fixture: ComponentFixture;
+ let store: Store;
+ let dispatchSpy: jest.SpyInstance;
beforeEach(() => {
TestBed.configureTestingModule({
@@ -181,87 +166,160 @@ describe('toggleDietaryPlan', () => {
imports: [
ReactiveFormsModule,
HttpClientModule,
- NavigationBarModule
+ NavigationBarModule,
+ NgxsModule.forRoot([MockCreateState])
]
});
- component = TestBed.createComponent(CreatePagComponent).componentInstance;
+
+ fixture = TestBed.createComponent(CreatePagComponent);
+ component = fixture.componentInstance;
fb = TestBed.inject(FormBuilder);
+ fixture.detectChanges();
+ store = TestBed.inject(Store);
+ dispatchSpy = jest.spyOn(store, 'dispatch');
+
component.recipeForm = fb.group({
- dietaryPlans: fb.array([]),
+ tag: ['', Validators.required],
});
});
- it('should remove the dietary plan if it is already selected', () => {
+ it("Should selet a meal type successfully", () => {
+ const mealType = 'Breakfast';
+ component.selectedMeal = mealType;
+ jest.spyOn(component, 'toggleMeal');
- const plan = 'Vegetarian';
- const dietaryPlans = component.recipeForm.get('dietaryPlans') as FormArray;
- dietaryPlans.push(fb.control(plan));
+ // Act
+ component.toggleMeal(mealType);
- component.toggleDietaryPlan(plan);
+ // Assert
+ expect(component.selectedMeal).toBe(mealType)
+ expect(component.toggleMeal).toBeCalledWith(mealType)
+ })
- expect(dietaryPlans.length).toBe(0);
- });
+ it("The selected meals should change when the user changes", () => {
+
+ const mealType = 'Lunch';
+ component.selectedMeal = mealType;
- it('should add the dietary plan if it is not selected', () => {
+ // Act
+ const mealType2 = 'Dinner';
+ // Act
+ component.toggleMeal(mealType2);
- const plan = 'Vegan';
- const dietaryPlans = component.recipeForm.get('dietaryPlans') as FormArray;
+ // Assert
+ expect(component.selectedMeal).toBe(mealType2);
+ expect(component.selectedMeal).not.toBe(mealType);
+
+ })
- component.toggleDietaryPlan(plan);
+ it("Should selet a difficulty successfully", () => {
+ const difficulty = 'Easy';
+ component.difficulty = difficulty;
+ jest.spyOn(component, 'toggleDifficulty');
- expect(dietaryPlans.length).toBe(1);
- expect(dietaryPlans.value).toContain(plan);
+ // Act
+ component.toggleDifficulty(difficulty);
+
+ // Assert
+ expect(component.difficulty).toBe(difficulty);
+ expect(component.toggleDifficulty).toBeCalledWith(difficulty);
+ expect(component.difficulty).toBe(difficulty);
+ expect(component.toggleDifficulty).toBeCalledWith(difficulty);
+ })
+
+ it("The selected difficulty should change when the user changes", () => {
+
+ const difficulty1 = 'Easy';
+ component.difficulty = difficulty1;
+
+ // Act
+ const difficulty2 = 'Medium';
+ // Act
+ component.toggleDifficulty(difficulty2);
+
+ // Assert
+ expect(component.difficulty).toBe(difficulty2);
+ expect(component.difficulty).not.toBe(difficulty1);
+
+ })
+
+ it('should not add a tag if tagValue is empty', () => {
+ // Arrange
+ component.recipeForm.get('tag')?.setValue('');
+ const size = component.tags.length;
+ // Act
+ component.addTag();
+
+ // Assert
+ expect(component.tags.length).toBe(size);
+ expect(dispatchSpy).toHaveBeenCalledWith(new ShowError('Please enter valid tag'));
});
+ it('should not add a duplicate tags', () => {
+ // Arrange
+ component.recipeForm.get('tag')?.setValue('Tag 1');
+ const testTags = ['Tag 1'];
+ component.tags = testTags;
+ const size = component.tags.length;
- it('Returns an array of instruction controls', () => {
- const formArray = new FormArray([
- new FormControl('Step 1'),
- new FormControl('Step 2'),
- new FormControl('Step 3'),
- ]);
+ // Act
+ component.addTag();
- // create a new recipe form using the form array
- const recipeForm = new FormGroup({
- instructions: formArray,
- });
+ // Assert
+ expect(component.tags.length).toBe(size);
+ expect(dispatchSpy).toHaveBeenCalledWith(new ShowError('No duplicates: Tag already selected'));
+ expect(component.tags).toEqual(testTags);
- component.recipeForm = recipeForm;
+ });
+
+ it('Should not add if tags is already at size three(3)', () => {
+ // Arrange
+ component.recipeForm.get('tag')?.setValue('Tag 4');
+ const testTags = ['Tag 1', 'Tag 2', 'Tag 3'];
+ component.tags = testTags;
+
+ // Act
+ component.addTag();
- const controls = component.instructionControls;
- expect(controls.length).toBe(3);
- expect(controls[0] instanceof FormControl).toBe(true);
- expect(controls[1] instanceof FormControl).toBe(true);
- expect(controls[2] instanceof FormControl).toBe(true);
-
+ // Assert
+ expect(component.tags.length).toBe(3);
+ expect(dispatchSpy).toHaveBeenCalledWith(new ShowError('Only a maximum of three tags'));
+ expect(component.tags).toEqual(testTags);
})
- it('Returns an array of ingredients controls', () => {
- const formArray = new FormArray([
- new FormControl('Mango'),
- new FormControl('Potato'),
- new FormControl('Banana'),
- ]);
+ it('should add a tag if tagValue is not empty', () => {
+ // Arrange
+ component.recipeForm.get('tag')?.setValue('Tag 1');
- // create a new recipe form using the form array
- const recipeForm = new FormGroup({
- ingredients: formArray,
- });
+ // Act
+ component.addTag();
+
+ const testTagsOutput = ['Tag 1'];
+ // Assert
+ expect(component.tags.length).toBe(1);
+ expect(component.tags).toEqual(testTagsOutput);
+ });
+
+ it("Should delete a meal tag successfully", () => {
+
+ const testTags = ['Tag 1', 'Tag 2', 'Tag 3'];
+ component.tags = testTags;
- component.recipeForm = recipeForm;
+ component.deleteTag(0);
- const controls = component.ingredientControls;
- expect(controls.length).toBe(3);
- expect(controls[0] instanceof FormControl).toBe(true);
- expect(controls[1] instanceof FormControl).toBe(true);
- expect(controls[2] instanceof FormControl).toBe(true);
-
+ const testTagsOutput = ['Tag 2', 'Tag 3'];
+ // Assert
+ expect(component.tags.length).toBe(2);
+ expect(component.tags).toEqual(testTagsOutput);
})
+
});
-describe('Testing Tags', () => {
+
+describe('Ingredients storing, deleting and returning', () => {
let component: CreatePagComponent;
- let fb: FormBuilder;
+ let fixture: ComponentFixture;
+ let formBuilder: FormBuilder;
beforeEach(() => {
TestBed.configureTestingModule({
@@ -270,140 +328,192 @@ describe('Testing Tags', () => {
imports: [
ReactiveFormsModule,
HttpClientModule,
- NavigationBarModule
+ NavigationBarModule,
+ NgxsModule.forRoot([MockCreateState])
]
});
+ fixture = TestBed.createComponent(CreatePagComponent);
+ component = fixture.componentInstance;
+ formBuilder = TestBed.inject(FormBuilder);
+
+ fixture.detectChanges();
- component = TestBed.createComponent(CreatePagComponent).componentInstance;
- fb = TestBed.inject(FormBuilder);
- component.recipeForm = fb.group({
- dietaryPlans: fb.array([]),
});
- });
- it('It should not add null values to tags array', () => {
+ it('Gets an array of IIngredient objects ', () => {
+ // create a mock form array with some form controls
+ const formArray = new FormArray([
+ new FormControl({
+ name: 'Mango',
+ amount: 100,
+ unit: 'g'
+ }),
+ new FormControl({
+ name: 'Potato',
+ amount: 1,
+ unit: 'kg'
+ }),
+ new FormControl({
+ name: 'Banana',
+ amount: 300,
+ unit: 'g'
+ }),
+ new FormControl({
+ name: 'Salad',
+ amount: 100,
+ unit: 'g'
+ }),
+ new FormControl({
+ name: 'Onion',
+ amount: 1,
+ unit: 'whole'
+ }),
+ ]);
- const plan1 = 'Vegetarian';
- const plan2 = 'Vegan';
- const plan3 = null;
- const dietaryPlans = component.recipeForm.get('dietaryPlans') as FormArray;
- dietaryPlans.push(fb.control(plan1));
- dietaryPlans.push(fb.control(plan2));
- dietaryPlans.push(fb.control(plan3));
- const tags = [];
+ // create a new recipe form using the form array
+ const recipeForm = new FormGroup({
+ ingredients: formArray,
+ });
- for (let index = 0; index < dietaryPlans.length; index++) {
- if(dietaryPlans.controls[index].value !== null){
- tags.push(dietaryPlans.controls[index].value)
- }
-
- }
+ component.recipeForm = recipeForm;
+
+ const ingredients : IIngredient[] = component.getIngredients();
+
+
+ // assert that the instructions array was created correctly
+ expect(ingredients[0]).toEqual({ name: "Mango", amount: 100, unit: "g" });
+ expect(ingredients[1]).toEqual({ name: "Potato", amount: 1, unit: "kg" })
+ expect(ingredients[2]).toEqual({ name: "Banana", amount: 300, unit: "g" })
+ expect(ingredients[3]).toEqual({ name: "Salad", amount: 100, unit: "g" })
+ expect(ingredients[4]).toEqual({ name: "Onion", amount: 1, unit: "whole" })
+
+ })
+
+ it('should remove the ingredient at the specified index', () => {
+
+ component.recipeForm = formBuilder.group({
+ ingredients: formBuilder.array([
+ formBuilder.group({
+ name: ['Ingredient 1', Validators.required],
+ amount: [1, Validators.required],
+ scale: ['kg', Validators.required],
+ }),
+ formBuilder.group({
+ name: ['Ingredient 2', Validators.required],
+ amount: [2, Validators.required],
+ scale: ['g', Validators.required],
+ }),
+ ]),
+ });
- expect(tags.length).toBe(2);
- expect(tags).not.toContain(null);
- expect(tags).toContain("Vegetarian");
- expect(tags).toContain("Vegan");
-
+ // Arrange
+ const indexToRemove = 1;
+ const initialIngredientsCount = component.ingredientControls.length;
+
+ // Act
+ component.removeIngredient(indexToRemove);
+
+ // Assert
+ const finalIngredientsCount = component.ingredientControls.length;
+ expect(finalIngredientsCount).toBe(initialIngredientsCount - 1);
+ expect(component.ingredientControls[1]).toBeUndefined();
});
-});
+ });
-describe('Ingredients storing and return', () => {
- let component: CreatePagComponent;
- let apiService: jest.Mocked;
- beforeEach(() => {
- TestBed.configureTestingModule({
- declarations: [ CreatePagComponent ],
- providers: [FormBuilder],
- imports: [
- ReactiveFormsModule,
- HttpClientModule,
- NavigationBarModule
- ]
+ describe("Testing placeholder texts for Amount", () => {
+
+ let component: CreatePagComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [ CreatePagComponent ],
+ providers: [FormBuilder],
+ imports: [
+ ReactiveFormsModule,
+ HttpClientModule,
+ NavigationBarModule,
+ NgxsModule.forRoot([MockCreateState])
+ ]
+ });
+ fixture = TestBed.createComponent(CreatePagComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
});
- component = TestBed.createComponent(CreatePagComponent).componentInstance;
- apiService = TestBed.inject(CreateAPI) as jest.Mocked;
+ it('should return "e.g 10" when window width is less than 1024', () => {
+ // Arrange
+ global.innerWidth = 800; // Set the window width to a value less than 1024
+
+ // Act
+ const placeholderText = component.getAmountPlaceholderText();
+
+ // Assert
+ expect(placeholderText).toBe('e.g 10');
});
- it('Create Ingredients', () => {
- // Mock data
- const expectData = {
- ingredientId : "123",
- name: "Chicken Falaty"
- }
+ it('should return "Amount" when window width is greater than or equal to 1024', () => {
+ // Arrange
+ global.innerWidth = 1200; // Set the window width to a value greater than or equal to 1024
- // Mocking the service
- const mockIngredients : IIngredient[] = [];
- mockIngredients.push(expectData)
+ // Act
+ const placeholderText = component.getAmountPlaceholderText();
- const mockApi = {
- createNewMultipleIngredients: jest.fn().mockReturnValue(mockIngredients),
- };
+ // Assert
+ expect(placeholderText).toBe('Amount');
+ });
+ })
- const testObject = { api: mockApi };
- const returnIngredients = testObject.api.createNewMultipleIngredients()
+ describe("Testing placeholder texts for Unit", () => {
- expect(returnIngredients[0]).toEqual(expectData);
+ let component: CreatePagComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [ CreatePagComponent ],
+ providers: [FormBuilder],
+ imports: [
+ ReactiveFormsModule,
+ HttpClientModule,
+ NavigationBarModule,
+ NgxsModule.forRoot([MockCreateState])
+ ]
+ });
+ fixture = TestBed.createComponent(CreatePagComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
});
- it("should call the createNewMultipleIngredients method on the ApiService object with the correct arguments", async () => {
- // Create a mock array of IIngredient objects
- const ingredients: IIngredient[] = [
- { name: "Ingredient 1" },
- { name: "Ingredient 2" },
- ];
-
- // Set up the mock response from the createNewMultipleIngredients method
- const response: IIngredient[] = [
- { ingredientId: "1", name: "Ingredient 1" },
- { ingredientId: "2", name: "Ingredient 2" },
- ];
- apiService.createNewMultipleIngredients = jest.fn().mockResolvedValue(response);
-
- // Call the createIngredients method and wait for it to resolve
- apiService.createNewMultipleIngredients(ingredients);
-
- // Verify that the createNewMultipleIngredients method was called on the ApiService object with the correct arguments
- expect(apiService.createNewMultipleIngredients).toHaveBeenCalledWith(ingredients);
+ it('should return "e.g 10" when window width is less than 1024', () => {
+ // Arrange
+ global.innerWidth = 800; // Set the window width to a value less than 1024
+
+ // Act
+ const placeholderText = component.getUnitPlaceholderText();
+
+ // Assert
+ expect(placeholderText).toBe('e.g L');
});
-
-
- // Assuming the ApiService is using `rxjs` Observables
-
- it("should resolve the promise with the correct response", async () => {
- // Create a mock array of IIngredient objects
- const ingredients: IIngredient[] = [
- { name: "Ingredient 1" },
- { name: "Ingredient 2" },
- ];
-
- // Set up the mock response from the createNewMultipleIngredients method
- const response: IIngredient[] = [
- { ingredientId: "1", name: "Ingredient 1" },
- { ingredientId: "2", name: "Ingredient 2" },
- ];
-
- // Mock the createNewMultipleIngredients method to return an observable
- apiService.createNewMultipleIngredients = jest.fn().mockReturnValue(of(response));
-
- // Call the createIngredients method and wait for it to resolve
- const result = await component.createIngredients(ingredients);
-
- // Verify that the promise resolves to the correct response
- expect(result).toEqual(response);
- });
-
-
- });
+ it('should return "Amount" when window width is greater than or equal to 1024', () => {
+ // Arrange
+ global.innerWidth = 1200; // Set the window width to a value greater than or equal to 1024
+ // Act
+ const placeholderText = component.getUnitPlaceholderText();
+
+ // Assert
+ expect(placeholderText).toBe('Unit');
+ });
+ })
+
+ describe("Image upload", () => {
- describe("Testing Recipe Creation", () => {
let component: CreatePagComponent;
- let fb: FormBuilder;
- let apiService: jest.Mocked
let fixture: ComponentFixture;
+
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ CreatePagComponent ],
@@ -411,189 +521,462 @@ describe('Ingredients storing and return', () => {
imports: [
ReactiveFormsModule,
HttpClientModule,
- NavigationBarModule
+ NavigationBarModule,
+ NgxsModule.forRoot([MockCreateState])
]
});
fixture = TestBed.createComponent(CreatePagComponent);
component = fixture.componentInstance;
- apiService = TestBed.inject(CreateAPI) as jest.Mocked;
- fb = TestBed.inject(FormBuilder);
- component.recipeForm = fb.group({
- dietaryPlans: fb.array([]),
+ fixture.detectChanges();
+ });
+
+ it('should update the imageUrl when a file is selected', () => {
+ // Arrange
+ const file = new File(['sample content'], 'sample.jpg', { type: 'image/jpeg' });
+ const event = { target: { files: [file] } };
+ const existingImage = component.imageUrl;
+
+ const readAsDataURLStringSpy = jest.spyOn(FileReader.prototype, 'readAsDataURL');
+
+ // Act
+ component.onFileChanged(event);
+
+ // Assert
+ expect(readAsDataURLStringSpy).toHaveBeenCalledWith(file);
+
+ const reader = new FileReader();
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ reader.addEventListener("load", function(event) {
+ expect(component.imageUrl).toBe(file.name);
+ expect(component.imageUrl).not.toBe(existingImage);
});
});
- it('creates an array of IRecipeStep objects', () => {
- // create a mock form array with some form controls
- const formArray = new FormArray([
+ });
+
+ describe('isFormValid()', () =>{
+
+ let component: CreatePagComponent;
+ let fixture: ComponentFixture;
+ let store: Store;
+ let dispatchSpy: jest.SpyInstance;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [ CreatePagComponent ],
+ providers: [FormBuilder],
+ imports: [
+ ReactiveFormsModule,
+ HttpClientModule,
+ NavigationBarModule,
+ NgxsModule.forRoot([MockCreateState])
+ ]
+ });
+ fixture = TestBed.createComponent(CreatePagComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ store = TestBed.inject(Store);
+ dispatchSpy = jest.spyOn(store, 'dispatch');
+ });
+
+ it('At least one ingredient should present', () => {
+
+ const formBuilder: FormBuilder = new FormBuilder();
+
+ const formGroup: FormGroup = formBuilder.group({
+ name: ['Name', Validators.required],
+ description: ['Description', Validators.required],
+ servings: [1, Validators.required],
+ preparationTime: [1, Validators.required],
+ ingredients: new FormArray([])
+ })
+
+ component.recipeForm = formGroup;
+ component.isFormValid();
+
+ expect(dispatchSpy).toHaveBeenCalledWith(new ShowError('No Ingredients'));
+
+ })
+
+ it('At least one instruction step should present', () => {
+
+ const formBuilder: FormBuilder = new FormBuilder();
+ const ingredientsFormArray = new FormArray([
+ new FormControl({
+ name: 'Mango',
+ amount: 100,
+ unit: 'g'
+ })])
+
+ const formGroup: FormGroup = formBuilder.group({
+ name: ['Name', Validators.required],
+ description: ['Description', Validators.required],
+ servings: [1, Validators.required],
+ preparationTime: [1, Validators.required],
+ ingredients: ingredientsFormArray,
+ instructions: new FormArray([])
+ })
+
+ component.recipeForm = formGroup;
+ component.isFormValid();
+
+ expect(dispatchSpy).toHaveBeenCalledWith(new ShowError('No Instructions'));
+
+ })
+
+ it('Tags if empty', () => {
+ const formBuilder: FormBuilder = new FormBuilder();
+ const ingredientsFormArray = new FormArray([
+ new FormControl({
+ name: 'Mango',
+ amount: 100,
+ unit: 'g'
+ })])
+ const instructionsFormArray = new FormArray([
+ new FormControl('Step 1')
+ ]);
+
+ const formGroup: FormGroup = formBuilder.group({
+ name: ['Name', Validators.required],
+ description: ['Description', Validators.required],
+ servings: [1, Validators.required],
+ preparationTime: [1, Validators.required],
+ ingredients: ingredientsFormArray,
+ instructions: instructionsFormArray
+ })
+
+ component.recipeForm = formGroup;
+ component.isFormValid();
+ expect(dispatchSpy).toHaveBeenCalledWith(new ShowError('No Tags'));
+ });
+
+
+ it('Meal Selection', () => {
+ const formBuilder: FormBuilder = new FormBuilder();
+ const ingredientsFormArray = new FormArray([
+ new FormControl({
+ name: 'Mango',
+ amount: 100,
+ unit: 'g'
+ })])
+ const instructionsFormArray = new FormArray([
+ new FormControl('Step 1')
+ ]);
+
+ const formGroup: FormGroup = formBuilder.group({
+ ingredients: ingredientsFormArray,
+ instructions: instructionsFormArray
+ })
+
+ component.tags = ['Asian']
+
+ component.recipeForm = formGroup;
+ component.isFormValid();
+ expect(dispatchSpy).toHaveBeenCalledWith(new ShowError('Please select a meal'));
+
+ })
+
+
+ it('Truthy Profile', () => {
+ const formBuilder: FormBuilder = new FormBuilder();
+ const ingredientsFormArray = new FormArray([
+ new FormControl({
+ name: 'Mango',
+ amount: 100,
+ unit: 'g'
+ })])
+ const instructionsFormArray = new FormArray([
+ new FormControl('Step 1')
+ ]);
+
+ const formGroup: FormGroup = formBuilder.group({
+ name: ['Name', Validators.required],
+ description: ['Description', Validators.required],
+ servings: [1, Validators.required],
+ preparationTime: [1, Validators.required],
+ ingredients: ingredientsFormArray,
+ instructions: instructionsFormArray
+ })
+
+ component.recipeForm = formGroup;
+ component.tags = ['Asian'];
+ component.selectedMeal = 'Breakfast';
+ component.isFormValid();
+ expect(dispatchSpy).toHaveBeenCalledWith(new ShowError('Please login to create a recipe'));
+ })
+
+
+ it('Form fields validation', () => {
+ const formBuilder: FormBuilder = new FormBuilder();
+ const ingredientsFormArray = new FormArray([
+ new FormControl({
+ name: 'Mango',
+ amount: 100,
+ unit: 'g'
+ })])
+ const instructionsFormArray = new FormArray([
+ new FormControl('Step 1')
+ ]);
+
+ const formGroup: FormGroup = formBuilder.group({
+ name: ['', Validators.required],
+ description: ['', Validators.required],
+ servings: [1, Validators.required],
+ preparationTime: [1, Validators.required],
+ ingredients: ingredientsFormArray,
+ instructions: instructionsFormArray,
+ tags: formBuilder.array([]),
+ })
+
+ const testProfile: IProfile = {
+ displayName: "John Doe",
+ username: "jdoe",
+ email: "jdoe@gmail.com",
+ savedRecipes: [],
+ ingredients: [],
+ profilePic: "image-url",
+ createdRecipes: [],
+ currMealPlan: null,
+ };
+
+ component.recipeForm = formGroup;
+ component.selectedMeal = 'Breakfast';
+ component.tags = ['Asian'];
+ component.profile = testProfile;
+ component.isFormValid();
+ expect(dispatchSpy).toHaveBeenCalledWith(new ShowError('Incomplete Form. Please fill out every field.'))
+ })
+
+
+ it('The form should test valid', () => {
+ const formBuilder: FormBuilder = new FormBuilder();
+
+ const stepsFormArray = new FormArray([
new FormControl('Step 1'),
new FormControl('Step 2'),
new FormControl('Step 3'),
]);
-
- // create a mock form group with the form array
- const formGroup = new FormGroup({
- instructions: formArray,
+
+ const ingredientsFormArray = new FormArray([
+ new FormControl({
+ name: 'Mango',
+ amount: 100,
+ unit: 'g'
+ }),
+ new FormControl({
+ name: 'Potato',
+ amount: 1,
+ unit: 'kg'
+ }),
+ new FormControl({
+ name: 'Banana',
+ amount: 300,
+ unit: 'g'
+ }),
+ new FormControl({
+ name: 'Salad',
+ amount: 100,
+ unit: 'g'
+ }),
+ new FormControl({
+ name: 'Onion',
+ amount: 1,
+ unit: 'whole'
+ }),
+ ]);
+
+ const testProfile: IProfile = {
+ displayName: "John Doe",
+ username: "jdoe",
+ email: "jdoe@gmail.com",
+ savedRecipes: [],
+ ingredients: [],
+ profilePic: "image-url",
+ createdRecipes: [],
+ currMealPlan: null,
+ };
+
+ const formGroup: FormGroup = formBuilder.group({
+ name: ['Name', Validators.required],
+ description: ['Description', Validators.required],
+ servings: [1, Validators.required],
+ preparationTime: [1, Validators.required],
+ ingredients: ingredientsFormArray,
+ instructions: stepsFormArray,
+ tags: formBuilder.array([]),
});
-
- // create a new instance of the RecipeComponent
-
- // assign the mock form group to the component's recipeForm property
+
+ component.selectedMeal = 'Breakfast';
+ component.tags = ['Asian'];
+ component.profile = testProfile
+
component.recipeForm = formGroup;
-
- // call the createInstructions method and check the result
- ;
-
- const instructions: IRecipeStep[] = [];
- for (let index = 0; index < component.instructionControls.length; index++) {
- instructions.push({
- instructionHeading: 'N/A',
- instructionBody: component.instructionControls[index].value,
- });
- }
+ expect(component.isFormValid()).toBe(true);
+ })
- });
-
- it('creates an array of IIngredient objects', () => {
- // create a mock form array with some form controls
- const formArray = new FormArray([
- new FormControl('Mango'),
- new FormControl('Potato'),
- new FormControl('Banana'),
- new FormControl('Salad'),
- new FormControl('Onion'),
- ]);
-
- // create a new recipe form using the form array
- const recipeForm = new FormGroup({
- ingredients: formArray,
- });
-
- component.recipeForm = recipeForm;
-
- const controls = component.ingredientControls;
-
- const ingredients : IIngredient[] = [];
- for (let index = 0; index < controls.length; index++) {
- ingredients.push({
- name: controls[index].value,
- });
+ })
+
+ describe("Testing Recipe Creation", () => {
+ let component: CreatePagComponent;
+ let fb: FormBuilder;
+ let fixture: ComponentFixture;
+ let store: Store;
+ let dispatchSpy: jest.SpyInstance;
+
+ const testProfile: IProfile = {
+ displayName: "John Doe",
+ username: "jdoe",
+ email: "jdoe@gmail.com",
+ savedRecipes: [],
+ ingredients: [],
+ profilePic: "image-url",
+ createdRecipes: [],
+ currMealPlan: null,
+ };
+
+ @State({
+ name: 'profile',
+ defaults: {
+ profile: testProfile
}
-
- // assert that the instructions array was created correctly
- expect(ingredients[0]).toEqual({ name: "Mango",});
- expect(ingredients[1]).toEqual({ name: "Potato" })
- expect(ingredients[2]).toEqual({ name: "Banana" })
- expect(ingredients[3]).toEqual({ name: "Salad" })
- expect(ingredients[4]).toEqual({ name: "Onion" })
-
})
+ @Injectable()
+ class MockProfileState {}
+
- it("should reject the promise if the response is falsy", async () => {
- // Create a mock array of IIngredient objects
- const ingredients: IIngredient[] = [
- { name: "Ingredient 1" },
- { name: "Ingredient 2" },
- ];
-
- // Set up the mock response from the createNewMultipleIngredients method as falsy (empty array)
- let response!: IIngredient[];
- jest.spyOn(apiService, 'createNewMultipleIngredients').mockReturnValue(of(response));
-
- // Call the createIngredients method
- const result = component.createIngredients(ingredients);
- expect(result).toBeTruthy();
- // Await the promise rejection and verify the expected result
- await expect(result).rejects.toEqual(response);
-
- // Verify that the createNewMultipleIngredients method was called with the correct arguments
- expect(apiService.createNewMultipleIngredients).toHaveBeenCalledWith(ingredients);
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [ CreatePagComponent ],
+ providers: [FormBuilder, Store],
+ imports: [
+ ReactiveFormsModule,
+ HttpClientModule,
+ NavigationBarModule,
+ NgxsModule.forRoot([MockCreateState, MockProfileState])
+ ]
+ });
+ fixture = TestBed.createComponent(CreatePagComponent);
+ component = fixture.componentInstance;
+ fb = TestBed.inject(FormBuilder);
+ store = TestBed.inject(Store);
+ dispatchSpy = jest.spyOn(store, 'dispatch');
});
- it("should resolve the promise if the response is truthy", async () => {
- // Create a mock array of IIngredient objects
- const ingredients: IIngredient[] = [
- { name: "Ingredient 1" },
- { name: "Ingredient 2" },
- ];
-
- // Set up the mock response from the createNewMultipleIngredients method as truthy
- const response: IIngredient[] = [
- { ingredientId: "1", name: "Ingredient 1" },
- { ingredientId: "2", name: "Ingredient 2" },
- ];
- jest.spyOn(apiService, 'createNewMultipleIngredients').mockReturnValue(of(response));
-
- // Call the createIngredients method
- const result = component.createIngredients(ingredients);
- expect(result).toBeTruthy();
- // Await the promise resolution and verify the expected result
- await expect(result).resolves.toEqual(response);
-
- // Verify that the createNewMultipleIngredients method was called with the correct arguments
- expect(apiService.createNewMultipleIngredients).toHaveBeenCalledWith(ingredients);
- });
+ it("Should render the user's username", () => {
+ component.profile$.subscribe((profile: IProfile) => {
+ expect(component.profile.username).toBe(profile.username);
+ })
+ })
+
+ it('Should dispatch CreateRecipe Action', async () => {
+
+ jest.spyOn(component, 'isFormValid');
+ const profileDataSubject = new BehaviorSubject(undefined);
- it('should create the recipe', async () => {
+ component.profile$.pipe(take(1)).subscribe((profile: IProfile) => {
+ component.profile = profile;
+ profileDataSubject.next(profile); // Update the BehaviorSubject with the profileData
+ });
+
+ // Mock the recipe data
const recipe: IRecipe = {
name: "Mock Recipe",
recipeImage: "https://example.com/image.jpg",
- ingredients: [
+ description: "Amazing meal for a family",
+ meal: "Dinner",
+ creator: profileDataSubject.value?.username ?? '',
+ ingredients: [ {name: 'ingredient1' , amount : 5, unit : 'L'},
+ {name: 'ingredient2' , amount : 3, unit : 'g'}
],
- instructions: [
- {
- instructionHeading: "N/A",
- instructionBody: "Mock instructions",
- },
+ steps: [
+ "Mock instructions",
],
- rating: 0,
- difficulty: "easy",
+ difficulty: "Easy",
prepTime: 30,
- numberOfServings: 4,
+ servings: 4,
tags: ["mock", "recipe"],
};
-
- const response: IRecipe = {
- recipeId: "1",
- ...recipe, // Copy the properties from the recipe object
- };
-
- jest.spyOn(component, "createIngredients").mockResolvedValue([]);
- jest.spyOn(apiService, "createNewRecipe").mockReturnValue(of(response));
-
+
component.imageUrl = recipe.recipeImage
// Mock the values and controls used in createRecipe
component.recipeForm = fb.group({
name: fb.control(recipe.name),
- servings: fb.control(recipe.numberOfServings),
+ description: fb.control(recipe.description),
+ difficulty: fb.control(recipe.difficulty),
+ servings: fb.control(recipe.servings),
preparationTime: fb.control(recipe.prepTime),
- ingredients: fb.array(recipe.ingredients.map(ingredient => fb.control(ingredient.name))),
- instructions: fb.array(recipe.instructions.map(instruction => fb.control(instruction.instructionBody))),
+ ingredients: fb.array(recipe.ingredients.map(ingredient => fb.control(ingredient))),
+ instructions: fb.array(recipe.steps.map(instruction => fb.control(instruction))),
dietaryPlans: fb.array((recipe.tags || []).map(tag => fb.control(tag))),
});
-
-
+
+ component.tags = recipe.tags;
+ component.selectedMeal = recipe.meal;
+
// Call the createRecipe method
component.createRecipe();
-
- // Wait for the promises to resolve
- await fixture.whenStable();
-
- // Verify that the createNewRecipe method was called with the correct recipe argument
- expect(apiService.createNewRecipe).toHaveBeenCalledWith(recipe);
- // expect(apiService.createNewRecipe).toBeTruthy();
-
- // Verify that the createIngredients method was called
- expect(component.createIngredients).toHaveBeenCalled();
+ expect(dispatchSpy).toHaveBeenCalledWith(new CreateRecipe(recipe));
+
+ expect(component.recipeForm.valid).toBe(true);
+ expect(component.isFormValid()).toBe(true)
+ expect(component.isFormValid).toHaveBeenCalled();
+ expect(component.profile.username).toBe(profileDataSubject.value?.username ?? '');
+
+
});
-
- })
+ it('Should not create recipe if form is invalid', () => {
+
+ jest.spyOn(component, 'isFormValid');
+
+
+ const profileDataSubject = new BehaviorSubject(undefined);
+
+ component.profile$.pipe(take(1)).subscribe((profile: IProfile) => {
+ component.profile = profile;
+ profileDataSubject.next(profile); // Update the BehaviorSubject with the profileData
+ });
+ // Mock the recipe data
+ const recipe: IRecipe = {
+ name: "Mock Recipe",
+ recipeImage: "https://example.com/image.jpg",
+ description: "Amazing meal for a family",
+ meal: "Dinner",
+ creator: profileDataSubject.value?.username ?? '',
+ ingredients: [],
+ steps: [],
+ difficulty: "Easy",
+ prepTime: 30,
+ servings: 4,
+ tags: ["mock", "recipe"],
+ };
+
+ component.imageUrl = recipe.recipeImage
+ // Mock the values and controls used in createRecipe
+ component.recipeForm = fb.group({
+ name: fb.control(recipe.name),
+ description: fb.control(recipe.description),
+ difficulty: fb.control(recipe.difficulty),
+ servings: fb.control(recipe.servings),
+ preparationTime: fb.control(recipe.prepTime),
+ ingredients: fb.array(recipe.ingredients.map(ingredient => fb.control(ingredient))),
+ instructions: fb.array(recipe.steps.map(instruction => fb.control(instruction))),
+ dietaryPlans: fb.array((recipe.tags || []).map(tag => fb.control(tag))),
+ });
+
+ component.tags = recipe.tags;
+ component.selectedMeal = recipe.meal;
+ // Call the createRecipe method
+ component.createRecipe();
+ expect(component.isFormValid).toHaveBeenCalled();
+ expect(component.isFormValid()).toBe(false)
+ expect(dispatchSpy).not.toHaveBeenCalledWith(new CreateRecipe(recipe));
+ })
-
-
\ No newline at end of file
+
+ })
diff --git a/libs/app/create/feature/src/create.page.ts b/libs/app/create/feature/src/create.page.ts
index bcb97a51..53a50a5d 100644
--- a/libs/app/create/feature/src/create.page.ts
+++ b/libs/app/create/feature/src/create.page.ts
@@ -1,20 +1,37 @@
-import { Component } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
-import { CreateAPI } from '@fridge-to-plate/app/create/data-access';
-import { IRecipe, IRecipeStep } from '@fridge-to-plate/app/recipe/utils';
+import { IRecipe } from '@fridge-to-plate/app/recipe/utils';
import { IIngredient } from '@fridge-to-plate/app/ingredient/utils';
+import { Select, Store } from '@ngxs/store';
+import { ShowError } from '@fridge-to-plate/app/error/utils';
+import { CreateRecipe } from '@fridge-to-plate/app/recipe/utils';
+import { ProfileState } from '@fridge-to-plate/app/profile/data-access';
+import { Observable, take } from 'rxjs';
+import { IProfile, UpdateProfile } from '@fridge-to-plate/app/profile/utils';
+import { RecipeState } from '@fridge-to-plate/app/recipe/data-access';
@Component({
selector: 'fridge-to-plate-app-create',
templateUrl: './create.page.html',
styleUrls: ['./create.page.scss'],
})
-export class CreatePagComponent {
+export class CreatePagComponent implements OnInit {
+
+ @Select(ProfileState.getProfile) profile$ !: Observable;
+ @Select(RecipeState.getRecipe) recipe$ !: Observable;
+
recipeForm!: FormGroup;
- imageUrl = 'https://img.icons8.com/ios-filled/50/cooking-book--v1.png';
+ imageUrl = 'https://img.freepik.com/free-photo/frying-pan-empty-with-various-spices-black-table_1220-561.jpg';
+ selectedMeal!: "Breakfast" | "Lunch" | "Dinner" | "Snack" | "Dessert";
+ difficulty: "Easy" | "Medium" | "Hard" = "Easy";
+ tags: string[] = [];
+ profile !: IProfile;
+
+ constructor(private fb: FormBuilder, private store : Store) {}
- constructor(private fb: FormBuilder, private api: CreateAPI) {
+ ngOnInit() {
this.createForm();
+ this.profile$.subscribe(profile => this.profile = profile);
}
createForm(): void {
@@ -25,19 +42,23 @@ export class CreatePagComponent {
preparationTime: ['', Validators.required],
ingredients: this.fb.array([]),
instructions: this.fb.array([]),
- dietaryPlans: this.fb.array([]),
+ tag: ['']
});
}
+
get ingredientControls() {
return (this.recipeForm.get('ingredients') as FormArray).controls;
}
- get dietaryPlans() {
- return (this.recipeForm.get('dietaryPlans') as FormArray).controls;
- }
-
addIngredient() {
- this.ingredientControls.push(this.fb.control(''));
+ const ingredientGroup = this.fb.group({
+ name: ['', Validators.required],
+ amount: ['', Validators.required],
+ unit: ['', Validators.required]
+ });
+
+ // Add the new ingredient group to the FormArray
+ (this.recipeForm.get('ingredients') as FormArray).push(ingredientGroup);
}
get instructionControls() {
@@ -45,7 +66,7 @@ export class CreatePagComponent {
}
addInstruction(): void {
- this.instructionControls.push(this.fb.control(''));
+ this.instructionControls.push(this.fb.control('', Validators.required));
}
removeIngredient(index: number): void {
@@ -56,108 +77,185 @@ export class CreatePagComponent {
this.instructionControls.splice(index, 1);
}
- toggleDietaryPlan(plan: string): void {
- const dietaryPlans = this.recipeForm.get('dietaryPlans') as FormArray;
+ getAmountPlaceholderText() {
+ if (window.innerWidth < 1024) {
+ return "e.g 10";
+ } else {
+ return "Amount";
+ }
+ }
- if (dietaryPlans != null && this.isDietaryPlanSelected(plan)) {
- // Remove the dietary plan if it's already selected
- dietaryPlans.removeAt(dietaryPlans.value.indexOf(plan));
+ getUnitPlaceholderText() {
+ if (window.innerWidth < 1024) {
+ return "e.g L";
} else {
- // Add the dietary plan if it's not selected
- dietaryPlans.push(this.fb.control(plan));
+ return "Unit";
}
}
- getDietaryPlanButtonClasses(plan: string): string {
- return this.isDietaryPlanSelected(plan)
- ? 'bg-gray-600 text-white'
- : 'bg-gray-300 text-gray-700';
+ createRecipe() : void {
+
+ // Check first if the form is completely valid
+ if(!this.isFormValid())
+ return;
+
+ // Ingredients array
+ const ingredients = this.getIngredients();
+
+ // Instructions array
+ const instructions = this.getInstructions()
+
+ // Create Recipe details
+ const recipe: IRecipe = {
+ name: this.recipeForm.get('name')?.value,
+ recipeImage: this.imageUrl,
+ description: this.recipeForm.get('description')?.value,
+ meal: this.selectedMeal,
+ creator: this.profile.username,
+ ingredients: ingredients,
+ steps: instructions,
+ difficulty: this.difficulty,
+ prepTime: this.recipeForm.get('preparationTime')?.value as number,
+ servings: this.recipeForm.get('servings')?.value as number,
+ tags: this.tags,
+ };
+
+ this.store.dispatch( new CreateRecipe(recipe) )
+ }
+
+
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ onFileChanged(event: any) {
+ const file = event.target.files[0];
+ const reader = new FileReader();
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ reader.onload = (e: any) => {
+ this.imageUrl = e.target.result;
+ };
+
+ reader.readAsDataURL(file);
}
- isDietaryPlanSelected(plan: string): boolean {
- const dietaryPlans = this.recipeForm.get('dietaryPlans')?.value;
+ toggleMeal(option: "Breakfast" | "Lunch" | "Dinner" | "Snack" | "Dessert") {
+ this.selectedMeal = option;
+ }
- return dietaryPlans.includes(plan);
+ getMealPlan(option: string) {
+ return {
+ 'bg-primary': this.selectedMeal === option,
+ 'bg-gray-200': this.selectedMeal !== option,
+ 'text-white': this.selectedMeal === option,
+ 'text-gray-700': this.selectedMeal !== option,
+ 'py-2': true,
+ 'px-4': true,
+ 'rounded-md': true,
+ 'mr-2': true
+ };
}
- createRecipe() : void {
- // Ingredients array
+ toggleDifficulty(option: "Easy" | "Medium" | "Hard") {
+ this.difficulty = option;
+ }
+
+ getDifficulty(option: string) {
+ return {
+ 'bg-primary': this.difficulty === option,
+ 'bg-gray-200': this.difficulty !== option,
+ 'text-white': this.difficulty === option,
+ 'text-gray-700': this.difficulty !== option,
+ 'py-2': true,
+ 'px-4': true,
+ 'rounded-md': true,
+ 'mr-2': true
+ };
+ }
+
+ addTag() {
+ const tagValue = this.recipeForm.get('tag')?.value as string;
+ if(!tagValue) {
+ this.store.dispatch( new ShowError("Please enter valid tag"))
+ }
+ else if (this.tags.length < 3) {
+ if(this.tags.includes(tagValue)){
+ this.store.dispatch( new ShowError("No duplicates: Tag already selected"))
+ return;
+ }
+ this.tags.push(tagValue);
+ }
+ else {
+ this.store.dispatch( new ShowError("Only a maximum of three tags"))
+ }
+ // reset the form value after adding it to array
+ this.recipeForm.get('tag')?.reset();
+ }
+
+ deleteTag(index: number) {
+ this.tags.splice(index, 1);
+ }
+
+ isFormValid(): boolean {
+
+ if(!this.recipeForm.valid){
+ this.store.dispatch( new ShowError("Incomplete Form. Please fill out every field."))
+ return false;
+ }
+
+ if(this.ingredientControls.length < 1) {
+ this.store.dispatch( new ShowError("No Ingredients"))
+ return false;
+ }
+
+ if(this.instructionControls.length < 1) {
+ this.store.dispatch( new ShowError("No Instructions"))
+ return false;
+ }
+
+ if(this.tags.length < 1) {
+ this.store.dispatch( new ShowError("No Tags"))
+ return false;
+ }
+
+ if(!this.selectedMeal){
+ this.store.dispatch( new ShowError("Please select a meal"))
+ return false;
+ }
+
+ if(!this.profile){
+ this.store.dispatch( new ShowError("Please login to create a recipe"))
+ return false;
+ }
+
+ return true;
+ }
+
+ getIngredients(): IIngredient[] {
const ingredients: IIngredient[] = [];
-
- let tags = new Array(this.dietaryPlans.length);
- this.ingredientControls.forEach((element) => {
- if (element.value !== null) {
-
- ingredients.push({
- name: element.value
+ this.ingredientControls.forEach((ingredient) => {
+ if (ingredient) {
+ ingredients.push({
+ name: ingredient.value.name,
+ amount: ingredient.value.amount,
+ unit: ingredient.value.unit
});
}
});
- // Instructions array
- const instructions: IRecipeStep[] = [];
+ return ingredients;
+ }
+
+ getInstructions() : string[] {
+ const instructions: string[] = [];
this.instructionControls.forEach((element) => {
if (element.value) {
- instructions.push({
- instructionHeading: 'N/A',
- instructionBody: element.value,
- });
- }
- });
-
- // Dietary plans array
- this.dietaryPlans.forEach((element) => {
- if (element.value !== null) {
- tags.push(element.value);
+ instructions.push(element.value);
}
});
- tags = tags.filter((value) => value !== null);
-
- // We store the ingredients and return ingredients
- const createdIngredients = this.createIngredients(ingredients);
-
- // After now having stored or created the ingredients, we create the recipe.
- createdIngredients.then((ingredientsArray) => {
-
- // The, create the recipe object
- const recipe: IRecipe = {
- name: this.recipeForm.get('name')?.value,
- recipeImage: this.imageUrl,
- ingredients: ingredientsArray,
- instructions: instructions,
- rating: 0,
- difficulty: 'easy',
- prepTime: this.recipeForm.get('preparationTime')?.value as number,
- numberOfServings: this.recipeForm.get('servings')?.value as number,
- tags: tags,
- };
-
- // Store the recipe to the database
- this.api.createNewRecipe(recipe).subscribe((response) => {
- if (!response) {
- return response;
- }
- return response;
- });
-
- });
+ return instructions;
}
- createIngredients(ingredients: IIngredient[]) : Promise {
- const recipe = new Promise((resolve, reject) => {
- this.api
- .createNewMultipleIngredients(ingredients)
- .subscribe((response) => {
- if (!response) {
- reject(response);
- }
- resolve(response);
- });
- })
-
- return recipe;
- }
}
diff --git a/libs/app/edit-recipe/data-access/.eslintrc.json b/libs/app/edit-recipe/data-access/.eslintrc.json
new file mode 100644
index 00000000..6bac7be5
--- /dev/null
+++ b/libs/app/edit-recipe/data-access/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "rules": {
+ "@angular-eslint/directive-selector": [
+ "error",
+ {
+ "type": "attribute",
+ "prefix": "fridgeToPlate",
+ "style": "camelCase"
+ }
+ ],
+ "@angular-eslint/component-selector": [
+ "error",
+ {
+ "type": "element",
+ "prefix": "fridge-to-plate",
+ "style": "kebab-case"
+ }
+ ]
+ },
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ]
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/libs/app/edit-recipe/data-access/README.md b/libs/app/edit-recipe/data-access/README.md
new file mode 100644
index 00000000..42ebf70a
--- /dev/null
+++ b/libs/app/edit-recipe/data-access/README.md
@@ -0,0 +1,7 @@
+# app-edit-recipe-data-access
+
+This library was generated with [Nx](https://nx.dev).
+
+## Running unit tests
+
+Run `nx test app-edit-recipe-data-access` to execute the unit tests.
diff --git a/libs/app/edit-recipe/data-access/jest.config.ts b/libs/app/edit-recipe/data-access/jest.config.ts
new file mode 100644
index 00000000..7a4835a9
--- /dev/null
+++ b/libs/app/edit-recipe/data-access/jest.config.ts
@@ -0,0 +1,22 @@
+/* eslint-disable */
+export default {
+ displayName: 'app-edit-recipe-data-access',
+ preset: '../../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ coverageDirectory: '../../../../coverage/libs/app/edit-recipe/data-access',
+ transform: {
+ '^.+\\.(ts|mjs|js|html)$': [
+ 'jest-preset-angular',
+ {
+ tsconfig: '/tsconfig.spec.json',
+ stringifyContentPathRegex: '\\.(html|svg)$',
+ },
+ ],
+ },
+ transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
+ snapshotSerializers: [
+ 'jest-preset-angular/build/serializers/no-ng-attributes',
+ 'jest-preset-angular/build/serializers/ng-snapshot',
+ 'jest-preset-angular/build/serializers/html-comment',
+ ],
+};
diff --git a/libs/app/edit-recipe/data-access/project.json b/libs/app/edit-recipe/data-access/project.json
new file mode 100644
index 00000000..07740929
--- /dev/null
+++ b/libs/app/edit-recipe/data-access/project.json
@@ -0,0 +1,34 @@
+{
+ "name": "app-edit-recipe-data-access",
+ "$schema": "../../../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "libs/app/edit-recipe/data-access/src",
+ "prefix": "fridge-to-plate",
+ "tags": [],
+ "projectType": "library",
+ "targets": {
+ "test": {
+ "executor": "@nx/jest:jest",
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+ "options": {
+ "jestConfig": "libs/app/edit-recipe/data-access/jest.config.ts",
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "codeCoverage": true
+ }
+ }
+ },
+ "lint": {
+ "executor": "@nx/linter:eslint",
+ "outputs": ["{options.outputFile}"],
+ "options": {
+ "lintFilePatterns": [
+ "libs/app/edit-recipe/data-access/**/*.ts",
+ "libs/app/edit-recipe/data-access/**/*.html"
+ ]
+ }
+ }
+ }
+}
diff --git a/libs/app/create/data-access/src/create.module.ts b/libs/app/edit-recipe/data-access/src/edit-recipe.module.ts
similarity index 60%
rename from libs/app/create/data-access/src/create.module.ts
rename to libs/app/edit-recipe/data-access/src/edit-recipe.module.ts
index 6898e203..3b4b2de9 100644
--- a/libs/app/create/data-access/src/create.module.ts
+++ b/libs/app/edit-recipe/data-access/src/edit-recipe.module.ts
@@ -1,8 +1,7 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
-import { CreateAPI } from './api/create.api';
@NgModule({
imports: [CommonModule],
})
-export class AppCreateDataAccessModule {}
+export class EditRecipeDataAccessModule {}
diff --git a/libs/app/edit-recipe/data-access/src/edit-recipe.state.ts b/libs/app/edit-recipe/data-access/src/edit-recipe.state.ts
new file mode 100644
index 00000000..8099acb0
--- /dev/null
+++ b/libs/app/edit-recipe/data-access/src/edit-recipe.state.ts
@@ -0,0 +1,48 @@
+import { Injectable } from "@angular/core";
+import { RecipeAPI } from "@fridge-to-plate/app/recipe/data-access";
+import { IRecipe } from "@fridge-to-plate/app/recipe/utils";
+import { Selector, Store, State, StateContext, Action} from "@ngxs/store";
+import { LoadRecipe } from '@fridge-to-plate/app/edit-recipe/utils'
+import { ShowError } from "@fridge-to-plate/app/error/utils";
+import { Navigate } from "@ngxs/router-plugin";
+
+export interface EditRecipeStateModel {
+ editRecipe: IRecipe | null;
+}
+
+
+ @State({
+ name: 'editRecipe',
+ defaults: {
+ editRecipe: null,
+ }
+ })
+
+ @Injectable()
+ export class RecipeState {
+
+ constructor(private api: RecipeAPI, private store: Store) {}
+
+ @Selector()
+ static getEditRecipe(state: EditRecipeStateModel) {
+ return state.editRecipe;
+ }
+
+ @Action(LoadRecipe)
+ loadRecipe({setState}: StateContext, {recipeId}: LoadRecipe) {
+
+ this.api.getRecipeById(recipeId).subscribe((recipe) => {
+ setState({
+ editRecipe: recipe,
+ });
+ this.store.dispatch(new Navigate(['/edit-recipe']));
+ },
+ (error: Error) => {
+ console.error('Failed to load recipe:', error);
+ this.store.dispatch(new ShowError(error.message));
+ }
+ );
+ }
+
+ }
+
\ No newline at end of file
diff --git a/libs/app/edit-recipe/data-access/src/index.ts b/libs/app/edit-recipe/data-access/src/index.ts
new file mode 100644
index 00000000..608595e0
--- /dev/null
+++ b/libs/app/edit-recipe/data-access/src/index.ts
@@ -0,0 +1,2 @@
+export * from './edit-recipe.module';
+export * from './edit-recipe.state'
diff --git a/libs/app/edit-recipe/data-access/src/test-setup.ts b/libs/app/edit-recipe/data-access/src/test-setup.ts
new file mode 100644
index 00000000..1100b3e8
--- /dev/null
+++ b/libs/app/edit-recipe/data-access/src/test-setup.ts
@@ -0,0 +1 @@
+import 'jest-preset-angular/setup-jest';
diff --git a/libs/app/edit-recipe/data-access/tsconfig.json b/libs/app/edit-recipe/data-access/tsconfig.json
new file mode 100644
index 00000000..b9e5be08
--- /dev/null
+++ b/libs/app/edit-recipe/data-access/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ],
+ "extends": "../../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/libs/app/edit-recipe/data-access/tsconfig.lib.json b/libs/app/edit-recipe/data-access/tsconfig.lib.json
new file mode 100644
index 00000000..91273870
--- /dev/null
+++ b/libs/app/edit-recipe/data-access/tsconfig.lib.json
@@ -0,0 +1,17 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../../dist/out-tsc",
+ "declaration": true,
+ "declarationMap": true,
+ "inlineSources": true,
+ "types": []
+ },
+ "exclude": [
+ "src/**/*.spec.ts",
+ "src/test-setup.ts",
+ "jest.config.ts",
+ "src/**/*.test.ts"
+ ],
+ "include": ["src/**/*.ts"]
+}
diff --git a/libs/app/edit-recipe/data-access/tsconfig.spec.json b/libs/app/edit-recipe/data-access/tsconfig.spec.json
new file mode 100644
index 00000000..6e5925e5
--- /dev/null
+++ b/libs/app/edit-recipe/data-access/tsconfig.spec.json
@@ -0,0 +1,16 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../../dist/out-tsc",
+ "module": "commonjs",
+ "target": "es2016",
+ "types": ["jest", "node"]
+ },
+ "files": ["src/test-setup.ts"],
+ "include": [
+ "jest.config.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/libs/app/edit-recipe/feature/.eslintrc.json b/libs/app/edit-recipe/feature/.eslintrc.json
new file mode 100644
index 00000000..6bac7be5
--- /dev/null
+++ b/libs/app/edit-recipe/feature/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "rules": {
+ "@angular-eslint/directive-selector": [
+ "error",
+ {
+ "type": "attribute",
+ "prefix": "fridgeToPlate",
+ "style": "camelCase"
+ }
+ ],
+ "@angular-eslint/component-selector": [
+ "error",
+ {
+ "type": "element",
+ "prefix": "fridge-to-plate",
+ "style": "kebab-case"
+ }
+ ]
+ },
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ]
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/libs/app/edit-recipe/feature/README.md b/libs/app/edit-recipe/feature/README.md
new file mode 100644
index 00000000..c1f54ad2
--- /dev/null
+++ b/libs/app/edit-recipe/feature/README.md
@@ -0,0 +1,7 @@
+# app-edit-recipe-feature
+
+This library was generated with [Nx](https://nx.dev).
+
+## Running unit tests
+
+Run `nx test app-edit-recipe-feature` to execute the unit tests.
diff --git a/libs/app/edit-recipe/feature/jest.config.ts b/libs/app/edit-recipe/feature/jest.config.ts
new file mode 100644
index 00000000..dc58d251
--- /dev/null
+++ b/libs/app/edit-recipe/feature/jest.config.ts
@@ -0,0 +1,22 @@
+/* eslint-disable */
+export default {
+ displayName: 'app-edit-recipe-feature',
+ preset: '../../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ coverageDirectory: '../../../../coverage/libs/app/edit-recipe/feature',
+ transform: {
+ '^.+\\.(ts|mjs|js|html)$': [
+ 'jest-preset-angular',
+ {
+ tsconfig: '/tsconfig.spec.json',
+ stringifyContentPathRegex: '\\.(html|svg)$',
+ },
+ ],
+ },
+ transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
+ snapshotSerializers: [
+ 'jest-preset-angular/build/serializers/no-ng-attributes',
+ 'jest-preset-angular/build/serializers/ng-snapshot',
+ 'jest-preset-angular/build/serializers/html-comment',
+ ],
+};
diff --git a/libs/app/edit-recipe/feature/project.json b/libs/app/edit-recipe/feature/project.json
new file mode 100644
index 00000000..70da48a4
--- /dev/null
+++ b/libs/app/edit-recipe/feature/project.json
@@ -0,0 +1,34 @@
+{
+ "name": "app-edit-recipe-feature",
+ "$schema": "../../../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "libs/app/edit-recipe/feature/src",
+ "prefix": "fridge-to-plate",
+ "tags": [],
+ "projectType": "library",
+ "targets": {
+ "test": {
+ "executor": "@nx/jest:jest",
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+ "options": {
+ "jestConfig": "libs/app/edit-recipe/feature/jest.config.ts",
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "codeCoverage": true
+ }
+ }
+ },
+ "lint": {
+ "executor": "@nx/linter:eslint",
+ "outputs": ["{options.outputFile}"],
+ "options": {
+ "lintFilePatterns": [
+ "libs/app/edit-recipe/feature/**/*.ts",
+ "libs/app/edit-recipe/feature/**/*.html"
+ ]
+ }
+ }
+ }
+}
diff --git a/libs/app/edit-recipe/feature/src/edit-recipe.module.ts b/libs/app/edit-recipe/feature/src/edit-recipe.module.ts
new file mode 100644
index 00000000..2e660152
--- /dev/null
+++ b/libs/app/edit-recipe/feature/src/edit-recipe.module.ts
@@ -0,0 +1,28 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { IonicModule } from '@ionic/angular';
+import { EditRecipeRouting } from './edit-recipe.routing';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { EditRecipeComponent } from './edit-recipe.page';
+import { RecipeDataAccessModule } from '@fridge-to-plate/app/recipe/data-access';
+// eslint-disable-next-line @nrwl/nx/enforce-module-boundaries
+import { RecipeModule } from '@fridge-to-plate/app/recipe/feature';
+import { NgxsModule } from '@ngxs/store';
+import {RecipeState as EditRecipeState } from '@fridge-to-plate/app/edit-recipe/data-access'
+
+
+@NgModule({
+ imports: [
+ CommonModule,
+ IonicModule,
+ EditRecipeRouting,
+ ReactiveFormsModule,
+ FormsModule,
+ RecipeDataAccessModule,
+ RecipeModule,
+ RecipeDataAccessModule,
+ NgxsModule.forFeature([EditRecipeState])
+ ],
+ declarations: [EditRecipeComponent]
+})
+export class EditRecipeModule {}
diff --git a/libs/app/auth/data-access/src/auth.api.ts b/libs/app/edit-recipe/feature/src/edit-recipe.page.css
similarity index 100%
rename from libs/app/auth/data-access/src/auth.api.ts
rename to libs/app/edit-recipe/feature/src/edit-recipe.page.css
diff --git a/libs/app/edit-recipe/feature/src/edit-recipe.page.html b/libs/app/edit-recipe/feature/src/edit-recipe.page.html
new file mode 100644
index 00000000..b1b29019
--- /dev/null
+++ b/libs/app/edit-recipe/feature/src/edit-recipe.page.html
@@ -0,0 +1,328 @@
+
+
+
{{ recipe.name }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libs/app/edit-recipe/feature/src/edit-recipe.page.spec.ts b/libs/app/edit-recipe/feature/src/edit-recipe.page.spec.ts
new file mode 100644
index 00000000..9b65e9e4
--- /dev/null
+++ b/libs/app/edit-recipe/feature/src/edit-recipe.page.spec.ts
@@ -0,0 +1,1185 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { EditRecipeComponent } from './edit-recipe.page';
+import { NgxsModule, State, Store } from '@ngxs/store';
+import { DeleteRecipe, IRecipe, UpdateRecipe } from '@fridge-to-plate/app/recipe/utils';
+import { IProfile, UpdateProfile } from '@fridge-to-plate/app/profile/utils';
+import { Injectable } from '@angular/core';
+import { HttpClientModule } from '@angular/common/http';
+import { NavigationBarModule } from '@fridge-to-plate/app/navigation/feature';
+import { IonicModule } from '@ionic/angular';
+import { RouterTestingModule } from '@angular/router/testing';
+import { ShowError } from '@fridge-to-plate/app/error/utils';
+import { BehaviorSubject, of } from 'rxjs';
+import { Location } from '@angular/common';
+
+import { FormArray, FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
+import { IIngredient } from '@fridge-to-plate/app/ingredient/utils';
+import { Navigate } from '@ngxs/router-plugin';
+
+describe('EditRecipeComponent', () => {
+ let component: EditRecipeComponent;
+ let fixture: ComponentFixture;
+
+ const testProfile: IProfile = {
+ displayName: "John Doe",
+ username: "jdoe",
+ email: "jdoe@gmail.com",
+ savedRecipes: [],
+ ingredients: [],
+ profilePic: "image-url",
+ createdRecipes: [],
+ currMealPlan: null,
+ };
+
+ @State({
+ name: 'profile',
+ defaults: {
+ profile: testProfile
+ }
+ })
+
+ @Injectable()
+ class MockProfileState {}
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [IonicModule, HttpClientModule, NavigationBarModule, RouterTestingModule, NgxsModule.forRoot([MockProfileState])],
+ declarations: [EditRecipeComponent],
+ providers: [
+
+ ]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(EditRecipeComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('Should go to Home Page', ()=> {
+ const storeDispatchSpy = jest.spyOn(TestBed.inject(Store), 'dispatch');
+ component.goHome();
+ expect(storeDispatchSpy).toHaveBeenCalledWith(new Navigate(['/home']));
+ })
+
+ it('test delete recipe with valid recipe id', () => {
+ const recipeId = 'valid_recipe_id';
+ const deleteRecipeSpy = jest.spyOn(EditRecipeComponent.prototype, 'deleteRecipe');
+ const storeDispatchSpy = jest.spyOn(TestBed.inject(Store), 'dispatch');
+ const component = fixture.componentInstance;
+ component.recipe = { recipeId } as IRecipe;
+ fixture.detectChanges();
+ component.deleteRecipe();
+
+ expect(deleteRecipeSpy).toHaveBeenCalled();
+ expect(storeDispatchSpy).toHaveBeenCalledWith(new DeleteRecipe(recipeId));
+ });
+
+
+ it('should initialize recipe and recipeId correctly', () => {
+ const mockRecipe = { recipeId: 'sampleId'} as IRecipe;
+ jest.spyOn(component.recipe$, 'pipe').mockReturnValue(of(mockRecipe));
+ component.initialize()
+ expect(component.recipe).toEqual(mockRecipe);
+ expect(component.recipeId).toBe(mockRecipe.recipeId);
+ expect(component.recipeId).not.toBeUndefined();
+
+ });
+
+ it('does not set the recipeId property if the recipe property does not have a recipeId', () => {
+ component.initialize();
+ expect(component.recipeId).toBeUndefined();
+});
+
+ it('test show error message if recipe id is not available', () => {
+ const showErrorSpy = jest.spyOn(TestBed.inject(Store), 'dispatch');
+ const component = fixture.componentInstance;
+ component.recipe = null;
+ fixture.detectChanges();
+ component.deleteRecipe();
+
+ expect(showErrorSpy).toHaveBeenCalledWith(new ShowError('Could not delete recipe'));
+ });
+
+
+ it('test dispatch delete recipe action with recipe id', () => {
+ const recipeId = 'valid_recipe_id';
+ const storeDispatchSpy = jest.spyOn(TestBed.inject(Store), 'dispatch').mockReturnValue(of(null));
+ const locationBackSpy = jest.spyOn(TestBed.inject(Location), 'back');
+ const component = fixture.componentInstance;
+ component.recipe = { recipeId } as IRecipe;
+ fixture.detectChanges();
+
+ component.deleteRecipe();
+
+ expect(storeDispatchSpy).toHaveBeenCalledWith(new DeleteRecipe(recipeId));
+ expect(locationBackSpy).toHaveBeenCalled();
+ });
+
+ it('test cancel edit calls location back', () => {
+ const locationBackSpy = jest.spyOn(TestBed.inject(Location), 'back');
+ component.cancelEdit();
+ expect(locationBackSpy).toHaveBeenCalled();
+});
+
+it('test populateForm with recipe', () => {
+ component.recipe = {
+ recipeId: '1',
+ name: 'Test Recipe',
+ recipeImage: 'https://img.freepik.com/free-photo/frying-pan-empty-with-various-spices-black-table_1220-561.jpg',
+ description: 'Test Recipe Description',
+ meal: 'Breakfast',
+ creator: '',
+ ingredients: [
+ {
+ name: 'Test Ingredient',
+ amount: 1,
+ unit: 'Test Unit'
+ }
+ ],
+ steps: ['Test Step'],
+ difficulty: 'Easy',
+ prepTime: 10,
+ servings: 2,
+ tags: ['Test Tag']
+ };
+
+ component.populateForm();
+ expect(component.ingredientControls.length).toEqual(1);
+ expect(component.instructionControls.length).toEqual(1);
+ expect(component.tags).toEqual(['Test Tag']);
+ expect(component.selectedMeal).toEqual('Breakfast');
+ expect(component.imageUrl).toEqual('https://img.freepik.com/free-photo/frying-pan-empty-with-various-spices-black-table_1220-561.jpg');
+ expect(component.difficulty).toEqual('Easy');
+});
+
+
+});
+@State({
+ name: 'create',
+ defaults: {
+ recipe: null,
+ }
+})
+
+@Injectable()
+class MockCreateState {}
+
+
+
+describe('Edit Recipe Page', () => {
+ let editRecipePage: EditRecipeComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ EditRecipeComponent ],
+ imports: [
+ ReactiveFormsModule,
+ IonicModule,
+ HttpClientModule,
+ NavigationBarModule,
+ RouterTestingModule,
+ NgxsModule.forRoot([MockCreateState])
+ ],
+ providers: [ FormBuilder ]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(EditRecipeComponent);
+
+ editRecipePage = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should set the name, description, servings, and preparationTime fields', () => {
+
+ const populateFormSpy = jest.spyOn(EditRecipeComponent.prototype, 'populateForm');
+
+ const recipe = {
+ recipeId: '1',
+ name: 'Test Recipe',
+ recipeImage: 'https://img.freepik.com/free-photo/frying-pan-empty-with-various-spices-black-table_1220-561.jpg',
+ description: 'Test Recipe Description',
+ meal: 'Breakfast',
+ creator: 'Test Creator',
+ ingredients: [
+ {
+ name: 'Test Ingredient 1',
+ amount: 1,
+ unit: 'Test Unit 1'
+ },
+ {
+ name: 'Test Ingredient 2',
+ amount: 2,
+ unit: 'Test Unit 2'
+ }
+ ],
+ steps: ['Test Step 1', 'Test Step 2'],
+ difficulty: 'Easy',
+ prepTime: 10,
+ servings: 2,
+ tags: ['Test Tag 1', 'Test Tag 2']
+ } as IRecipe;
+
+ const initializeSpy = jest.spyOn(EditRecipeComponent.prototype, 'initialize').mockImplementation( () => {
+ editRecipePage.recipe = recipe
+ });
+
+ editRecipePage.createForm();
+ expect(initializeSpy).toBeCalled();
+ expect(populateFormSpy).toBeCalled();
+ expect(editRecipePage.recipeForm.value.name).toEqual('Test Recipe');
+ expect(editRecipePage.recipeForm.value.description).toEqual('Test Recipe Description');
+ expect(editRecipePage.recipeForm.value.servings).toEqual(2);
+ expect(editRecipePage.recipeForm.value.preparationTime).toEqual(10);
+});
+
+
+ it('should add a new ingredient control to the form', () => {
+ const initialLength = editRecipePage.ingredientControls.length;
+ editRecipePage.addIngredient();
+ const newLength = editRecipePage.ingredientControls.length;
+ expect(newLength).toBe(initialLength + 1);
+ }
+ );
+
+ it('should remove an ingredient control from the form', () => {
+ const initialLength = editRecipePage.ingredientControls.length;
+ if(initialLength == 0) {
+ expect(initialLength).toBe(0)
+ return
+ }
+ editRecipePage.removeIngredient(0);
+ const newLength = editRecipePage.ingredientControls.length;
+ expect(newLength).toBe(initialLength - 1);
+ }
+ );
+
+ it('should add a new instruction control to the form', () => {
+ const initialLength = editRecipePage.instructionControls.length;
+ editRecipePage.addInstruction();
+ const newLength = editRecipePage.instructionControls.length;
+ expect(newLength).toBe(initialLength + 1);
+ }
+ );
+
+
+ it('get instruction steps as String[]', () => {
+ const formArray = new FormArray([
+ new FormControl('Step 1'),
+ new FormControl('Step 2'),
+ new FormControl('Step 3'),
+ ]);
+
+ // create a new recipe form using the form array
+ const recipeForm = new FormGroup({
+ instructions: formArray,
+ });
+
+ editRecipePage.recipeForm = recipeForm;
+
+ const instructions = editRecipePage.getInstructions();
+
+ expect(instructions[0]).toBe('Step 1');
+ expect(instructions[1]).toBe('Step 2');
+ expect(instructions[2]).toBe('Step 3');
+ })
+
+
+ it('should remove an instruction control from the form', () => {
+
+ const formArray = new FormArray([
+ new FormControl('Step 1'),
+ new FormControl('Step 2'),
+ new FormControl('Step 3'),
+ ]);
+
+ // create a new recipe form using the form array
+ const recipeForm = new FormGroup({
+ instructions: formArray,
+ });
+
+ editRecipePage.recipeForm = recipeForm;
+
+ const initialLength = editRecipePage.instructionControls.length;
+ editRecipePage.removeInstruction(0);
+ const newLength = editRecipePage.instructionControls.length;
+ expect(newLength).toBe(initialLength - 1);
+ expect(editRecipePage.getInstructions()).toEqual(['Step 2', 'Step 3'])
+ }
+ );
+});
+
+
+describe('Testing Tags', () => {
+ let component: EditRecipeComponent;
+ let fb: FormBuilder;
+ let fixture: ComponentFixture;
+ let store: Store;
+ let dispatchSpy: jest.SpyInstance;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [ EditRecipeComponent ],
+ providers: [FormBuilder],
+ imports: [
+ ReactiveFormsModule,
+ HttpClientModule,
+ NavigationBarModule,
+ RouterTestingModule,
+ NgxsModule.forRoot([MockCreateState])
+ ]
+ });
+
+ fixture = TestBed.createComponent(EditRecipeComponent);
+ component = fixture.componentInstance;
+ fb = TestBed.inject(FormBuilder);
+ fixture.detectChanges();
+ store = TestBed.inject(Store);
+ dispatchSpy = jest.spyOn(store, 'dispatch');
+
+ component.recipeForm = fb.group({
+ tags: [''],
+ });
+ });
+
+ it("Should selet a meal type successfully", () => {
+ const mealType = 'Breakfast';
+ component.selectedMeal = mealType;
+ jest.spyOn(component, 'toggleMeal');
+
+ // Act
+ component.toggleMeal(mealType);
+
+ // Assert
+ expect(component.selectedMeal).toBe(mealType)
+ expect(component.toggleMeal).toBeCalledWith(mealType)
+ })
+
+ it("The selected meals should change when the user changes", () => {
+
+ const mealType = 'Lunch';
+ component.selectedMeal = mealType;
+
+ // Act
+ const mealType2 = 'Dinner';
+ // Act
+ component.toggleMeal(mealType2);
+
+ // Assert
+ expect(component.selectedMeal).toBe(mealType2);
+ expect(component.selectedMeal).not.toBe(mealType);
+
+ })
+
+ it("Should selet a difficulty successfully", () => {
+ const difficulty = 'Easy';
+ component.difficulty = difficulty;
+ jest.spyOn(component, 'toggleDifficulty');
+
+ // Act
+ component.toggleDifficulty(difficulty);
+
+ // Assert
+ expect(component.difficulty).toBe(difficulty);
+ expect(component.toggleDifficulty).toBeCalledWith(difficulty);
+ })
+
+ it("The selected difficulty should change when the user changes", () => {
+
+ const difficulty1 = 'Easy';
+ component.difficulty = difficulty1;
+
+ // Act
+ const difficulty2 = 'Medium';
+ // Act
+ component.toggleDifficulty(difficulty2);
+
+ // Assert
+ expect(component.difficulty).toBe(difficulty2);
+ expect(component.toggleDifficulty).not.toBe(difficulty1);
+
+ })
+
+ it('should not add a tag if tagValue is empty', () => {
+ // Arrange
+ component.recipeForm.get('tag')?.setValue('');
+ const size = component.tags.length;
+ // Act
+ component.addTag();
+
+ // Assert
+ expect(component.tags.length).toBe(size);
+ expect(dispatchSpy).toHaveBeenCalledWith(new ShowError('Please enter valid tag'));
+ });
+
+ it('should not add a duplicate tag', () => {
+ // Arrange
+
+ component.recipeForm.get('tags')?.setValue('Tag');
+ const testTags = ['Tag'];
+ component.tags = testTags;
+ const size = component.tags.length;
+
+ // Act
+ component.addTag();
+
+ // Assert
+ expect(component.tags.length).toBe(size);
+ expect(dispatchSpy).toHaveBeenCalledWith(new ShowError('No duplicates: Tag already selected'));
+ expect(component.tags).toEqual(testTags);
+
+ });
+
+ it('Should not add if tags is already at size three(3)', () => {
+ // Arrange
+ component.recipeForm.get('tags')?.setValue('Tag 4');
+ const testTags = ['Tag 1', 'Tag 2', 'Tag 3'];
+ component.tags = testTags;
+
+ // Act
+ component.addTag();
+
+ // Assert
+ expect(component.tags.length).toBe(3);
+ expect(dispatchSpy).toHaveBeenCalledWith(new ShowError('Only a maximum of three tags'));
+ expect(component.tags).toEqual(testTags);
+ })
+
+ it('should add a tag if tagValue is not empty', () => {
+ // Arrange
+ component.recipeForm.get('tags')?.setValue('Tag 1');
+
+ // Act
+ component.addTag();
+
+ const testTagsOutput = ['Tag 1'];
+ // Assert
+ expect(component.tags.length).toBe(1);
+ expect(component.tags).toEqual(testTagsOutput);
+ });
+
+ it("Should delete a meal tag successfully", () => {
+
+ const testTags = ['Tag 1', 'Tag 2', 'Tag 3'];
+ component.tags = testTags;
+
+ component.deleteTag(0);
+
+ const testTagsOutput = ['Tag 2', 'Tag 3'];
+ // Assert
+ expect(component.tags.length).toBe(2);
+ expect(component.tags).toEqual(testTagsOutput);
+ })
+
+});
+
+
+describe('Ingredients storing, deleting and returning', () => {
+ let component: EditRecipeComponent;
+ let fixture: ComponentFixture;
+ let formBuilder: FormBuilder;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [ EditRecipeComponent ],
+ providers: [FormBuilder],
+ imports: [
+ ReactiveFormsModule,
+ HttpClientModule,
+ NavigationBarModule,
+ RouterTestingModule,
+ NgxsModule.forRoot([MockCreateState])
+ ]
+ });
+ fixture = TestBed.createComponent(EditRecipeComponent);
+ component = fixture.componentInstance;
+ formBuilder = TestBed.inject(FormBuilder);
+
+ fixture.detectChanges();
+
+ });
+
+ it('Gets an array of IIngredient objects ', () => {
+ // create a mock form array with some form controls
+ const formArray = new FormArray([
+ new FormControl({
+ name: 'Mango',
+ amount: 100,
+ unit: 'g'
+ }),
+ new FormControl({
+ name: 'Potato',
+ amount: 1,
+ unit: 'kg'
+ }),
+ new FormControl({
+ name: 'Banana',
+ amount: 300,
+ unit: 'g'
+ }),
+ new FormControl({
+ name: 'Salad',
+ amount: 100,
+ unit: 'g'
+ }),
+ new FormControl({
+ name: 'Onion',
+ amount: 1,
+ unit: 'whole'
+ }),
+ ]);
+
+ // create a new recipe form using the form array
+ const recipeForm = new FormGroup({
+ ingredients: formArray,
+ });
+
+ component.recipeForm = recipeForm;
+
+ const ingredients : IIngredient[] = component.getIngredients();
+
+
+ // assert that the instructions array was created correctly
+ expect(ingredients[0]).toEqual({ name: "Mango", amount: 100, unit: "g" });
+ expect(ingredients[1]).toEqual({ name: "Potato", amount: 1, unit: "kg" })
+ expect(ingredients[2]).toEqual({ name: "Banana", amount: 300, unit: "g" })
+ expect(ingredients[3]).toEqual({ name: "Salad", amount: 100, unit: "g" })
+ expect(ingredients[4]).toEqual({ name: "Onion", amount: 1, unit: "whole" })
+
+ })
+
+ it('should remove the ingredient at the specified index', () => {
+
+ component.recipeForm = formBuilder.group({
+ ingredients: formBuilder.array([
+ formBuilder.group({
+ name: ['Ingredient 1', Validators.required],
+ amount: [1, Validators.required],
+ scale: ['kg', Validators.required],
+ }),
+ formBuilder.group({
+ name: ['Ingredient 2', Validators.required],
+ amount: [2, Validators.required],
+ scale: ['g', Validators.required],
+ }),
+ ]),
+ });
+
+ // Arrange
+ const indexToRemove = 1;
+ const initialIngredientsCount = component.ingredientControls.length;
+
+ // Act
+ component.removeIngredient(indexToRemove);
+
+ // Assert
+ const finalIngredientsCount = component.ingredientControls.length;
+ expect(finalIngredientsCount).toBe(initialIngredientsCount - 1);
+ expect(component.ingredientControls[1]).toBeUndefined();
+ });
+
+ });
+
+ describe("Testing placeholder texts for Amount", () => {
+
+ let component: EditRecipeComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [ EditRecipeComponent ],
+ providers: [FormBuilder],
+ imports: [
+ ReactiveFormsModule,
+ HttpClientModule,
+ NavigationBarModule,
+ RouterTestingModule,
+ NgxsModule.forRoot([MockCreateState])
+ ]
+ });
+ fixture = TestBed.createComponent(EditRecipeComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should return "e.g 10" when window width is less than 1024', () => {
+ // Arrange
+ global.innerWidth = 800; // Set the window width to a value less than 1024
+
+ // Act
+ const placeholderText = component.getAmountPlaceholderText();
+
+ // Assert
+ expect(placeholderText).toBe('e.g 10');
+ });
+
+ it('should return "Amount" when window width is greater than or equal to 1024', () => {
+ // Arrange
+ global.innerWidth = 1200; // Set the window width to a value greater than or equal to 1024
+
+ // Act
+ const placeholderText = component.getAmountPlaceholderText();
+
+ // Assert
+ expect(placeholderText).toBe('Amount');
+ });
+ })
+
+ describe("Testing placeholder texts for Unit", () => {
+
+ let component: EditRecipeComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [ EditRecipeComponent ],
+ providers: [FormBuilder],
+ imports: [
+ ReactiveFormsModule,
+ HttpClientModule,
+ NavigationBarModule,
+ RouterTestingModule,
+ NgxsModule.forRoot([MockCreateState])
+ ]
+ });
+ fixture = TestBed.createComponent(EditRecipeComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should return "e.g 10" when window width is less than 1024', () => {
+ // Arrange
+ global.innerWidth = 800; // Set the window width to a value less than 1024
+
+ // Act
+ const placeholderText = component.getUnitPlaceholderText();
+
+ // Assert
+ expect(placeholderText).toBe('e.g L');
+ });
+
+ it('should return "Amount" when window width is greater than or equal to 1024', () => {
+ // Arrange
+ global.innerWidth = 1200; // Set the window width to a value greater than or equal to 1024
+
+ // Act
+ const placeholderText = component.getUnitPlaceholderText();
+
+ // Assert
+ expect(placeholderText).toBe('Unit');
+ });
+ })
+
+ describe("Image upload", () => {
+
+ let component: EditRecipeComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [ EditRecipeComponent ],
+ providers: [FormBuilder],
+ imports: [
+ ReactiveFormsModule,
+ HttpClientModule,
+ NavigationBarModule,
+ RouterTestingModule,
+ NgxsModule.forRoot([MockCreateState])
+ ]
+ });
+ fixture = TestBed.createComponent(EditRecipeComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should update the imageUrl when a file is selected', () => {
+ // Arrange
+ const file = new File(['sample content'], 'sample.jpg', { type: 'image/jpeg' });
+ const event = { target: { files: [file] } };
+ const existingImage = component.imageUrl;
+
+ const readAsDataURLStringSpy = jest.spyOn(FileReader.prototype, 'readAsDataURL');
+
+ // Act
+ component.onFileChanged(event);
+
+ // Assert
+ expect(readAsDataURLStringSpy).toHaveBeenCalledWith(file);
+
+ const reader = new FileReader();
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ reader.addEventListener("load", function(event) {
+ expect(component.imageUrl).toBe(file.name);
+ expect(component.imageUrl).not.toBe(existingImage);
+ });
+ });
+
+ });
+
+ describe('isFormValid()', () =>{
+
+ let component: EditRecipeComponent;
+ let fixture: ComponentFixture;
+ let store: Store;
+ let dispatchSpy: jest.SpyInstance;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [ EditRecipeComponent ],
+ providers: [FormBuilder],
+ imports: [
+ ReactiveFormsModule,
+ HttpClientModule,
+ NavigationBarModule,
+ RouterTestingModule,
+ NgxsModule.forRoot([MockCreateState])
+ ]
+ });
+ fixture = TestBed.createComponent(EditRecipeComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ store = TestBed.inject(Store);
+ dispatchSpy = jest.spyOn(store, 'dispatch');
+ });
+
+ it('At least one ingredient should present', () => {
+
+ const formBuilder: FormBuilder = new FormBuilder();
+
+ const formGroup: FormGroup = formBuilder.group({
+ name: ['Name', Validators.required],
+ description: ['Description', Validators.required],
+ servings: [1, Validators.required],
+ preparationTime: [1, Validators.required],
+ ingredients: new FormArray([])
+ })
+
+ component.recipeForm = formGroup;
+ component.isFormValid();
+
+ expect(dispatchSpy).toHaveBeenCalledWith(new ShowError('No Ingredients'));
+
+ })
+
+ it('At least one instruction step should present', () => {
+
+ const formBuilder: FormBuilder = new FormBuilder();
+ const ingredientsFormArray = new FormArray([
+ new FormControl({
+ name: 'Mango',
+ amount: 100,
+ unit: 'g'
+ })])
+
+ const formGroup: FormGroup = formBuilder.group({
+ name: ['Name', Validators.required],
+ description: ['Description', Validators.required],
+ servings: [1, Validators.required],
+ preparationTime: [1, Validators.required],
+ ingredients: ingredientsFormArray,
+ instructions: new FormArray([])
+ })
+
+ component.recipeForm = formGroup;
+ component.isFormValid();
+
+ expect(dispatchSpy).toHaveBeenCalledWith(new ShowError('No Instructions'));
+
+ })
+
+ it('Tags if empty', () => {
+ const formBuilder: FormBuilder = new FormBuilder();
+ const ingredientsFormArray = new FormArray([
+ new FormControl({
+ name: 'Mango',
+ amount: 100,
+ unit: 'g'
+ })])
+ const instructionsFormArray = new FormArray([
+ new FormControl('Step 1')
+ ]);
+
+ const formGroup: FormGroup = formBuilder.group({
+ name: ['Name', Validators.required],
+ description: ['Description', Validators.required],
+ servings: [1, Validators.required],
+ preparationTime: [1, Validators.required],
+ ingredients: ingredientsFormArray,
+ instructions: instructionsFormArray
+ })
+
+ component.recipeForm = formGroup;
+ component.isFormValid();
+ expect(dispatchSpy).toHaveBeenCalledWith(new ShowError('No Tags'));
+ });
+
+
+ it('Meal Selection', () => {
+ const formBuilder: FormBuilder = new FormBuilder();
+ const ingredientsFormArray = new FormArray([
+ new FormControl({
+ name: 'Mango',
+ amount: 100,
+ unit: 'g'
+ })])
+ const instructionsFormArray = new FormArray([
+ new FormControl('Step 1')
+ ]);
+
+ const formGroup: FormGroup = formBuilder.group({
+ ingredients: ingredientsFormArray,
+ instructions: instructionsFormArray
+ })
+
+ component.tags = ['Asian']
+
+ component.recipeForm = formGroup;
+ component.isFormValid();
+ expect(dispatchSpy).toHaveBeenCalledWith(new ShowError('Please select a meal'));
+
+ })
+
+
+ it('Truthy Profile', () => {
+ const formBuilder: FormBuilder = new FormBuilder();
+ const ingredientsFormArray = new FormArray([
+ new FormControl({
+ name: 'Mango',
+ amount: 100,
+ unit: 'g'
+ })])
+ const instructionsFormArray = new FormArray([
+ new FormControl('Step 1')
+ ]);
+
+ const formGroup: FormGroup = formBuilder.group({
+ name: ['Name', Validators.required],
+ description: ['Description', Validators.required],
+ servings: [1, Validators.required],
+ preparationTime: [1, Validators.required],
+ ingredients: ingredientsFormArray,
+ instructions: instructionsFormArray
+ })
+
+ component.recipeForm = formGroup;
+ component.tags = ['Asian'];
+ component.selectedMeal = 'Breakfast';
+ component.isFormValid();
+ expect(dispatchSpy).toHaveBeenCalledWith(new ShowError('Please login to create a recipe'));
+ })
+
+
+ it('Form fields validation', () => {
+ const formBuilder: FormBuilder = new FormBuilder();
+ const ingredientsFormArray = new FormArray([
+ new FormControl({
+ name: 'Mango',
+ amount: 100,
+ unit: 'g'
+ })])
+ const instructionsFormArray = new FormArray([
+ new FormControl('Step 1')
+ ]);
+
+ const formGroup: FormGroup = formBuilder.group({
+ name: ['', Validators.required],
+ description: ['', Validators.required],
+ servings: [1, Validators.required],
+ preparationTime: [1, Validators.required],
+ ingredients: ingredientsFormArray,
+ instructions: instructionsFormArray,
+ tags: formBuilder.array([]),
+ })
+
+ const testProfile: IProfile = {
+ displayName: "John Doe",
+ username: "jdoe",
+ email: "jdoe@gmail.com",
+ savedRecipes: [],
+ ingredients: [],
+ profilePic: "image-url",
+ createdRecipes: [],
+ currMealPlan: null,
+ };
+
+ component.recipeForm = formGroup;
+ component.selectedMeal = 'Breakfast';
+ component.tags = ['Asian'];
+ component.profile = testProfile;
+ component.isFormValid();
+ expect(dispatchSpy).toHaveBeenCalledWith(new ShowError('Incomplete Form. Please fill out every field.'))
+ })
+
+
+ it('The form should test valid', () => {
+ const formBuilder: FormBuilder = new FormBuilder();
+
+ const stepsFormArray = new FormArray([
+ new FormControl('Step 1'),
+ new FormControl('Step 2'),
+ new FormControl('Step 3'),
+ ]);
+
+ const ingredientsFormArray = new FormArray([
+ new FormControl({
+ name: 'Mango',
+ amount: 100,
+ unit: 'g'
+ }),
+ new FormControl({
+ name: 'Potato',
+ amount: 1,
+ unit: 'kg'
+ }),
+ new FormControl({
+ name: 'Banana',
+ amount: 300,
+ unit: 'g'
+ }),
+ new FormControl({
+ name: 'Salad',
+ amount: 100,
+ unit: 'g'
+ }),
+ new FormControl({
+ name: 'Onion',
+ amount: 1,
+ unit: 'whole'
+ }),
+ ]);
+
+ const testProfile: IProfile = {
+ displayName: "John Doe",
+ username: "jdoe",
+ email: "jdoe@gmail.com",
+ savedRecipes: [],
+ ingredients: [],
+ profilePic: "image-url",
+ createdRecipes: [],
+ currMealPlan: null,
+ };
+
+ const formGroup: FormGroup = formBuilder.group({
+ name: ['Name', Validators.required],
+ description: ['Description', Validators.required],
+ servings: [1, Validators.required],
+ preparationTime: [1, Validators.required],
+ ingredients: ingredientsFormArray,
+ instructions: stepsFormArray,
+ tags: formBuilder.array([]),
+ });
+
+ component.selectedMeal = 'Breakfast';
+ component.tags = ['Asian'];
+ component.profile = testProfile
+
+ component.recipeForm = formGroup;
+ expect(component.isFormValid()).toBe(true);
+ })
+
+ })
+
+ describe("Testing Recipe Update ", () => {
+ let component: EditRecipeComponent;
+ let fb: FormBuilder;
+ let fixture: ComponentFixture;
+ let store: Store;
+ let dispatchSpy: jest.SpyInstance;
+
+ const testProfile: IProfile = {
+ displayName: "John Doe",
+ username: "jdoe",
+ email: "jdoe@gmail.com",
+ savedRecipes: [],
+ ingredients: [],
+ profilePic: "image-url",
+ createdRecipes: [],
+ currMealPlan: null,
+ };
+
+ @State({
+ name: 'profile',
+ defaults: {
+ profile: testProfile
+ }
+ })
+ @Injectable()
+ class MockProfileState {}
+
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [ EditRecipeComponent ],
+ providers: [FormBuilder, Store],
+ imports: [
+ ReactiveFormsModule,
+ HttpClientModule,
+ NavigationBarModule,
+ RouterTestingModule,
+ NgxsModule.forRoot([MockCreateState, MockProfileState])
+ ]
+ });
+ fixture = TestBed.createComponent(EditRecipeComponent);
+ component = fixture.componentInstance;
+ fb = TestBed.inject(FormBuilder);
+ store = TestBed.inject(Store);
+ dispatchSpy = jest.spyOn(store, 'dispatch');
+ });
+
+
+
+ it('Should dispatch Update Recipe Action', async () => {
+
+ jest.spyOn(component, 'isFormValid');
+
+ // Mock the recipe data
+ const recipe: IRecipe = {
+ name: "Mock Recipe",
+ recipeImage: "https://example.com/image.jpg",
+ description: "Amazing meal for a family",
+ meal: "Dinner",
+ creator: '',
+ ingredients: [ {name: 'ingredient1' , amount : 5, unit : 'L'},
+ {name: 'ingredient2' , amount : 3, unit : 'g'}
+ ],
+ steps: [
+ "Mock instructions",
+ ],
+ difficulty: "Easy",
+ prepTime: 30,
+ servings: 4,
+ tags: ["mock", "recipe"],
+ };
+
+ component.imageUrl = recipe.recipeImage
+ // Mock the values and controls used in createRecipe
+ component.recipeForm = fb.group({
+ name: fb.control(recipe.name),
+ description: fb.control(recipe.description),
+ difficulty: fb.control(recipe.difficulty),
+ servings: fb.control(recipe.servings),
+ preparationTime: fb.control(recipe.prepTime),
+ ingredients: fb.array(recipe.ingredients.map(ingredient => fb.control(ingredient))),
+ instructions: fb.array(recipe.steps.map(instruction => fb.control(instruction))),
+ dietaryPlans: fb.array((recipe.tags || []).map(tag => fb.control(tag))),
+ });
+
+ component.tags = recipe.tags;
+ component.selectedMeal = recipe.meal;
+ component.profile = testProfile;
+ component.profile.createdRecipes = [recipe];
+
+ // Call the createRecipe method
+ component.updateRecipe();
+ expect(dispatchSpy).toHaveBeenCalledWith(new UpdateRecipe(recipe));
+ expect(dispatchSpy).toHaveBeenCalledWith(new UpdateProfile(testProfile));
+ expect(dispatchSpy).toHaveBeenCalledWith(new Navigate([`/recipe/${recipe.recipeId}`]));
+ expect(component.recipeForm.valid).toBe(true);
+ expect(component.isFormValid()).toBe(true)
+ expect(component.isFormValid).toHaveBeenCalled();
+
+ });
+
+
+ it('Should dispatch Could not update recipe error', () => {
+ component = fixture.componentInstance;
+ jest.spyOn(component, 'isFormValid');
+
+ // Mock the recipe data
+ const recipe: IRecipe = {
+ name: "Mock Recipe",
+ recipeImage: "https://example.com/image.jpg",
+ description: "Amazing meal for a family",
+ meal: "Dinner",
+ creator: '',
+ ingredients: [ {name: 'ingredient1' , amount : 5, unit : 'L'},
+ {name: 'ingredient2' , amount : 3, unit : 'g'}
+ ],
+ steps: [
+ "Mock instructions",
+ ],
+ difficulty: "Easy",
+ prepTime: 30,
+ servings: 4,
+ tags: ["mock", "recipe"],
+ };
+
+ component.imageUrl = recipe.recipeImage
+ // Mock the values and controls used in createRecipe
+ component.recipeForm = fb.group({
+ name: fb.control(recipe.name),
+ description: fb.control(recipe.description),
+ difficulty: fb.control(recipe.difficulty),
+ servings: fb.control(recipe.servings),
+ preparationTime: fb.control(recipe.prepTime),
+ ingredients: fb.array(recipe.ingredients.map(ingredient => fb.control(ingredient))),
+ instructions: fb.array(recipe.steps.map(instruction => fb.control(instruction))),
+ dietaryPlans: fb.array((recipe.tags || []).map(tag => fb.control(tag))),
+ });
+
+ component.tags = recipe.tags;
+ component.selectedMeal = recipe.meal;
+ component.profile = testProfile;
+ component.profile.createdRecipes = [];
+
+ // Call the createRecipe method
+ component.updateRecipe();
+ expect(dispatchSpy).toHaveBeenCalledWith(new ShowError('Could not update recipe'));
+ })
+
+ it('Should not create recipe if form is invalid', () => {
+
+ jest.spyOn(component, 'isFormValid');
+
+
+ const profileDataSubject = new BehaviorSubject(undefined);
+ // Mock the recipe data
+ const recipe: IRecipe = {
+ name: "Mock Recipe",
+ recipeImage: "https://example.com/image.jpg",
+ description: "Amazing meal for a family",
+ meal: "Dinner",
+ creator: profileDataSubject.value?.username ?? '',
+ ingredients: [],
+ steps: [],
+ difficulty: "Easy",
+ prepTime: 30,
+ servings: 4,
+ tags: ["mock", "recipe"],
+ };
+
+ component.imageUrl = recipe.recipeImage
+ // Mock the values and controls used in createRecipe
+ component.recipeForm = fb.group({
+ name: fb.control(recipe.name),
+ description: fb.control(recipe.description),
+ difficulty: fb.control(recipe.difficulty),
+ servings: fb.control(recipe.servings),
+ preparationTime: fb.control(recipe.prepTime),
+ ingredients: fb.array(recipe.ingredients.map(ingredient => fb.control(ingredient))),
+ instructions: fb.array(recipe.steps.map(instruction => fb.control(instruction))),
+ dietaryPlans: fb.array((recipe.tags || []).map(tag => fb.control(tag))),
+ });
+
+ component.tags = recipe.tags;
+ component.selectedMeal = recipe.meal;
+
+ // Call the createRecipe method
+ component.updateRecipe();
+ expect(component.isFormValid).toHaveBeenCalled();
+ expect(component.isFormValid()).toBe(false)
+ expect(dispatchSpy).not.toHaveBeenCalledWith(new UpdateRecipe(recipe));
+ })
+
+
+
+
+
+ })
diff --git a/libs/app/edit-recipe/feature/src/edit-recipe.page.ts b/libs/app/edit-recipe/feature/src/edit-recipe.page.ts
new file mode 100644
index 00000000..6a3e2cd4
--- /dev/null
+++ b/libs/app/edit-recipe/feature/src/edit-recipe.page.ts
@@ -0,0 +1,331 @@
+import { Component, OnInit } from '@angular/core';
+import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { DeleteRecipe, IRecipe, RetrieveRecipe, UpdateRecipe } from '@fridge-to-plate/app/recipe/utils';
+import { IIngredient } from '@fridge-to-plate/app/ingredient/utils';
+import { Select, Store, ofActionSuccessful, Actions } from '@ngxs/store';
+import { ShowError } from '@fridge-to-plate/app/error/utils';
+import { IProfile, UpdateProfile } from '@fridge-to-plate/app/profile/utils';
+import { Location } from '@angular/common';
+import { ActivatedRoute } from '@angular/router';
+import { RecipeState } from '@fridge-to-plate/app/edit-recipe/data-access';
+import { Observable, take } from 'rxjs';
+import { ProfileState } from '@fridge-to-plate/app/profile/data-access';
+import { Navigate } from '@ngxs/router-plugin';
+
+@Component({
+ selector: 'fridge-to-plate-edit-recipe',
+ templateUrl: './edit-recipe.page.html',
+ styleUrls: ['./edit-recipe.page.css'],
+})
+export class EditRecipeComponent implements OnInit {
+
+ recipeForm!: FormGroup;
+ imageUrl = 'https://img.freepik.com/free-photo/frying-pan-empty-with-various-spices-black-table_1220-561.jpg';
+ selectedMeal!: "Breakfast" | "Lunch" | "Dinner" | "Snack" | "Dessert";
+ difficulty: "Easy" | "Medium" | "Hard" = "Easy";
+ tags: string[] = [];
+ profile !: IProfile;
+ recipeId !: string;
+ recipe !: IRecipe | null;
+
+ @Select(RecipeState.getEditRecipe) recipe$ !: Observable;
+ @Select(ProfileState.getProfile) profile$ !: Observable;
+
+ constructor(private fb: FormBuilder, private store : Store, private location: Location, public route: ActivatedRoute, private actions$: Actions) {}
+
+ ngOnInit() {
+ this.createForm();
+ this.profile$.pipe(take(1)).subscribe( (profile: IProfile) => {this.profile = profile})
+
+ }
+
+ createForm(): void {
+ this.initialize();
+ this.recipeForm = this.fb.group({
+ name: [this.recipe?.name, Validators.required],
+ description: [this.recipe?.description, Validators.required],
+ servings: [this.recipe?.servings, Validators.required],
+ preparationTime: [this.recipe?.prepTime, Validators.required],
+ ingredients: this.fb.array([]),
+ instructions: this.fb.array([]),
+ tags: [''],
+ });
+ this.populateForm();
+ }
+
+ initialize(): void {
+ this.recipe$.pipe(take(1)).subscribe(recipe =>
+ {
+ this.recipe = recipe;
+ if(recipe.recipeId) {
+ this.recipeId = recipe.recipeId;
+ }
+ }); // ?
+ }
+
+ populateForm(): void {
+
+ this.recipe?.ingredients.forEach((ingredient) => {
+ const ingredientGroup = this.fb.group({
+ name: [ingredient.name, Validators.required],
+ amount: [ingredient.amount, Validators.required],
+ unit: [ingredient.unit, Validators.required]
+ });
+
+ (this.recipeForm.get('ingredients') as FormArray).push(ingredientGroup);
+ }
+ );
+
+ this.recipe?.steps.forEach((step) => {
+ this.instructionControls.push(this.fb.control(step, Validators.required));
+ }
+ );
+ this.tags = this.recipe?.tags ?? this.tags;
+ this.selectedMeal = this.recipe?.meal ?? this.selectedMeal;
+ this.imageUrl = this.recipe?.recipeImage ?? this.imageUrl
+ this.difficulty = this.recipe?.difficulty ?? this.difficulty;
+ }
+
+ get ingredientControls() {
+ return (this.recipeForm.get('ingredients') as FormArray).controls;
+ }
+
+ addIngredient() {
+ const ingredientGroup = this.fb.group({
+ name: ['', Validators.required],
+ amount: ['', Validators.required],
+ unit: ['', Validators.required]
+ });
+
+ // Add the new ingredient group to the FormArray
+ (this.recipeForm.get('ingredients') as FormArray).push(ingredientGroup);
+ }
+
+ get instructionControls() {
+ return (this.recipeForm.get('instructions') as FormArray).controls;
+ }
+
+ addInstruction(): void {
+ this.instructionControls.push(this.fb.control('', Validators.required));
+ }
+
+ removeIngredient(index: number): void {
+ this.ingredientControls.splice(index, 1);
+ }
+
+ removeInstruction(index: number) : void{
+ this.instructionControls.splice(index, 1);
+ }
+
+ getAmountPlaceholderText() {
+ if (window.innerWidth < 1024) {
+ return "e.g 10";
+ } else {
+ return "Amount";
+ }
+ }
+
+ getUnitPlaceholderText() {
+ if (window.innerWidth < 1024) {
+ return "e.g L";
+ } else {
+ return "Unit";
+ }
+ }
+
+ updateRecipe() : void {
+ // Check first if the form is completely valid
+ if(!this.isFormValid())
+ return;
+
+ // Ingredients array
+ const ingredients = this.getIngredients();
+
+ // Instructions array
+ const instructions = this.getInstructions();
+
+ // Create Recipe details
+ const recipe: IRecipe = {
+ recipeId: this.recipe?.recipeId,
+ name: this.recipeForm.value.name,
+ recipeImage: this.imageUrl,
+ description: this.recipeForm.value.description,
+ meal: this.selectedMeal,
+ creator: this.recipe?.creator ?? '',
+ ingredients: ingredients,
+ steps: instructions,
+ difficulty: this.difficulty,
+ prepTime: this.recipeForm.value.preparationTime as number,
+ servings: this.recipeForm.value.servings as number,
+ tags: this.tags,
+ };
+
+ this.profile$.pipe(take(1)).subscribe( (profile: IProfile) => {
+ const index = profile.createdRecipes.findIndex( recipe => this.recipeId === recipe.recipeId);
+ if(index === -1) {
+ this.store.dispatch( new ShowError('Could not update recipe'));
+ return;
+ }
+ this.store.dispatch( new UpdateRecipe(recipe) );
+ profile.createdRecipes[index] = recipe;
+ this.store.dispatch( new UpdateProfile(profile))
+ this.store.dispatch(new Navigate([`/recipe/${this.recipeId}`]))
+ })
+
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ onFileChanged(event: any) {
+ const file = event.target.files[0];
+ const reader = new FileReader();
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ reader.onload = (e: any) => {
+ this.imageUrl = e.target.result;
+ };
+
+ reader.readAsDataURL(file);
+ }
+
+ deleteRecipe() {
+
+ if(!this.recipe?.recipeId) {
+ this.store.dispatch( new ShowError('Could not delete recipe'));
+ }
+
+ this.store.dispatch( new DeleteRecipe( this.recipe?.recipeId as string ))
+ this.profile$.pipe(take(1)).subscribe( (profile: IProfile) => {
+ profile.createdRecipes = profile.createdRecipes.filter( recipe => this.recipeId !== recipe.recipeId);
+ this.store.dispatch( new UpdateProfile(profile))
+ })
+ this.location.back()
+ }
+
+ toggleMeal(option: "Breakfast" | "Lunch" | "Dinner" | "Snack" | "Dessert") {
+ this.selectedMeal = option;
+ }
+
+ getMealPlan(option: string) {
+ return {
+ 'bg-primary': this.selectedMeal === option,
+ 'bg-gray-200': this.selectedMeal !== option,
+ 'text-white': this.selectedMeal === option,
+ 'text-gray-700': this.selectedMeal !== option,
+ 'py-2': true,
+ 'px-4': true,
+ 'rounded-md': true,
+ 'mr-2': true
+ };
+ }
+
+ toggleDifficulty(option: "Easy" | "Medium" | "Hard") {
+ this.difficulty = option;
+ }
+
+ getDifficulty(option: string) {
+ return {
+ 'bg-primary': this.difficulty === option,
+ 'bg-gray-200': this.difficulty !== option,
+ 'text-white': this.difficulty === option,
+ 'text-gray-700': this.difficulty !== option,
+ 'py-2': true,
+ 'px-4': true,
+ 'rounded-md': true,
+ 'mr-2': true
+ };
+ }
+
+
+ addTag() {
+ const tagValue = this.recipeForm.get('tags')?.value as string;
+ if(!tagValue) {
+ this.store.dispatch( new ShowError("Please enter valid tag"))
+ }
+ else if (this.tags.length < 3) {
+ if(this.tags.includes(tagValue)){
+ this.store.dispatch( new ShowError("No duplicates: Tag already selected"))
+ return;
+ }
+ this.tags.push(tagValue);
+ }
+ else {
+ this.store.dispatch( new ShowError("Only a maximum of three tags"))
+ }
+ // reset the form value after adding it to array
+ this.recipeForm.get('tags')?.reset();
+ }
+
+ deleteTag(index: number) {
+ this.tags.splice(index, 1);
+ }
+
+ isFormValid(): boolean {
+
+ if(!this.recipeForm.valid){
+ this.store.dispatch( new ShowError("Incomplete Form. Please fill out every field."))
+ return false;
+ }
+
+ if(this.ingredientControls.length < 1) {
+ this.store.dispatch( new ShowError("No Ingredients"))
+ return false;
+ }
+
+ if(this.instructionControls.length < 1) {
+ this.store.dispatch( new ShowError("No Instructions"))
+ return false;
+ }
+
+ if(this.tags.length < 1) {
+ this.store.dispatch( new ShowError("No Tags"))
+ return false;
+ }
+
+ if(!this.selectedMeal){
+ this.store.dispatch( new ShowError("Please select a meal"))
+ return false;
+ }
+
+ if(!this.profile){
+ this.store.dispatch( new ShowError("Please login to create a recipe"))
+ return false;
+ }
+
+ return true;
+ }
+
+ getIngredients(): IIngredient[] {
+ const ingredients: IIngredient[] = [];
+ this.ingredientControls.forEach((ingredient) => {
+ if (ingredient.value) {
+ ingredients.push({
+ name: ingredient.value.name,
+ amount: ingredient.value.amount,
+ unit: ingredient.value.unit
+ })
+ }
+ });
+
+ return ingredients;
+ }
+
+ getInstructions() : string[] {
+ const instructions: string[] = [];
+ this.instructionControls.forEach((element) => {
+ if (element.value) {
+ instructions.push(element.value);
+ }
+ });
+
+ return instructions;
+ }
+
+ cancelEdit(): void {
+ this.location.back();
+ }
+
+ goHome(): void {
+ this.store.dispatch(new Navigate(['/home']));
+ }
+
+}
diff --git a/libs/app/edit-recipe/feature/src/edit-recipe.routing.ts b/libs/app/edit-recipe/feature/src/edit-recipe.routing.ts
new file mode 100644
index 00000000..fb0614bd
--- /dev/null
+++ b/libs/app/edit-recipe/feature/src/edit-recipe.routing.ts
@@ -0,0 +1,18 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { EditRecipeComponent } from './edit-recipe.page';
+
+
+const routes: Routes = [
+ {
+ path: '',
+ pathMatch: 'full',
+ component: EditRecipeComponent,
+ },
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule],
+})
+export class EditRecipeRouting {}
\ No newline at end of file
diff --git a/libs/app/edit-recipe/feature/src/index.ts b/libs/app/edit-recipe/feature/src/index.ts
new file mode 100644
index 00000000..6f00f95c
--- /dev/null
+++ b/libs/app/edit-recipe/feature/src/index.ts
@@ -0,0 +1,3 @@
+export * from './edit-recipe.module';
+export * from './edit-recipe.page';
+export * from './edit-recipe.routing';
diff --git a/libs/app/edit-recipe/feature/src/test-setup.ts b/libs/app/edit-recipe/feature/src/test-setup.ts
new file mode 100644
index 00000000..1100b3e8
--- /dev/null
+++ b/libs/app/edit-recipe/feature/src/test-setup.ts
@@ -0,0 +1 @@
+import 'jest-preset-angular/setup-jest';
diff --git a/libs/app/edit-recipe/feature/tsconfig.json b/libs/app/edit-recipe/feature/tsconfig.json
new file mode 100644
index 00000000..b9e5be08
--- /dev/null
+++ b/libs/app/edit-recipe/feature/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ],
+ "extends": "../../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/libs/app/edit-recipe/feature/tsconfig.lib.json b/libs/app/edit-recipe/feature/tsconfig.lib.json
new file mode 100644
index 00000000..91273870
--- /dev/null
+++ b/libs/app/edit-recipe/feature/tsconfig.lib.json
@@ -0,0 +1,17 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../../dist/out-tsc",
+ "declaration": true,
+ "declarationMap": true,
+ "inlineSources": true,
+ "types": []
+ },
+ "exclude": [
+ "src/**/*.spec.ts",
+ "src/test-setup.ts",
+ "jest.config.ts",
+ "src/**/*.test.ts"
+ ],
+ "include": ["src/**/*.ts"]
+}
diff --git a/libs/app/edit-recipe/feature/tsconfig.spec.json b/libs/app/edit-recipe/feature/tsconfig.spec.json
new file mode 100644
index 00000000..6e5925e5
--- /dev/null
+++ b/libs/app/edit-recipe/feature/tsconfig.spec.json
@@ -0,0 +1,16 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../../dist/out-tsc",
+ "module": "commonjs",
+ "target": "es2016",
+ "types": ["jest", "node"]
+ },
+ "files": ["src/test-setup.ts"],
+ "include": [
+ "jest.config.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/libs/app/edit-recipe/utils/.eslintrc.json b/libs/app/edit-recipe/utils/.eslintrc.json
new file mode 100644
index 00000000..6bac7be5
--- /dev/null
+++ b/libs/app/edit-recipe/utils/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "rules": {
+ "@angular-eslint/directive-selector": [
+ "error",
+ {
+ "type": "attribute",
+ "prefix": "fridgeToPlate",
+ "style": "camelCase"
+ }
+ ],
+ "@angular-eslint/component-selector": [
+ "error",
+ {
+ "type": "element",
+ "prefix": "fridge-to-plate",
+ "style": "kebab-case"
+ }
+ ]
+ },
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ]
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/libs/app/edit-recipe/utils/README.md b/libs/app/edit-recipe/utils/README.md
new file mode 100644
index 00000000..a9110065
--- /dev/null
+++ b/libs/app/edit-recipe/utils/README.md
@@ -0,0 +1,7 @@
+# app-edit-recipe-utils
+
+This library was generated with [Nx](https://nx.dev).
+
+## Running unit tests
+
+Run `nx test app-edit-recipe-utils` to execute the unit tests.
diff --git a/libs/app/edit-recipe/utils/jest.config.ts b/libs/app/edit-recipe/utils/jest.config.ts
new file mode 100644
index 00000000..09351461
--- /dev/null
+++ b/libs/app/edit-recipe/utils/jest.config.ts
@@ -0,0 +1,22 @@
+/* eslint-disable */
+export default {
+ displayName: 'app-edit-recipe-utils',
+ preset: '../../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ coverageDirectory: '../../../../coverage/libs/app/edit-recipe/utils',
+ transform: {
+ '^.+\\.(ts|mjs|js|html)$': [
+ 'jest-preset-angular',
+ {
+ tsconfig: '/tsconfig.spec.json',
+ stringifyContentPathRegex: '\\.(html|svg)$',
+ },
+ ],
+ },
+ transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
+ snapshotSerializers: [
+ 'jest-preset-angular/build/serializers/no-ng-attributes',
+ 'jest-preset-angular/build/serializers/ng-snapshot',
+ 'jest-preset-angular/build/serializers/html-comment',
+ ],
+};
diff --git a/libs/app/edit-recipe/utils/project.json b/libs/app/edit-recipe/utils/project.json
new file mode 100644
index 00000000..1ba93108
--- /dev/null
+++ b/libs/app/edit-recipe/utils/project.json
@@ -0,0 +1,34 @@
+{
+ "name": "app-edit-recipe-utils",
+ "$schema": "../../../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "libs/app/edit-recipe/utils/src",
+ "prefix": "fridge-to-plate",
+ "tags": [],
+ "projectType": "library",
+ "targets": {
+ "test": {
+ "executor": "@nx/jest:jest",
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+ "options": {
+ "jestConfig": "libs/app/edit-recipe/utils/jest.config.ts",
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "codeCoverage": true
+ }
+ }
+ },
+ "lint": {
+ "executor": "@nx/linter:eslint",
+ "outputs": ["{options.outputFile}"],
+ "options": {
+ "lintFilePatterns": [
+ "libs/app/edit-recipe/utils/**/*.ts",
+ "libs/app/edit-recipe/utils/**/*.html"
+ ]
+ }
+ }
+ }
+}
diff --git a/libs/app/edit-recipe/utils/src/edit-recipe.actions.ts b/libs/app/edit-recipe/utils/src/edit-recipe.actions.ts
new file mode 100644
index 00000000..04cb7b55
--- /dev/null
+++ b/libs/app/edit-recipe/utils/src/edit-recipe.actions.ts
@@ -0,0 +1,5 @@
+
+export class LoadRecipe {
+ static readonly type = '[EditRecipe] Load Recipe';
+ constructor(public readonly recipeId: string) {}
+}
\ No newline at end of file
diff --git a/libs/app/edit-recipe/utils/src/edit-recipe.module.ts b/libs/app/edit-recipe/utils/src/edit-recipe.module.ts
new file mode 100644
index 00000000..8b75a244
--- /dev/null
+++ b/libs/app/edit-recipe/utils/src/edit-recipe.module.ts
@@ -0,0 +1,7 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+@NgModule({
+ imports: [CommonModule],
+})
+export class AppEditRecipeUtilsModule {}
diff --git a/libs/app/edit-recipe/utils/src/index.ts b/libs/app/edit-recipe/utils/src/index.ts
new file mode 100644
index 00000000..ab2e3094
--- /dev/null
+++ b/libs/app/edit-recipe/utils/src/index.ts
@@ -0,0 +1,2 @@
+export * from './edit-recipe.module';
+export * from './edit-recipe.actions'
diff --git a/libs/app/edit-recipe/utils/src/test-setup.ts b/libs/app/edit-recipe/utils/src/test-setup.ts
new file mode 100644
index 00000000..1100b3e8
--- /dev/null
+++ b/libs/app/edit-recipe/utils/src/test-setup.ts
@@ -0,0 +1 @@
+import 'jest-preset-angular/setup-jest';
diff --git a/libs/app/edit-recipe/utils/tsconfig.json b/libs/app/edit-recipe/utils/tsconfig.json
new file mode 100644
index 00000000..b9e5be08
--- /dev/null
+++ b/libs/app/edit-recipe/utils/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ],
+ "extends": "../../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/libs/app/edit-recipe/utils/tsconfig.lib.json b/libs/app/edit-recipe/utils/tsconfig.lib.json
new file mode 100644
index 00000000..91273870
--- /dev/null
+++ b/libs/app/edit-recipe/utils/tsconfig.lib.json
@@ -0,0 +1,17 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../../dist/out-tsc",
+ "declaration": true,
+ "declarationMap": true,
+ "inlineSources": true,
+ "types": []
+ },
+ "exclude": [
+ "src/**/*.spec.ts",
+ "src/test-setup.ts",
+ "jest.config.ts",
+ "src/**/*.test.ts"
+ ],
+ "include": ["src/**/*.ts"]
+}
diff --git a/libs/app/edit-recipe/utils/tsconfig.spec.json b/libs/app/edit-recipe/utils/tsconfig.spec.json
new file mode 100644
index 00000000..6e5925e5
--- /dev/null
+++ b/libs/app/edit-recipe/utils/tsconfig.spec.json
@@ -0,0 +1,16 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../../dist/out-tsc",
+ "module": "commonjs",
+ "target": "es2016",
+ "types": ["jest", "node"]
+ },
+ "files": ["src/test-setup.ts"],
+ "include": [
+ "jest.config.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/libs/app/environments/utils/.eslintrc.json b/libs/app/environments/utils/.eslintrc.json
new file mode 100644
index 00000000..6bac7be5
--- /dev/null
+++ b/libs/app/environments/utils/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "rules": {
+ "@angular-eslint/directive-selector": [
+ "error",
+ {
+ "type": "attribute",
+ "prefix": "fridgeToPlate",
+ "style": "camelCase"
+ }
+ ],
+ "@angular-eslint/component-selector": [
+ "error",
+ {
+ "type": "element",
+ "prefix": "fridge-to-plate",
+ "style": "kebab-case"
+ }
+ ]
+ },
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ]
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/libs/app/environments/utils/README.md b/libs/app/environments/utils/README.md
new file mode 100644
index 00000000..0223c8b7
--- /dev/null
+++ b/libs/app/environments/utils/README.md
@@ -0,0 +1,7 @@
+# app-environments-utils
+
+This library was generated with [Nx](https://nx.dev).
+
+## Running unit tests
+
+Run `nx test app-environments-utils` to execute the unit tests.
diff --git a/libs/app/environments/utils/jest.config.ts b/libs/app/environments/utils/jest.config.ts
new file mode 100644
index 00000000..221bf2e2
--- /dev/null
+++ b/libs/app/environments/utils/jest.config.ts
@@ -0,0 +1,22 @@
+/* eslint-disable */
+export default {
+ displayName: 'app-environments-utils',
+ preset: '../../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ coverageDirectory: '../../../../coverage/libs/app/environments/utils',
+ transform: {
+ '^.+\\.(ts|mjs|js|html)$': [
+ 'jest-preset-angular',
+ {
+ tsconfig: '/tsconfig.spec.json',
+ stringifyContentPathRegex: '\\.(html|svg)$',
+ },
+ ],
+ },
+ transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
+ snapshotSerializers: [
+ 'jest-preset-angular/build/serializers/no-ng-attributes',
+ 'jest-preset-angular/build/serializers/ng-snapshot',
+ 'jest-preset-angular/build/serializers/html-comment',
+ ],
+};
diff --git a/libs/app/create/data-access/project.json b/libs/app/environments/utils/project.json
similarity index 71%
rename from libs/app/create/data-access/project.json
rename to libs/app/environments/utils/project.json
index 925a46cc..88333096 100644
--- a/libs/app/create/data-access/project.json
+++ b/libs/app/environments/utils/project.json
@@ -1,7 +1,7 @@
{
- "name": "app-create-data-access",
+ "name": "app-environments-utils",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
- "sourceRoot": "libs/app/create/data-access/src",
+ "sourceRoot": "libs/app/environments/utils/src",
"prefix": "fridge-to-plate",
"tags": [],
"projectType": "library",
@@ -10,7 +10,7 @@
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
- "jestConfig": "libs/app/create/data-access/jest.config.ts",
+ "jestConfig": "libs/app/environments/utils/jest.config.ts",
"passWithNoTests": true
},
"configurations": {
@@ -25,8 +25,8 @@
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": [
- "libs/app/create/data-access/**/*.ts",
- "libs/app/create/data-access/**/*.html"
+ "libs/app/environments/utils/**/*.ts",
+ "libs/app/environments/utils/**/*.html"
]
}
}
diff --git a/libs/app/environments/utils/src/environment.ts b/libs/app/environments/utils/src/environment.ts
new file mode 100644
index 00000000..b0e9241b
--- /dev/null
+++ b/libs/app/environments/utils/src/environment.ts
@@ -0,0 +1,6 @@
+export const environment = {
+ TYPE: process.env['TYPE'],
+ API_URL: process.env['API_URL'],
+ COGNITO_USERPOOL_ID: process.env['COGNITO_USERPOOL_ID'] || 'none',
+ COGNITO_APP_CLIENT_ID: process.env['COGNITO_APP_CLIENT_ID'] || 'none'
+}
\ No newline at end of file
diff --git a/libs/app/environments/utils/src/index.ts b/libs/app/environments/utils/src/index.ts
new file mode 100644
index 00000000..fc56cdff
--- /dev/null
+++ b/libs/app/environments/utils/src/index.ts
@@ -0,0 +1 @@
+export * from './environment';
\ No newline at end of file
diff --git a/libs/app/environments/utils/src/test-setup.ts b/libs/app/environments/utils/src/test-setup.ts
new file mode 100644
index 00000000..1100b3e8
--- /dev/null
+++ b/libs/app/environments/utils/src/test-setup.ts
@@ -0,0 +1 @@
+import 'jest-preset-angular/setup-jest';
diff --git a/libs/app/environments/utils/tsconfig.json b/libs/app/environments/utils/tsconfig.json
new file mode 100644
index 00000000..b9e5be08
--- /dev/null
+++ b/libs/app/environments/utils/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ],
+ "extends": "../../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/libs/app/environments/utils/tsconfig.lib.json b/libs/app/environments/utils/tsconfig.lib.json
new file mode 100644
index 00000000..6eddf60e
--- /dev/null
+++ b/libs/app/environments/utils/tsconfig.lib.json
@@ -0,0 +1,17 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../../dist/out-tsc",
+ "declaration": true,
+ "declarationMap": true,
+ "inlineSources": true,
+ "types": ["node"]
+ },
+ "exclude": [
+ "src/**/*.spec.ts",
+ "src/test-setup.ts",
+ "jest.config.ts",
+ "src/**/*.test.ts"
+ ],
+ "include": ["src/**/*.ts"]
+}
diff --git a/libs/app/environments/utils/tsconfig.spec.json b/libs/app/environments/utils/tsconfig.spec.json
new file mode 100644
index 00000000..6e5925e5
--- /dev/null
+++ b/libs/app/environments/utils/tsconfig.spec.json
@@ -0,0 +1,16 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../../dist/out-tsc",
+ "module": "commonjs",
+ "target": "es2016",
+ "types": ["jest", "node"]
+ },
+ "files": ["src/test-setup.ts"],
+ "include": [
+ "jest.config.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/libs/app/error/data-access/.eslintrc.json b/libs/app/error/data-access/.eslintrc.json
new file mode 100644
index 00000000..6bac7be5
--- /dev/null
+++ b/libs/app/error/data-access/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "rules": {
+ "@angular-eslint/directive-selector": [
+ "error",
+ {
+ "type": "attribute",
+ "prefix": "fridgeToPlate",
+ "style": "camelCase"
+ }
+ ],
+ "@angular-eslint/component-selector": [
+ "error",
+ {
+ "type": "element",
+ "prefix": "fridge-to-plate",
+ "style": "kebab-case"
+ }
+ ]
+ },
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ]
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/libs/app/error/data-access/README.md b/libs/app/error/data-access/README.md
new file mode 100644
index 00000000..b91ffecb
--- /dev/null
+++ b/libs/app/error/data-access/README.md
@@ -0,0 +1,7 @@
+# app-error-data-access
+
+This library was generated with [Nx](https://nx.dev).
+
+## Running unit tests
+
+Run `nx test app-error-data-access` to execute the unit tests.
diff --git a/libs/app/error/data-access/jest.config.ts b/libs/app/error/data-access/jest.config.ts
new file mode 100644
index 00000000..197c792f
--- /dev/null
+++ b/libs/app/error/data-access/jest.config.ts
@@ -0,0 +1,22 @@
+/* eslint-disable */
+export default {
+ displayName: 'app-error-data-access',
+ preset: '../../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ coverageDirectory: '../../../../coverage/libs/app/error/data-access',
+ transform: {
+ '^.+\\.(ts|mjs|js|html)$': [
+ 'jest-preset-angular',
+ {
+ tsconfig: '/tsconfig.spec.json',
+ stringifyContentPathRegex: '\\.(html|svg)$',
+ },
+ ],
+ },
+ transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
+ snapshotSerializers: [
+ 'jest-preset-angular/build/serializers/no-ng-attributes',
+ 'jest-preset-angular/build/serializers/ng-snapshot',
+ 'jest-preset-angular/build/serializers/html-comment',
+ ],
+};
diff --git a/libs/app/error/data-access/project.json b/libs/app/error/data-access/project.json
new file mode 100644
index 00000000..aadc2ba2
--- /dev/null
+++ b/libs/app/error/data-access/project.json
@@ -0,0 +1,34 @@
+{
+ "name": "app-error-data-access",
+ "$schema": "../../../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "libs/app/error/data-access/src",
+ "prefix": "fridge-to-plate",
+ "tags": [],
+ "projectType": "library",
+ "targets": {
+ "test": {
+ "executor": "@nx/jest:jest",
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+ "options": {
+ "jestConfig": "libs/app/error/data-access/jest.config.ts",
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "codeCoverage": true
+ }
+ }
+ },
+ "lint": {
+ "executor": "@nx/linter:eslint",
+ "outputs": ["{options.outputFile}"],
+ "options": {
+ "lintFilePatterns": [
+ "libs/app/error/data-access/**/*.ts",
+ "libs/app/error/data-access/**/*.html"
+ ]
+ }
+ }
+ }
+}
diff --git a/libs/app/error/data-access/src/error.state.ts b/libs/app/error/data-access/src/error.state.ts
new file mode 100644
index 00000000..8818adc9
--- /dev/null
+++ b/libs/app/error/data-access/src/error.state.ts
@@ -0,0 +1,37 @@
+import { Action, State, StateContext } from "@ngxs/store";
+import { Injectable } from '@angular/core';
+import { ShowError } from "@fridge-to-plate/app/error/utils";
+import { ToastController } from "@ionic/angular";
+
+export interface ErrorStateModel {
+ error: string;
+}
+
+@State({
+ name: 'error',
+ defaults: {
+ error: ""
+ }
+})
+
+@Injectable()
+export class ErrorState {
+
+ constructor(private toastController: ToastController) {}
+
+ @Action(ShowError)
+ async showError({ patchState } : StateContext, { error }: ShowError) {
+ patchState({
+ error: error
+ });
+
+ const toast = await this.toastController.create({
+ message: "ERROR: " + error,
+ color: 'danger',
+ duration: 2500,
+ position: 'bottom',
+ });
+
+ await toast.present();
+ }
+}
\ No newline at end of file
diff --git a/libs/app/error/data-access/src/index.ts b/libs/app/error/data-access/src/index.ts
new file mode 100644
index 00000000..de339a1a
--- /dev/null
+++ b/libs/app/error/data-access/src/index.ts
@@ -0,0 +1 @@
+export * from './error.state';
diff --git a/libs/app/error/data-access/src/test-setup.ts b/libs/app/error/data-access/src/test-setup.ts
new file mode 100644
index 00000000..1100b3e8
--- /dev/null
+++ b/libs/app/error/data-access/src/test-setup.ts
@@ -0,0 +1 @@
+import 'jest-preset-angular/setup-jest';
diff --git a/libs/app/error/data-access/tsconfig.json b/libs/app/error/data-access/tsconfig.json
new file mode 100644
index 00000000..b9e5be08
--- /dev/null
+++ b/libs/app/error/data-access/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ],
+ "extends": "../../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/libs/app/error/data-access/tsconfig.lib.json b/libs/app/error/data-access/tsconfig.lib.json
new file mode 100644
index 00000000..91273870
--- /dev/null
+++ b/libs/app/error/data-access/tsconfig.lib.json
@@ -0,0 +1,17 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../../dist/out-tsc",
+ "declaration": true,
+ "declarationMap": true,
+ "inlineSources": true,
+ "types": []
+ },
+ "exclude": [
+ "src/**/*.spec.ts",
+ "src/test-setup.ts",
+ "jest.config.ts",
+ "src/**/*.test.ts"
+ ],
+ "include": ["src/**/*.ts"]
+}
diff --git a/libs/app/error/data-access/tsconfig.spec.json b/libs/app/error/data-access/tsconfig.spec.json
new file mode 100644
index 00000000..6e5925e5
--- /dev/null
+++ b/libs/app/error/data-access/tsconfig.spec.json
@@ -0,0 +1,16 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../../dist/out-tsc",
+ "module": "commonjs",
+ "target": "es2016",
+ "types": ["jest", "node"]
+ },
+ "files": ["src/test-setup.ts"],
+ "include": [
+ "jest.config.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/libs/app/error/utils/.eslintrc.json b/libs/app/error/utils/.eslintrc.json
new file mode 100644
index 00000000..6bac7be5
--- /dev/null
+++ b/libs/app/error/utils/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "rules": {
+ "@angular-eslint/directive-selector": [
+ "error",
+ {
+ "type": "attribute",
+ "prefix": "fridgeToPlate",
+ "style": "camelCase"
+ }
+ ],
+ "@angular-eslint/component-selector": [
+ "error",
+ {
+ "type": "element",
+ "prefix": "fridge-to-plate",
+ "style": "kebab-case"
+ }
+ ]
+ },
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ]
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/libs/app/error/utils/README.md b/libs/app/error/utils/README.md
new file mode 100644
index 00000000..98c97d1d
--- /dev/null
+++ b/libs/app/error/utils/README.md
@@ -0,0 +1,7 @@
+# app-error-utils
+
+This library was generated with [Nx](https://nx.dev).
+
+## Running unit tests
+
+Run `nx test app-error-utils` to execute the unit tests.
diff --git a/libs/app/error/utils/jest.config.ts b/libs/app/error/utils/jest.config.ts
new file mode 100644
index 00000000..6cefb690
--- /dev/null
+++ b/libs/app/error/utils/jest.config.ts
@@ -0,0 +1,22 @@
+/* eslint-disable */
+export default {
+ displayName: 'app-error-utils',
+ preset: '../../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ coverageDirectory: '../../../../coverage/libs/app/error/utils',
+ transform: {
+ '^.+\\.(ts|mjs|js|html)$': [
+ 'jest-preset-angular',
+ {
+ tsconfig: '/tsconfig.spec.json',
+ stringifyContentPathRegex: '\\.(html|svg)$',
+ },
+ ],
+ },
+ transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
+ snapshotSerializers: [
+ 'jest-preset-angular/build/serializers/no-ng-attributes',
+ 'jest-preset-angular/build/serializers/ng-snapshot',
+ 'jest-preset-angular/build/serializers/html-comment',
+ ],
+};
diff --git a/libs/app/error/utils/project.json b/libs/app/error/utils/project.json
new file mode 100644
index 00000000..40013f65
--- /dev/null
+++ b/libs/app/error/utils/project.json
@@ -0,0 +1,34 @@
+{
+ "name": "app-error-utils",
+ "$schema": "../../../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "libs/app/error/utils/src",
+ "prefix": "fridge-to-plate",
+ "tags": [],
+ "projectType": "library",
+ "targets": {
+ "test": {
+ "executor": "@nx/jest:jest",
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+ "options": {
+ "jestConfig": "libs/app/error/utils/jest.config.ts",
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "codeCoverage": true
+ }
+ }
+ },
+ "lint": {
+ "executor": "@nx/linter:eslint",
+ "outputs": ["{options.outputFile}"],
+ "options": {
+ "lintFilePatterns": [
+ "libs/app/error/utils/**/*.ts",
+ "libs/app/error/utils/**/*.html"
+ ]
+ }
+ }
+ }
+}
diff --git a/libs/app/error/utils/src/error.actions.ts b/libs/app/error/utils/src/error.actions.ts
new file mode 100644
index 00000000..5ed22156
--- /dev/null
+++ b/libs/app/error/utils/src/error.actions.ts
@@ -0,0 +1,5 @@
+export class ShowError {
+ static readonly type = '[Errors] ShowError';
+ constructor(public readonly error: string) {}
+ }
+
\ No newline at end of file
diff --git a/libs/app/auth/data-access/src/auth.module.ts b/libs/app/error/utils/src/error.module.ts
similarity index 78%
rename from libs/app/auth/data-access/src/auth.module.ts
rename to libs/app/error/utils/src/error.module.ts
index 8436acac..51501c0d 100644
--- a/libs/app/auth/data-access/src/auth.module.ts
+++ b/libs/app/error/utils/src/error.module.ts
@@ -4,4 +4,4 @@ import { CommonModule } from '@angular/common';
@NgModule({
imports: [CommonModule],
})
-export class AuthDataAccessModule {}
+export class ErrorUtilsModule {}
diff --git a/libs/app/error/utils/src/index.ts b/libs/app/error/utils/src/index.ts
new file mode 100644
index 00000000..6b00d89b
--- /dev/null
+++ b/libs/app/error/utils/src/index.ts
@@ -0,0 +1,2 @@
+export * from './error.module';
+export * from './error.actions';
\ No newline at end of file
diff --git a/libs/app/error/utils/src/test-setup.ts b/libs/app/error/utils/src/test-setup.ts
new file mode 100644
index 00000000..1100b3e8
--- /dev/null
+++ b/libs/app/error/utils/src/test-setup.ts
@@ -0,0 +1 @@
+import 'jest-preset-angular/setup-jest';
diff --git a/libs/app/error/utils/tsconfig.json b/libs/app/error/utils/tsconfig.json
new file mode 100644
index 00000000..b9e5be08
--- /dev/null
+++ b/libs/app/error/utils/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ],
+ "extends": "../../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/libs/app/error/utils/tsconfig.lib.json b/libs/app/error/utils/tsconfig.lib.json
new file mode 100644
index 00000000..91273870
--- /dev/null
+++ b/libs/app/error/utils/tsconfig.lib.json
@@ -0,0 +1,17 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../../dist/out-tsc",
+ "declaration": true,
+ "declarationMap": true,
+ "inlineSources": true,
+ "types": []
+ },
+ "exclude": [
+ "src/**/*.spec.ts",
+ "src/test-setup.ts",
+ "jest.config.ts",
+ "src/**/*.test.ts"
+ ],
+ "include": ["src/**/*.ts"]
+}
diff --git a/libs/app/error/utils/tsconfig.spec.json b/libs/app/error/utils/tsconfig.spec.json
new file mode 100644
index 00000000..6e5925e5
--- /dev/null
+++ b/libs/app/error/utils/tsconfig.spec.json
@@ -0,0 +1,16 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../../dist/out-tsc",
+ "module": "commonjs",
+ "target": "es2016",
+ "types": ["jest", "node"]
+ },
+ "files": ["src/test-setup.ts"],
+ "include": [
+ "jest.config.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/libs/app/explore/data-access/.eslintrc.json b/libs/app/explore/data-access/.eslintrc.json
new file mode 100644
index 00000000..6bac7be5
--- /dev/null
+++ b/libs/app/explore/data-access/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "rules": {
+ "@angular-eslint/directive-selector": [
+ "error",
+ {
+ "type": "attribute",
+ "prefix": "fridgeToPlate",
+ "style": "camelCase"
+ }
+ ],
+ "@angular-eslint/component-selector": [
+ "error",
+ {
+ "type": "element",
+ "prefix": "fridge-to-plate",
+ "style": "kebab-case"
+ }
+ ]
+ },
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ]
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/libs/app/explore/data-access/README.md b/libs/app/explore/data-access/README.md
new file mode 100644
index 00000000..9befe122
--- /dev/null
+++ b/libs/app/explore/data-access/README.md
@@ -0,0 +1,7 @@
+# app-explore-data-access
+
+This library was generated with [Nx](https://nx.dev).
+
+## Running unit tests
+
+Run `nx test app-explore-data-access` to execute the unit tests.
diff --git a/libs/app/explore/data-access/jest.config.ts b/libs/app/explore/data-access/jest.config.ts
new file mode 100644
index 00000000..e2e3081e
--- /dev/null
+++ b/libs/app/explore/data-access/jest.config.ts
@@ -0,0 +1,22 @@
+/* eslint-disable */
+export default {
+ displayName: 'app-explore-data-access',
+ preset: '../../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ coverageDirectory: '../../../../coverage/libs/app/explore/data-access',
+ transform: {
+ '^.+\\.(ts|mjs|js|html)$': [
+ 'jest-preset-angular',
+ {
+ tsconfig: '/tsconfig.spec.json',
+ stringifyContentPathRegex: '\\.(html|svg)$',
+ },
+ ],
+ },
+ transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
+ snapshotSerializers: [
+ 'jest-preset-angular/build/serializers/no-ng-attributes',
+ 'jest-preset-angular/build/serializers/ng-snapshot',
+ 'jest-preset-angular/build/serializers/html-comment',
+ ],
+};
diff --git a/libs/app/explore/data-access/project.json b/libs/app/explore/data-access/project.json
new file mode 100644
index 00000000..5400787a
--- /dev/null
+++ b/libs/app/explore/data-access/project.json
@@ -0,0 +1,34 @@
+{
+ "name": "app-explore-data-access",
+ "$schema": "../../../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "libs/app/explore/data-access/src",
+ "prefix": "fridge-to-plate",
+ "tags": [],
+ "projectType": "library",
+ "targets": {
+ "test": {
+ "executor": "@nx/jest:jest",
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+ "options": {
+ "jestConfig": "libs/app/explore/data-access/jest.config.ts",
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "codeCoverage": true
+ }
+ }
+ },
+ "lint": {
+ "executor": "@nx/linter:eslint",
+ "outputs": ["{options.outputFile}"],
+ "options": {
+ "lintFilePatterns": [
+ "libs/app/explore/data-access/**/*.ts",
+ "libs/app/explore/data-access/**/*.html"
+ ]
+ }
+ }
+ }
+}
diff --git a/libs/app/explore/data-access/src/explore.api.ts b/libs/app/explore/data-access/src/explore.api.ts
new file mode 100644
index 00000000..1ab94374
--- /dev/null
+++ b/libs/app/explore/data-access/src/explore.api.ts
@@ -0,0 +1,38 @@
+import { ingredientsArray } from './explore.mock';
+import { IRecipe } from '@fridge-to-plate/app/recipe/utils';
+import { IIngredient } from '@fridge-to-plate/app/ingredient/utils';
+import { HttpClient } from '@angular/common/http';
+import { BehaviorSubject, Observable, catchError, switchMap } from 'rxjs';
+import { Injectable } from '@angular/core';
+import { IProfile } from '@fridge-to-plate/app/profile/utils';
+import { Store } from '@ngxs/store';
+import { IExplore } from '@fridge-to-plate/app/explore/utils';
+import { environment } from '@fridge-to-plate/app/environments/utils';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class ExploreAPI {
+
+ constructor(private http: HttpClient, private store: Store) {}
+
+ private baseUrl = environment.API_URL + "/explore";
+
+ getRecipes(recipename: string) {
+ const url = `${this.baseUrl}/${recipename}`;
+
+ return this.http.get(url);
+ }
+
+ getProfile(username: string) {
+ const url = `${this.baseUrl}/profiles/${username}`;
+
+ return this.http.get(url);
+ }
+
+ searchCategory(search : IExplore): Observable {
+ const url = `${this.baseUrl}/search`;
+ return this.http.post