diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..557f156b4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +node_modules +.github +dist +coverage +Makefile +.gitignore +*keys.ts +*.keys.json diff --git a/.gitignore b/.gitignore index 93e5cb260..6cf093c1c 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,6 @@ Thumbs.db # stryker temp files .stryker-tmp + +#ENV VARIABLES +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..24f6ebb5b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +FROM node:14 AS development + +ENV USERNAME timetracker +ENV HOME /home/${USERNAME} + +RUN useradd -ms /bin/bash ${USERNAME} + +WORKDIR ${HOME}/time-tracker-ui +COPY . . +RUN rm -f .env +RUN chown ${USERNAME}:${USERNAME} -R ${HOME}/time-tracker-ui + +USER ${USERNAME} +RUN npm cache clean --force && npm install +EXPOSE 4200 +EXPOSE 9876 +CMD npm run config && ${HOME}/time-tracker-ui/node_modules/.bin/ng serve --host 0.0.0.0 --disableHostCheck + + + +FROM development as build +COPY .env . +RUN npm run config && npm run build + + + +FROM nginx:1.21 AS production + +ENV USERNAME app +RUN useradd -ms /bin/bash ${USERNAME} + +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /home/timetracker/time-tracker-ui/dist/time-tracker /usr/share/nginx/html + +RUN chown -R ${USERNAME}:${USERNAME} /var/cache/nginx && \ + chown -R ${USERNAME}:${USERNAME} /var/log/nginx && \ + chown -R ${USERNAME}:${USERNAME} /etc/nginx/conf.d +RUN touch /var/run/nginx.pid && chown -R ${USERNAME}:${USERNAME} /var/run/nginx.pid + +USER ${USERNAME} + +EXPOSE 4200 \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..a436cf461 --- /dev/null +++ b/Makefile @@ -0,0 +1,75 @@ +override SHELL := /bin/bash + +.PHONY: help +help: ## Show this help message. + @echo 'Usage:' + @echo ' make [target] ...' + @echo + @echo 'Targets:' + @grep --no-filename -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ + sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: build +build: ## Create docker image with dependencies needed for development. + docker-compose build + +.PHONY: cleanup +cleanup: ## Delete image timetracker_ui + docker rmi timetracker_ui + +.PHONY: run +run: ## Execute timetracker_ui docker containe. + docker-compose --env-file ./.env up -d + +.PHONY: logs +logs: ## Show logs of timetracker_ui. + docker logs -f timetracker_ui + +.PHONY: stop +stop: ## Stop container timetracker_ui. + docker-compose stop + +.PHONY: restart +restart: ## Restart container timetracker_ui. + docker-compose stop + docker-compose up -d + +.PHONY: remove +remove: ## Delete container timetracker_ui. + docker-compose down --volumes --remove-orphans --rmi local + +.PHONY: test +test: ## Run all tests on docker container timetracker_ui. + docker-compose --env-file ./.env up -d + docker exec -it timetracker_ui bash -c "npm run test" + +.PHONY: publish +publish: ## Publish the container image timetracker_ui. + docker tag timetracker_ui:latest $(registry_url)/timetracker_ui:latest + docker push $(registry_url)/timetracker_ui:latest + +.PHONY: build_prod +build_prod: ## Create docker image with dependencies needed for production. + docker build --target production -t timetracker_ui_prod -f Dockerfile . + +.PHONY: run_prod +run_prod: ## Execute timetracker_ui_prod docker container. + docker run -d -p 4200:4200 --name timetracker_ui_prod timetracker_ui_prod + +.PHONY: stop_prod +stop_prod: ## Stop container timetracker_ui_prod. + docker stop timetracker_ui_prod + +.PHONY: remove_prod +remove_prod: ## Delete container timetracker_ui_prod. + docker stop timetracker_ui_prod + docker rm timetracker_ui_prod + +.PHONY: publish_prod +publish_prod: ## Publish the container image timetracker_ui_prod. + docker tag timetracker_ui_prod:latest $(registry_url)/timetracker_ui_prod:latest + docker push $(registry_url)/timetracker_ui_prod:latest + +.PHONY: login +login: ## Login in respository of docker images. + az acr login --name $(container_registry) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..d7516c1a1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,31 @@ +version: '3.9' +services: + time-tracker-ui: + container_name: timetracker_ui + image: timetracker_ui + build: + target: development + context: . + dockerfile: ./Dockerfile + ports: + - 4200:4200 + - 9876:9876 + environment: + AUTHORITY: ${AUTHORITY} + CLIENT_ID: ${CLIENT_ID} + SCOPES: ${SCOPES} + STACK_EXCHANGE_ID: ${STACK_EXCHANGE_ID} + STACK_EXCHANGE_ACCESS_TOKEN: ${STACK_EXCHANGE_ACCESS_TOKEN} + AZURE_APP_CONFIGURATION_CONNECTION_STRING: ${AZURE_APP_CONFIGURATION_CONNECTION_STRING} + AUTHORITY_JSON: ${AUTHORITY_JSON} + CLIENT_ID_JSON: ${CLIENT_ID_JSON} + SCOPES_JSON: ${SCOPES_JSON} + volumes: + - ./src:/home/timetracker/time-tracker-ui/src/ + - ./scripts:/home/timetracker/time-tracker-ui/scripts/ + - ./e2e:/home/timetracker/time-tracker-ui/e2e/ + - ./coverage:/home/timetracker/time-tracker-ui/coverage + - ./angular.json:/home/timetracker/time-tracker-ui/angular.json + - ./karma.conf.js:/home/timetracker/time-tracker-ui/karma.conf.js + - ./package.json:/home/timetracker/time-tracker-ui/package.json + - ./webpack.config.js:/home/timetracker/time-tracker-ui/webpack.config.js diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 000000000..824374fdc --- /dev/null +++ b/nginx.conf @@ -0,0 +1,12 @@ +server { + listen 4200; + + root /usr/share/nginx/html; + index index.html; + + server_name _; + + location / { + try_files $uri /index.html; + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3ebe355f9..ac7aee405 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "time-tracker", - "version": "1.60.5", + "version": "1.65.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -6472,15 +6472,6 @@ "p-try": "^2.0.0" } }, - "serialize-javascript": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", - "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, "ssri": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", @@ -7877,6 +7868,11 @@ "is-obj": "^2.0.0" } }, + "dotenv": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-14.2.0.tgz", + "integrity": "sha512-05POuPJyPpO6jqzTNweQFfAyMSD4qa4lvsMOWyTRTdpHKy6nnnN+IYWaXF+lHivhBH/ufDKlR4IWCAN3oPnHuw==" + }, "duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", @@ -11870,9 +11866,9 @@ "optional": true }, "nanoid": { - "version": "3.1.25", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", - "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", + "integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==", "dev": true }, "nanomatch": { @@ -21621,6 +21617,15 @@ } } }, + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, "serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", @@ -23454,9 +23459,9 @@ } }, "serialize-javascript": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", - "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", "dev": true, "requires": { "randombytes": "^2.1.0" @@ -23638,9 +23643,9 @@ "dev": true }, "trim-off-newlines": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", - "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.3.tgz", + "integrity": "sha512-kh6Tu6GbeSNMGfrrZh6Bb/4ZEHV1QlB4xNDBeog8Y9/QwFlKTRyWvY3Fs9tRDAMZliVUwieMgEdIeL/FtqjkJg==", "dev": true }, "ts-node": { @@ -24690,9 +24695,9 @@ }, "dependencies": { "serialize-javascript": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", - "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", "dev": true, "requires": { "randombytes": "^2.1.0" diff --git a/package.json b/package.json index 0ed0cc466..8cd1f98f0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "name": "time-tracker", - "version": "1.60.5", + "version": "1.65.0", "scripts": { + "config": "ts-node ./scripts/setenv.ts", "preinstall": "npx npm-force-resolutions", "ng": "ng", "start": "ng serve", @@ -44,6 +45,7 @@ "datatables.net-responsive": "2.2.6", "datatables.net-responsive-dt": "2.2.6", "date-fns": "2.22.1", + "dotenv": "^14.2.0", "jquery": "3.5.1", "jszip": "3.7.0", "minimist": "1.2.5", diff --git a/scripts/setenv.ts b/scripts/setenv.ts new file mode 100644 index 000000000..0c07a1b30 --- /dev/null +++ b/scripts/setenv.ts @@ -0,0 +1,34 @@ +const { writeFile } = require('fs'); +require('dotenv').config(); + +const pathJs = `./src/environments/keys.ts` +const contentKeys = +`export const AUTHORITY = '${process.env.AUTHORITY}'; +export const CLIENT_ID = '${process.env.CLIENT_ID}'; +export const SCOPES = ['${process.env.SCOPES}']; +export const STACK_EXCHANGE_ID = '${process.env.STACK_EXCHANGE_ID}'; +export const STACK_EXCHANGE_ACCESS_TOKEN = '${process.env.STACK_EXCHANGE_ACCESS_TOKEN}'; +export const AZURE_APP_CONFIGURATION_CONNECTION_STRING = '${process.env.AZURE_APP_CONFIGURATION_CONNECTION_STRING}'; +`; + +writeFile(pathJs, contentKeys, function (err) { + if (err) { + console.log(err); + } + console.log(`Wrote variables to ${pathJs}`); +}); + +const pathJson = `./src/environments/.keys.json` +const contentKeysJson = +`{ + "authority": "${process.env.AUTHORITY_JSON}", + "client_id": "${process.env.CLIENT_ID_JSON}", + "scopes": ["${process.env.SCOPES_JSON}"] +}`; + +writeFile(pathJson, contentKeysJson, function (err) { + if (err) { + console.log(err); + } + console.log(`Wrote variables to ${pathJson}`); +}); diff --git a/src/app/modules/activities-management/store/activity-management.actions.ts b/src/app/modules/activities-management/store/activity-management.actions.ts index 95094b412..bf69a3c31 100644 --- a/src/app/modules/activities-management/store/activity-management.actions.ts +++ b/src/app/modules/activities-management/store/activity-management.actions.ts @@ -20,6 +20,7 @@ export enum ActivityManagementActionTypes { UNARCHIVE_ACTIVITY_FAIL = '[ActivityManagement] UNARCHIVE_ACTIVITY_FAIL', SET_ACTIVITY_ID_TO_EDIT = '[ActivityManagement] SET_ACTIVITY_ID_TO_EDIT', RESET_ACTIVITY_ID_TO_EDIT = '[ActivityManagement] RESET_ACTIVITY_ID_TO_EDIT', + DEFAULT_ACTIVITY = '[ActivityManagement] DEFAULT_ACTIVITY', } export class LoadActivities implements Action { @@ -118,6 +119,10 @@ export class ResetActivityToEdit implements Action { public readonly type = ActivityManagementActionTypes.RESET_ACTIVITY_ID_TO_EDIT; } +export class DefaultActivities implements Action { + public readonly type = ActivityManagementActionTypes.DEFAULT_ACTIVITY; +} + export type ActivityManagementActions = | LoadActivities | LoadActivitiesSuccess @@ -135,4 +140,5 @@ export type ActivityManagementActions = | UnarchiveActivitySuccess | UnarchiveActivityFail | SetActivityToEdit - | ResetActivityToEdit; + | ResetActivityToEdit + | DefaultActivities; diff --git a/src/app/modules/activities-management/store/activity-management.reducers.spec.ts b/src/app/modules/activities-management/store/activity-management.reducers.spec.ts index 7da4d4419..560e063a4 100644 --- a/src/app/modules/activities-management/store/activity-management.reducers.spec.ts +++ b/src/app/modules/activities-management/store/activity-management.reducers.spec.ts @@ -142,4 +142,25 @@ describe('activityManagementReducer', () => { expect(state.message).toEqual('Something went wrong unarchiving activities!'); expect(state.isLoading).toBeFalse(); }); + + it('on SetActivityidToEdit, message equal to \"Set activityIdToEdit property\"', () => { + const action = new actions.SetActivityToEdit('1'); + const state = activityManagementReducer(initialState, action); + expect(state.message).toEqual('Set activityIdToEdit property'); + expect(state.isLoading).toBeFalse(); + }); + + it('on ResetActivityIdToEdit, message equal to \"Reset activityIdToEdit property\"', () => { + const action = new actions.ResetActivityToEdit(); + const state = activityManagementReducer(initialState, action); + expect(state.message).toEqual('Reset activityIdToEdit property'); + expect(state.isLoading).toBeFalse(); + }); + + it('on DefaultAction, state equal to initial state', () => { + const action = new actions.DefaultActivities(); + const state = activityManagementReducer(initialState, action); + expect(state.data).toEqual(initialState.data); + }); + }); diff --git a/src/app/modules/customer-management/components/management-customer-projects/management-customer-projects.component.spec.ts b/src/app/modules/customer-management/components/management-customer-projects/management-customer-projects.component.spec.ts index 90618e4ee..3668b3a1d 100644 --- a/src/app/modules/customer-management/components/management-customer-projects/management-customer-projects.component.spec.ts +++ b/src/app/modules/customer-management/components/management-customer-projects/management-customer-projects.component.spec.ts @@ -3,6 +3,7 @@ import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { ManagementCustomerProjectsComponent } from './management-customer-projects.component'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { CustomerState } from '../../store'; +import { of } from 'rxjs'; describe('ManagmentCustomerProjectsComponent', () => { let component: ManagementCustomerProjectsComponent; @@ -38,6 +39,18 @@ describe('ManagmentCustomerProjectsComponent', () => { expect(component).toBeTruthy(); }); + it('set customerName on ngOnInit', () => { + spyOn(store, 'dispatch'); + spyOn(store, 'pipe').and.returnValue(of({ + id: 1, + name: 'project 4', + description: 'project 1 to test methos projectss', + status: 'inactive' + })); + component.ngOnInit(); + expect(component.customerName).toEqual('project 4'); + }); + it('should be enable tabs', () => { component.areTabsActive = false; component.activeTabs(true); diff --git a/src/app/modules/customer-management/components/projects-type/components/project-type-list/project-type-list.component.spec.ts b/src/app/modules/customer-management/components/projects-type/components/project-type-list/project-type-list.component.spec.ts index e6b23b93f..beaac1a18 100644 --- a/src/app/modules/customer-management/components/projects-type/components/project-type-list/project-type-list.component.spec.ts +++ b/src/app/modules/customer-management/components/projects-type/components/project-type-list/project-type-list.component.spec.ts @@ -5,6 +5,7 @@ import { NgxPaginationModule } from 'ngx-pagination'; import { DeleteProjectType, SetProjectTypeToEdit } from './../../store/project-type.actions'; import { allProjectTypes, ProjectTypeState } from '../../store'; import { ProjectTypeListComponent } from './project-type-list.component'; +import { ProjectType } from 'src/app/modules/shared/models'; describe('ProjectTypeTableListComponent', () => { let component: ProjectTypeListComponent; @@ -68,4 +69,16 @@ describe('ProjectTypeTableListComponent', () => { expect(store.dispatch).toHaveBeenCalledWith(new SetProjectTypeToEdit('id')); }); + it('open the modal with the correct parameters', () => { + const item: ProjectType = { + id: `1`, + name: `Project 1`, + description: `Description project 1` + }; + component.openModal(item); + expect(component.idToDelete).toEqual(item.id); + expect(component.message).toEqual(`Are you sure you want to delete ${item.name}?`); + expect(component.showModal).toBe(true); + }); + }); diff --git a/src/app/modules/customer-management/components/projects-type/store/project-type.reducers.spec.ts b/src/app/modules/customer-management/components/projects-type/store/project-type.reducers.spec.ts index e87804249..b46d0fe2c 100644 --- a/src/app/modules/customer-management/components/projects-type/store/project-type.reducers.spec.ts +++ b/src/app/modules/customer-management/components/projects-type/store/project-type.reducers.spec.ts @@ -6,7 +6,7 @@ describe('projectTypeReducer', () => { const initialState: ProjectTypeState = { data: [], isLoading: false, message: '', projectTypeIdToEdit: '' }; const projectType: ProjectType = { id: '1', name: 'Training', description: 'It is good for learning' }; - it('on Default, ', () => { + it('on DefaultAction, state equal to initial state', () => { const action = new actions.DefaultProjectTypes(); const state = projectTypeReducer(initialState, action); expect(state.data).toEqual(initialState.data); diff --git a/src/app/modules/customer-management/components/projects/components/services/project.service.spec.ts b/src/app/modules/customer-management/components/projects/components/services/project.service.spec.ts index 496867eef..f1fd70831 100644 --- a/src/app/modules/customer-management/components/projects/components/services/project.service.spec.ts +++ b/src/app/modules/customer-management/components/projects/components/services/project.service.spec.ts @@ -48,6 +48,16 @@ describe('ProjectService', () => { } )); + it('all projects are read using GET from url', () => { + const projectsFoundSize = projectsList.length; + service.getAllProjects().subscribe((projectsInResponse) => { + expect(projectsInResponse.length).toBe(projectsFoundSize); + }); + const getProjectsRequest = httpMock.expectOne(service.url); + expect(getProjectsRequest.request.method).toBe('GET'); + getProjectsRequest.flush(projectsList); + }); + it('projects are read using GET from url', () => { const projectsFoundSize = projectsList.length; service.url = '/projects'; diff --git a/src/app/modules/customer-management/components/projects/components/store/project.actions.ts b/src/app/modules/customer-management/components/projects/components/store/project.actions.ts index d0c26cc24..ef75d7efe 100644 --- a/src/app/modules/customer-management/components/projects/components/store/project.actions.ts +++ b/src/app/modules/customer-management/components/projects/components/store/project.actions.ts @@ -25,6 +25,7 @@ export enum ProjectActionTypes { UNARCHIVE_PROJECT = '[Projects] UNARCHIVE_PROJECT', UNARCHIVE_PROJECT_SUCCESS = '[Projects] UNARCHIVE_PROJECT_SUCCESS', UNARCHIVE_PROJECT_FAIL = '[Projects] UNARCHIVE_PROJECT_FAIL', + DEFAULT_PROJECT = '[Projects] DEFAULT_PROJECTS', } export class CleanCustomerProjects implements Action { @@ -154,6 +155,10 @@ export class UnarchiveProjectFail implements Action { constructor(public error: string) {} } +export class DefaultProjects implements Action { + public readonly type = ProjectActionTypes.DEFAULT_PROJECT; +} + export type ProjectActions = | CleanCustomerProjects | LoadProjects @@ -177,4 +182,5 @@ export type ProjectActions = | DeleteProjectFail | UnarchiveProject | UnarchiveProjectSuccess - | UnarchiveProjectFail; + | UnarchiveProjectFail + | DefaultProjects; diff --git a/src/app/modules/customer-management/components/projects/components/store/project.reducer.spec.ts b/src/app/modules/customer-management/components/projects/components/store/project.reducer.spec.ts index cf387017a..4e3cda813 100644 --- a/src/app/modules/customer-management/components/projects/components/store/project.reducer.spec.ts +++ b/src/app/modules/customer-management/components/projects/components/store/project.reducer.spec.ts @@ -22,6 +22,26 @@ describe('projectReducer', () => { }); it('on LoadProjects, isLoading is true', () => { + const action = new actions.LoadProjects(); + const state = projectReducer(initialState, action); + expect(state.isLoading).toEqual(true); + }); + + it('on LoadProjectsSucces, projectsFound are saved in the store', () => { + const projectsFound: Project[] = [project]; + const action = new actions.LoadProjectsSuccess(projectsFound); + const state = projectReducer(initialState, action); + expect(action.payload).toEqual([project]); + expect(state.isLoading).toEqual(false); + }); + + it('on LoadProjectsFail, loadProject equal []', () => { + const action = new actions.LoadProjectsFail('error'); + const state = projectReducer(initialState, action); + expect(state.isLoading).toEqual(false); + }); + + it('on LoadCustomerProjects, isLoading is true', () => { const action = new actions.LoadCustomerProjects('1'); const state = projectReducer(initialState, action); expect(state.isLoading).toEqual(true); @@ -201,4 +221,11 @@ describe('projectReducer', () => { expect(state.message).toEqual('Something went wrong unarchiving projects!'); expect(state.isLoading).toEqual(false); }); + + it('on DefaultAction, state equal to initial state', () => { + const action = new actions.DefaultProjects(); + const state = projectReducer(initialState, action); + expect(state).toEqual(initialState); + }); + }); diff --git a/src/app/modules/customer-management/store/customer-management.actions.ts b/src/app/modules/customer-management/store/customer-management.actions.ts index 2dc9ff43d..dc7a6c100 100644 --- a/src/app/modules/customer-management/store/customer-management.actions.ts +++ b/src/app/modules/customer-management/store/customer-management.actions.ts @@ -19,6 +19,7 @@ export enum CustomerManagementActionTypes { UNARCHIVE_CUSTOMER = '[CustomerManagement] UNARCHIVE_CUSTOMER', UNARCHIVE_CUSTOMER_SUCCESS = '[CustomerManagement] UNARCHIVE_CUSTOMER_SUCCESS', UNARCHIVE_CUSTOMER_FAIL = '[CustomerManagement] UNARCHIVE_CUSTOMER_FAIL', + DEFAULT_CUSTOMER = '[CustomerManagement] DEFAULT_CUSTOMER', } export class LoadCustomers implements Action { @@ -118,6 +119,10 @@ export class UnarchiveCustomerFail implements Action { constructor(public error: string) {} } +export class DefaultCustomer implements Action { + public readonly type = CustomerManagementActionTypes.DEFAULT_CUSTOMER; +} + export type CustomerManagementActions = | CreateCustomer | CreateCustomerSuccess @@ -135,4 +140,5 @@ export type CustomerManagementActions = | ResetCustomerToEdit | UnarchiveCustomer | UnarchiveCustomerSuccess - | UnarchiveCustomerFail; + | UnarchiveCustomerFail + | DefaultCustomer; diff --git a/src/app/modules/customer-management/store/customer-management.reducers.spec.ts b/src/app/modules/customer-management/store/customer-management.reducers.spec.ts index e662e6277..7dd8911ff 100644 --- a/src/app/modules/customer-management/store/customer-management.reducers.spec.ts +++ b/src/app/modules/customer-management/store/customer-management.reducers.spec.ts @@ -166,4 +166,10 @@ describe('customerManagementReducer', () => { expect(state.message).toEqual('Something went wrong unarchiving customer!'); expect(state.isLoading).toEqual(false); }); + + it('on DefaultAction, state equal to initial state', () => { + const action = new actions.DefaultCustomer(); + const state = customerManagementReducer(initialState, action); + expect(state.data).toEqual(initialState.data); + }); }); diff --git a/src/app/modules/login/services/azure.ad.b2c.service.spec.ts b/src/app/modules/login/services/azure.ad.b2c.service.spec.ts index 8020c05f9..baf6a364c 100644 --- a/src/app/modules/login/services/azure.ad.b2c.service.spec.ts +++ b/src/app/modules/login/services/azure.ad.b2c.service.spec.ts @@ -21,6 +21,7 @@ describe('AzureAdB2CService', () => { name: 'abc', idToken: { iss: ' http://hostname.com/12345/v0/', + emails: 'abcd' }, idTokenClaims: {}, sid: 'abc', @@ -179,4 +180,10 @@ describe('AzureAdB2CService', () => { expect(UserAgentApplication.prototype.getAccount).toHaveBeenCalled(); }); + + it('should get email', () => { + spyOn(UserAgentApplication.prototype, 'getAccount').and.returnValues(account); + const email = service.getUserEmail(); + expect(email).toEqual('a'); + }); }); diff --git a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.spec.ts b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.spec.ts index 9712a7544..4f7c460f1 100644 --- a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.spec.ts +++ b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.spec.ts @@ -1,5 +1,6 @@ import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { DataTableDirective } from 'angular-datatables'; import { Entry } from 'src/app/modules/shared/models'; import { SubstractDatePipe } from 'src/app/modules/shared/pipes/substract-date/substract-date.pipe'; import { getReportDataSource } from 'src/app/modules/time-clock/store/entry.selectors'; @@ -134,6 +135,17 @@ describe('Reports Page', () => { expect(component.bodyExportOptions(entry, row, column, node)).toBe('https://TT-392-uri'); }); + it('when the rerenderDataTable method is called and dtElement and dtInstance are defined, the destroy and next methods are called ', + () => { + component.dtElement = { + dtInstance: { + then : (dtInstance: DataTables.Api) => { dtInstance.destroy(); } + } + } as unknown as DataTableDirective; + spyOn(component.dtElement.dtInstance, 'then'); + component.ngAfterViewInit(); + expect(component.dtElement.dtInstance.then).toHaveBeenCalled(); + }); afterEach(() => { fixture.destroy(); diff --git a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts index 5658a972e..9e6dcb415 100644 --- a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts +++ b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts @@ -50,7 +50,8 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn text: 'CSV', filename: `time-entries-${formatDate(new Date(), 'MM_dd_yyyy-HH_mm', 'en')}` }, - ] + ], + columnDefs: [{ type: 'date', targets: 2 }] }; dtTrigger: Subject = new Subject(); @ViewChild(DataTableDirective, { static: false }) diff --git a/src/app/modules/shared/components/details-fields/details-fields.component.html b/src/app/modules/shared/components/details-fields/details-fields.component.html index 3b737aab9..f89af7b41 100644 --- a/src/app/modules/shared/components/details-fields/details-fields.component.html +++ b/src/app/modules/shared/components/details-fields/details-fields.component.html @@ -72,7 +72,7 @@
- +
- -
- - + +
+ + {{ this.getTimeDifference() }} +
@@ -143,15 +128,6 @@
-
- -
- - {{ this.getTimeDifference() }} - -
-
- { technology: '', }; component.ngOnChanges(); - expect(component.shouldRestartEntry).toBeFalse(); expect(component.entryForm.value).toEqual(formValue); component.activities$.subscribe((item) => { @@ -518,32 +517,6 @@ describe('DetailsFieldsComponent', () => { expect(component.projectSelected.emit).toHaveBeenCalledWith(data); }); - it('on selected start_date should change end_date', () => { - const expectedStartDate = '2020-02-05'; - - component.onStartDateChange(expectedStartDate); - fixture.detectChanges(); - const endDateInput: HTMLInputElement = fixture.debugElement.nativeElement.querySelector('#end_date'); - - expect(endDateInput.value).toEqual(expectedStartDate); - }); - - it('on selected end_date should not change start_date', () => { - const expectedStartDate = '2020-02-05'; - const expectedEndDate = '2020-02-06'; - - component.ngOnInit(); - fixture.detectChanges(); - const startDateInput: HTMLInputElement = fixture.debugElement.nativeElement.querySelector('#start_date'); - const endDateInput: HTMLInputElement = fixture.debugElement.nativeElement.querySelector('#end_date'); - startDateInput.value = expectedStartDate; - endDateInput.value = expectedEndDate; - endDateInput.dispatchEvent(new Event('#end_date')); - - expect(endDateInput.value).not.toEqual(startDateInput.value); - expect(startDateInput.value).toEqual(expectedStartDate); - }); - it('on get current date should return expected date', () => { const expectedDate = moment(new Date()).format(DATE_FORMAT_YEAR); @@ -561,15 +534,6 @@ describe('DetailsFieldsComponent', () => { expect(startDateInput.max).toEqual(expectedDate); }); - it('on the input with id #end_date we could get the current Date ', () => { - fixture.detectChanges(); - const expectedDate = moment(new Date()).format(DATE_FORMAT_YEAR); - const endDateInput = fixture.debugElement.nativeElement.querySelector('[id=end_date]'); - - expect(endDateInput.id).toEqual('end_date'); - expect(endDateInput.max).toEqual(expectedDate); - }); - const diffParams = [ { case: 'positive should return correctly diff', @@ -730,6 +694,27 @@ describe('DetailsFieldsComponent', () => { expect(numberinISOFormat).toBe(expectedISOFormatNumbers[numberIndex]); }); }); + + it('when the user selects technologies, set them in the variable selectedTechnologies', () => { + const techs = ['php', 'angular']; + component.onTechnologiesUpdated(techs); + expect(component.selectedTechnologies).toEqual(techs); + }); + + it('when the user does not select a project, display a warning message.', () => { + spyOn(toastrServiceStub, 'warning'); + component.onclickFormAction(true); + expect(toastrServiceStub.warning).toHaveBeenCalled(); + }); + + it('if entry is set to project_name search_fiend is assigned in entryForm', () => { + const listProjects: Project[] = [{ id: 'id', name: 'abc', status: 'active', search_field: 'name'}]; + component.listProjects = listProjects; + component.entryToEdit = { ...entryToEdit }; + component.ngOnChanges(); + expect(component.entryForm.value.project_name).toBe('name'); + }); + /* TODO As part of https://github.com/ioet/time-tracker-ui/issues/424 a new parameter was added to the details-field-component, and now these couple of tests are failing. A solution to this error might be generate a Test Wrapper Component. More details here: diff --git a/src/app/modules/shared/components/input-date/input-date.component.spec.ts b/src/app/modules/shared/components/input-date/input-date.component.spec.ts index bf8e50de0..f2ccb6753 100644 --- a/src/app/modules/shared/components/input-date/input-date.component.spec.ts +++ b/src/app/modules/shared/components/input-date/input-date.component.spec.ts @@ -37,6 +37,42 @@ describe('InputDateComponent', () => { expect(component.value).toEqual('2020-05-20'); })); + it('it calls the close method if opened equals true', () => { + const datepicker: any = { opened : true, open : () => ({}), close : () => ({}) }; + spyOn(datepicker, 'close'); + component.openOrCloseDatePicker(datepicker); + expect(datepicker.close).toHaveBeenCalled(); + }); + + it('it calls the open method if opened equals false', () => { + const datepicker: any = { opened : false, open : () => ({}), close : () => ({}) }; + spyOn(datepicker, 'open'); + component.openOrCloseDatePicker(datepicker); + expect(datepicker.open).toHaveBeenCalled(); + }); + + it('isDisabled should be true if parameter is true', () => { + component.setDisabledState(true); + expect(component.isDisabled).toBe(true); + }); + + it('isDisabled should be false if parameter is false', () => { + component.setDisabledState(false); + expect(component.isDisabled).toBe(false); + }); + + it(`value should be '' in writeValue function when parameter is null`, () => { + const value: any = null; + component.writeValue(value); + expect(component.value).toEqual(''); + }); + + it(`value should be '' in writeValue function when parameter is not defined`, () => { + const value: any = undefined; + component.writeValue(value); + expect(component.value).toEqual(''); + }); + const params: boolean[] = [true, false]; params.forEach(disable => { it('when the disabled attribute is provided, it should disable the input ', fakeAsync(() => { diff --git a/src/app/modules/shared/components/sidebar/sidebar.component.html b/src/app/modules/shared/components/sidebar/sidebar.component.html index 164bfc1ec..4afd966c6 100644 --- a/src/app/modules/shared/components/sidebar/sidebar.component.html +++ b/src/app/modules/shared/components/sidebar/sidebar.component.html @@ -2,7 +2,7 @@ - +
- @@ -62,9 +62,6 @@ - - - diff --git a/src/app/modules/time-entries/pages/time-entries.component.spec.ts b/src/app/modules/time-entries/pages/time-entries.component.spec.ts index 9b263a500..7c1afcde4 100644 --- a/src/app/modules/time-entries/pages/time-entries.component.spec.ts +++ b/src/app/modules/time-entries/pages/time-entries.component.spec.ts @@ -712,4 +712,50 @@ describe('TimeEntriesComponent', () => { expect(HTMLTimeEntriesView).not.toBeNull(); }); + + it('open the modal with the correct parameters', () => { + const item: any = { + id: `1`, + activity_name: `Activity 1`, + }; + component.openModal(item); + expect(component.idToDelete).toEqual(item.id); + expect(component.message).toEqual(`Are you sure you want to delete ${item.activity_name}?`); + expect(component.showModal).toBe(true); + }); + + it('if there are no entries, the isTheEntryToEditTheLastOne function should return false', () => { + const entries: Entry[] = []; + expect(component.isTheEntryToEditTheLastOne(entries)).toBe(false); + }); + + it('should create new Entry even though the activeEntry overlaps', () => { + component.entryId = 'entry_2'; + const activeEntry = { + id: 'entry_1', + project_id: 'abc', + project_name: 'Time-tracker', + start_date: new Date('2020-02-05T15:36:15.887Z'), + end_date: new Date('2020-02-05T18:36:15.887Z'), + customer_name: 'ioet Inc.', + activity_id: 'development', + technologies: ['Angular', 'TypeScript'], + description: 'No comments', + uri: 'EY-25', + }; + const entryToSave = { + entry: { + project_id: 'project-id', + end_date: '2010-05-05T10:04', + start_date: null, + timezone_offset: 300, + }, + shouldRestartEntry: true + }; + spyOn(component, 'doSave'); + component.activeTimeEntry = activeEntry; + component.saveEntry(entryToSave); + expect(component.doSave).toHaveBeenCalledWith(entryToSave); + }); + }); diff --git a/src/app/modules/time-entries/pages/time-entries.component.ts b/src/app/modules/time-entries/pages/time-entries.component.ts index 49e2648ae..0ecdbc921 100644 --- a/src/app/modules/time-entries/pages/time-entries.component.ts +++ b/src/app/modules/time-entries/pages/time-entries.component.ts @@ -50,7 +50,6 @@ export class TimeEntriesComponent implements OnInit, OnDestroy, AfterViewInit { isActiveEntryOverlapping = false; calendarView: CalendarView = CalendarView.Month; actualDate: Date; - readonly NO_DATA_MESSAGE: string = 'No data available in table'; constructor( private store: Store, private toastrService: ToastrService, @@ -122,7 +121,7 @@ export class TimeEntriesComponent implements OnInit, OnDestroy, AfterViewInit { return false; } } - private isTheEntryToEditTheLastOne(entries: Entry[]) { + public isTheEntryToEditTheLastOne(entries: Entry[]) { if (entries && entries.length > 0) { const lastEntry = entries[0]; return lastEntry.id === this.entryId; diff --git a/src/app/modules/user/store/user.actions.ts b/src/app/modules/user/store/user.actions.ts index b5b5a664b..1543327e7 100644 --- a/src/app/modules/user/store/user.actions.ts +++ b/src/app/modules/user/store/user.actions.ts @@ -5,6 +5,7 @@ export enum UserActionTypes { LOAD_USER = '[User] LOAD_USER', LOAD_USER_SUCCESS = '[User] LOAD_USER_SUCCESS', LOAD_USER_FAIL = '[User] LOAD_USER_FAIL', + DEFAULT_USER = '[User] DEFAULT_USER', } export class LoadUser implements Action { @@ -24,4 +25,8 @@ export class LoadUserFail implements Action { constructor(public error: string) {} } -export type UserActions = LoadUser | LoadUserSuccess | LoadUserFail; +export class DefaultUser implements Action { + public readonly type = UserActionTypes.DEFAULT_USER; +} + +export type UserActions = LoadUser | LoadUserSuccess | LoadUserFail | DefaultUser; diff --git a/src/app/modules/user/store/user.reducer.spec.ts b/src/app/modules/user/store/user.reducer.spec.ts index a73313362..9fadbf2d3 100644 --- a/src/app/modules/user/store/user.reducer.spec.ts +++ b/src/app/modules/user/store/user.reducer.spec.ts @@ -1,5 +1,5 @@ import { userReducer } from './user.reducer'; -import { LoadUser, LoadUserFail, LoadUserSuccess } from './user.actions'; +import { LoadUser, LoadUserFail, LoadUserSuccess, DefaultUser } from './user.actions'; import { User } from '../models/user'; describe('userReducer', () => { @@ -41,4 +41,11 @@ describe('userReducer', () => { expect(state).toEqual(initialState); }); + + it('on DefaultAction, state equal to initial state', () => { + const action = new DefaultUser(); + const state = userReducer(initialState, action); + expect(state).toEqual(initialState); + }); + }); diff --git a/src/app/modules/users/store/user.reducers.spec.ts b/src/app/modules/users/store/user.reducers.spec.ts index 8cc8ffac8..8096f1390 100644 --- a/src/app/modules/users/store/user.reducers.spec.ts +++ b/src/app/modules/users/store/user.reducers.spec.ts @@ -187,7 +187,7 @@ describe('userReducer', () => { expect(state.message).toBe('Something went wrong revoking access role to the user'); }); - it('on Default, ', () => { + it('on DefaultAction, state equal to initial state', () => { const action = new actions.DefaultUser(); const state = userReducer(initialState, action); expect(state).toEqual(initialState); diff --git a/src/assets/img/time-tracker-logo.png b/src/assets/img/time-tracker-logo.png new file mode 100644 index 000000000..8a8e9499d Binary files /dev/null and b/src/assets/img/time-tracker-logo.png differ diff --git a/src/favicon.ico b/src/favicon.ico index ee11b6c6d..ec84d0bfd 100644 Binary files a/src/favicon.ico and b/src/favicon.ico differ
{{NO_DATA_MESSAGE}}
{{ entry.start_date | date: 'MM/dd/yyyy' }} {{ entry.start_date | date: 'HH:mm' }} - {{ entry.end_date | date: 'HH:mm' }}