From c8d2c7513fecd3effbc40ae7f9be9ddf90599f70 Mon Sep 17 00:00:00 2001 From: yannick-aneo <136307285+yannick-aneo@users.noreply.github.com> Date: Thu, 12 Oct 2023 17:28:54 +0200 Subject: [PATCH 01/37] chore/ unit test formation with Jest --- unit_test_formation.md | 63 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 unit_test_formation.md diff --git a/unit_test_formation.md b/unit_test_formation.md new file mode 100644 index 000000000..a286a1d83 --- /dev/null +++ b/unit_test_formation.md @@ -0,0 +1,63 @@ +## Unit testing in ArmoniK + + +### Brief introduction on testing in frontend + Testing is a crucial part of software developement. + + It brings to developers more confidence on what they're working on and more quality on software. + + We have traditionnaly 3 types of tests. + + Tests + 1. Unit tests + 2. Integration tests + 3. End-to-End tests + +![628b0dca3e6eda9219d40a6a_The-Testing-Pyramid-Simplified-for-One-and-All-1280X720 (1)](https://github.com/aneoconsulting/ArmoniK.Admin.GUI/assets/136307285/a2f411d5-db43-4d1a-9855-7dadc1816a5f) + + + + + We'll be focused on unit testing. + +#### What are Unit tests ? + + Unit testing is focused on writing tests for small building blocks of an application. + + It's about test every expected and unexpected behavior of units of our app. We usually write unit tests for classes, functions and small components. + + We tests our classes and functions by checking their inputs and outputs in order to work more predictable and consistent code. + + In Javascript eco-system, you can use librairies like [Jasmine](https://jasmine.github.io/), [Jest](https://jestjs.io/) and recently [Vitest](https://vitest.dev/). + + We choose using Jest for his large popularity and adoption in Javascript community and frameworks like Angular. + + + + + #### Jest installation and configuration + + ArmoniK Admin GUI is developed with Angular. Angular natively offers to use Jasmine and Karma for testing. + As we choose using Jest, we need to delete Jasmine and Karma of our project. + Then, we replace it by Jest Library and some usefuls packages with. + + To remove Jasmine and Karma from the project, run `pnpm remove jasmine karma`. + + #### Installation of Jest library and packages + + Run `pnpm add --save-dev jest @types/jest jest-environment-jsdom ts-jest ts-node jest-preset-angular`. + + @types/jest ts-node ts-jest and jest-preset-angular are needed for jest configuration in Typescript/Angular projects. + + jest-environment-jsdom is a package recommended by Jest documentation for DOM simulation. (https://jestjs.io/docs/tutorial-jquery). + + + + + + + + + + + From c5cbb9c072a1e66ef7561214679d5166d725adfa Mon Sep 17 00:00:00 2001 From: "Yannick BLEY (ANEO)" Date: Fri, 13 Oct 2023 09:42:30 +0200 Subject: [PATCH 02/37] docs: add config documentation --- image.png | Bin 0 -> 5225 bytes unit_test_formation.md | 14 ++++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 image.png diff --git a/image.png b/image.png new file mode 100644 index 0000000000000000000000000000000000000000..e11e3259eb09a7b82cb7614d86b26fb46f24eef6 GIT binary patch literal 5225 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 S8-KqCaa5JHz~zdTLH`2U#|uRO literal 0 HcmV?d00001 diff --git a/unit_test_formation.md b/unit_test_formation.md index a286a1d83..04b009752 100644 --- a/unit_test_formation.md +++ b/unit_test_formation.md @@ -35,11 +35,11 @@ - #### Jest installation and configuration + #### Jest installation ArmoniK Admin GUI is developed with Angular. Angular natively offers to use Jasmine and Karma for testing. + As we choose using Jest, we need to delete Jasmine and Karma of our project. - Then, we replace it by Jest Library and some usefuls packages with. To remove Jasmine and Karma from the project, run `pnpm remove jasmine karma`. @@ -51,6 +51,16 @@ jest-environment-jsdom is a package recommended by Jest documentation for DOM simulation. (https://jestjs.io/docs/tutorial-jquery). + + + #### Jest setup and configuration + +Once Jest installed, we need to declare it in our Typescript configuration file. +in ./tsconfig.spec.json, add "jest" and "node" to property types. +![Alt text](image.png) + + + From 5232f839d3420adf9b78ce747b8f6bdc8c1bab2f Mon Sep 17 00:00:00 2001 From: yannick-aneo <136307285+yannick-aneo@users.noreply.github.com> Date: Fri, 13 Oct 2023 16:56:19 +0200 Subject: [PATCH 03/37] Update unit_test_formation.md --- unit_test_formation.md | 137 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 124 insertions(+), 13 deletions(-) diff --git a/unit_test_formation.md b/unit_test_formation.md index 04b009752..762bb67e2 100644 --- a/unit_test_formation.md +++ b/unit_test_formation.md @@ -6,7 +6,7 @@ It brings to developers more confidence on what they're working on and more quality on software. - We have traditionnaly 3 types of tests. + We have traditionally 3 types of tests. Tests 1. Unit tests @@ -20,32 +20,34 @@ We'll be focused on unit testing. -#### What are Unit tests ? +### What are Unit tests ? Unit testing is focused on writing tests for small building blocks of an application. - It's about test every expected and unexpected behavior of units of our app. We usually write unit tests for classes, functions and small components. + It's about testing every expected and unexpected behaviour of our small unit of code. We usually write unit tests for classes, functions and standalone components. - We tests our classes and functions by checking their inputs and outputs in order to work more predictable and consistent code. + We test classes and functions by checking their inputs and outputs in order to work with more predictable and consistent code. + + Unit testing allows us to quicker find out and fix bugs. It also adds a technical documentation support for developers. - In Javascript eco-system, you can use librairies like [Jasmine](https://jasmine.github.io/), [Jest](https://jestjs.io/) and recently [Vitest](https://vitest.dev/). + In Javascript community you can use librairies like [Jasmine](https://jasmine.github.io/), [Jest](https://jestjs.io/) and recently [Vitest](https://vitest.dev/). - We choose using Jest for his large popularity and adoption in Javascript community and frameworks like Angular. + We choose using Jest for his large adoption and popularity by Javascript community and frameworks like React, Vue and Angular. - #### Jest installation + ### Jest installation ArmoniK Admin GUI is developed with Angular. Angular natively offers to use Jasmine and Karma for testing. - As we choose using Jest, we need to delete Jasmine and Karma of our project. + As we choose using Jest, we need to delete Jasmine and Karma of the project. - To remove Jasmine and Karma from the project, run `pnpm remove jasmine karma`. + For removing Jasmine and Karma from the project, run `pnpm remove karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter` and `pnpm remove jasmine-core @types/jasmine`. #### Installation of Jest library and packages - Run `pnpm add --save-dev jest @types/jest jest-environment-jsdom ts-jest ts-node jest-preset-angular`. + Run `pnpm add --save-dev jest @types/jest @types/node jest-environment-jsdom ts-jest ts-node jest-preset-angular`. @types/jest ts-node ts-jest and jest-preset-angular are needed for jest configuration in Typescript/Angular projects. @@ -53,11 +55,120 @@ - #### Jest setup and configuration + ### Jest setup and configuration Once Jest installed, we need to declare it in our Typescript configuration file. -in ./tsconfig.spec.json, add "jest" and "node" to property types. -![Alt text](image.png) +in `tsconfig.spec.json`, replace "jasmine" by : +``` +"types": [ + "jest", + "node", + "@angular/localize", + ] +``` + +Add also this after types array in tsconfig.spec.json: +``` + "emitDecoratorMetadata": true, + "esModuleInterop": true, + "module": "CommonJS" +``` + +In `tsconfig.app.json`, add again "jest" into types array: + +``` + types: [ + "jest" +] +``` +In project root, create a file named setup-jest.ts and add the following contents: +``` +import 'jest-preset-angular/setup-jest'; +import '@angular/localize/init'; +import { TextEncoder } from 'util'; + +global.TextEncoder = TextEncoder; +``` +Create a file named `jest.config.ts` with the following contents +``` +import type {Config} from 'jest'; + +const config: Config = { + clearMocks: true, + globals: { + structuredClone, + }, + moduleNameMapper: { + '@components/(.*)': '/src/app/components/$1', + '@services/(.*)': '/src/app/services/$1', + '@pipes/(.*)': '/src/app/pipes/$1', + '@app/(.*)': '/src/app/$1' + }, + preset: 'jest-preset-angular', + setupFilesAfterEnv: [ + '/setup-jest.ts' + ], + testEnvironment: './JSDOMEnvironmentPatch.ts', + testEnvironmentOptions: { + customExportConditions: ['named'] + } +}; + +export default config; +``` +We use the javascript function structuredClone() for deep-copying objects in GUI. Unfornately, jest-environment-jsdom doesn't support it. So we use a polyfill for testing services using it. + + +To add it, run: + + 1. `pnpm install @ungap/structured-clone` + 2. `pnpm i --save-dev @types/ungap__structured-clone` + 3. add `"allowSyntheticDefaultImports": true` into compilerOptions object JSON in tsconfig.json. + 4. Then, add the following line at the top of jest.config.ts `import structuredClone from '@ungap/structured-clone'`. + +Create a file named `JSDOMEnvironmentPatch.ts` with the following contents: +``` + import JSDOMEnvironment from 'jest-environment-jsdom'; + export default class JSDOMEnvironmentPatch extends JSDOMEnvironment { + constructor( + ...args: ConstructorParameters + ) { + super(...args); + + this.global.structuredClone = structuredClone; + } +} +``` +Don't forget to link it with jest.config.ts file with : `testEnvironment: './JSDOMEnvironmentPatch.ts'`. + +In package.json, replace `ng test` by `jest` into scripts : `"test": "jest"`. +``` +// package.json + ..."test": "jest" +``` +run `pnpm run test` let's go for tests !! + + + +### Write our first test + + +Jest provides us some API and functions to test our components. + +`describe()` allows us to define our tests suite. two parameters are expected: a string for the name of your test suite and a callback function in which we are going to write our unit tests. + +Before, we need to set up our component with BeforeEach() function in which we gonna create a component/service and provide his dependencies. + +We'll see later mocking is a quite good practice for testing components with dependecny injection. + +It's going to help us to only select what we need from other services and avoid to call real services and components in our tests when possible. + + + + + + + From 8060bb3e2b6a06ad3f41573eb627f4a41c089f78 Mon Sep 17 00:00:00 2001 From: yannick-aneo <136307285+yannick-aneo@users.noreply.github.com> Date: Mon, 16 Oct 2023 15:17:28 +0200 Subject: [PATCH 04/37] docs: documentation about setting up and creating unit test for a simple service --- unit_test_formation.md | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/unit_test_formation.md b/unit_test_formation.md index 762bb67e2..8bcf53f1d 100644 --- a/unit_test_formation.md +++ b/unit_test_formation.md @@ -150,18 +150,46 @@ run `pnpm run test` let's go for tests !! -### Write our first test +### Write our first test suite +We start by setting up our environment for testing our class. + +For a simple service, there is nothing hard to do. + +Jest provides us some API and functions to make our tests easier and readable. + +In `describe()` function, you must define the target of your test suite. + +A test suite is a set of multiple tests programmed for testing expected or unexpected behaviours of a class. + +The second argument is a callback function where you will write all your unit tests about the class. + + +![image](https://github.com/aneoconsulting/ArmoniK.Admin.GUI/assets/136307285/e040fafc-7d81-4ae2-ac6b-d65d79d72b46) + + +In this example, we classicly create our service. + +We use Jest API `expect` for testing that the service is really instantiated. + +[Expect](https://jestjs.io/docs/expect#expectvalue) API provides us lot of functions called "matchers" to check values and class behaviours in our assertions. + +We use `ToBeTruthy()` matcher to check if our service is correctly instantiated. + +You can read the documentation for more informations about matchers : https://jestjs.io/docs/expect#matchers + + +We use `it` function or `test()` function to write our unit test within. + +The first argument must describe what we expect from the unit test. +In order to make our tests more readable and quick to understand, it's usually important to write precisely the expected behaviour or value. + +The second argument is a callback function including our test. -Jest provides us some API and functions to test our components. -`describe()` allows us to define our tests suite. two parameters are expected: a string for the name of your test suite and a callback function in which we are going to write our unit tests. -Before, we need to set up our component with BeforeEach() function in which we gonna create a component/service and provide his dependencies. -We'll see later mocking is a quite good practice for testing components with dependecny injection. -It's going to help us to only select what we need from other services and avoid to call real services and components in our tests when possible. From 12f8fcd9eda065c8074724013f1f7565f7f2b785 Mon Sep 17 00:00:00 2001 From: yannick-aneo <136307285+yannick-aneo@users.noreply.github.com> Date: Mon, 16 Oct 2023 17:31:34 +0200 Subject: [PATCH 05/37] Write documentation on setting up tests for service with dependencies --- unit_test_formation.md | 75 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/unit_test_formation.md b/unit_test_formation.md index 8bcf53f1d..abe449593 100644 --- a/unit_test_formation.md +++ b/unit_test_formation.md @@ -150,7 +150,7 @@ run `pnpm run test` let's go for tests !! -### Write our first test suite +### Write unit tests for a simple service We start by setting up our environment for testing our class. @@ -168,7 +168,7 @@ The second argument is a callback function where you will write all your unit t ![image](https://github.com/aneoconsulting/ArmoniK.Admin.GUI/assets/136307285/e040fafc-7d81-4ae2-ac6b-d65d79d72b46) -In this example, we classicly create our service. +In the example above, we classicly create our service. We use Jest API `expect` for testing that the service is really instantiated. @@ -187,6 +187,77 @@ In order to make our tests more readable and quick to understand, it's usually i The second argument is a callback function including our test. +![image](https://github.com/aneoconsulting/ArmoniK.Admin.GUI/assets/136307285/39c4989f-1fc4-4a95-b74a-91509161e171) + +In the example above, we use the well-known AAA pattern for organzing our unit test + +1. Arrange : We initialize the object with methods and parameters we want to run after. +2. Act : We invoke methods or functions. +3. Assert : We check outputs and if returned values match with the expected behaviour. + +In this case, we use `jest.spyOn()`function to watch method calls. +You can read the documentation for more informations: https://jestjs.io/docs/jest-object#jestspyonobject-methodname + +Once spied, we call the getter. + +Then, we check with the spy if the getter was actually called with `toHaveBeenCalled()`. +You can read the documentation for more informations: https://jestjs.io/docs/expect#tohavebeencalled + + + +### Write unit tests for a service with dependencies + + +For testing a service with dependencies, we need to use more tools for configuration. + +We are going to work with Storage service for example. +As we can see, the Storage.service have 2 others services injected. +DefaultConfigService and Storage. +To test methods linked with these injected services and reflect dependency injection , we are going to mock them. + +![image](https://github.com/aneoconsulting/ArmoniK.Admin.GUI/assets/136307285/5bb526c8-a55d-42d3-bfc9-008378036cd9) + + +In the example above, we create two mocks: +1. mockItemData for simulating an object stored in local storage. +2. mockStorage for imitating behaviours of local storage browser Web API implemented by the service. + +![image](https://github.com/aneoconsulting/ArmoniK.Admin.GUI/assets/136307285/9e806d3e-7b24-46a5-a4a2-6a330740ff65) + +Then, we create our service by provding its required dependencies. We use the Angular API TestBed giving access to configureTestingModule. +We are going to configure our service thanks to this method. It takes an object wherein we will push mocked dependencies into an array at providers property. +We can directly push required services into providers' array. + +Or we can use an object like this : ![image](https://github.com/aneoconsulting/ArmoniK.Admin.GUI/assets/136307285/c5fa29d6-d75c-4c83-a35f-0ed31d2eb05c) + The "provide" value is the required service name. + + The "useValue" value is the mock in charge to replicating behaviours of the real dependency. + +we also need to push the real Storage Service into providers' array. + +After this, we call the inject() method to inject the dependencies. We call this function because we use it our project instead of using constructor. + + + + + + + + + + + + + + + + + + + + + + From af71c9df6991596d73e10f9e78d59e2c871c690c Mon Sep 17 00:00:00 2001 From: yannick-aneo <136307285+yannick-aneo@users.noreply.github.com> Date: Mon, 16 Oct 2023 17:52:48 +0200 Subject: [PATCH 06/37] update unit test formation --- unit_test_formation.md | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/unit_test_formation.md b/unit_test_formation.md index abe449593..1678fd7f0 100644 --- a/unit_test_formation.md +++ b/unit_test_formation.md @@ -35,6 +35,11 @@ We choose using Jest for his large adoption and popularity by Javascript community and frameworks like React, Vue and Angular. +### Prerequisites + +Before getting started with unit testing using Jest, make sure you have the following prerequisites: + +- Node.js and npm installed ### Jest installation @@ -208,7 +213,7 @@ You can read the documentation for more informations: https://jestjs.io/docs/exp ### Write unit tests for a service with dependencies -For testing a service with dependencies, we need to use more tools for configuration. +For testing a service with dependencies, we need to use more tools. We are going to work with Storage service for example. As we can see, the Storage.service have 2 others services injected. @@ -224,18 +229,34 @@ In the example above, we create two mocks: ![image](https://github.com/aneoconsulting/ArmoniK.Admin.GUI/assets/136307285/9e806d3e-7b24-46a5-a4a2-6a330740ff65) -Then, we create our service by provding its required dependencies. We use the Angular API TestBed giving access to configureTestingModule. -We are going to configure our service thanks to this method. It takes an object wherein we will push mocked dependencies into an array at providers property. +Then, we create our service by provding its required dependencies inside BeforeEach() function. + +This function will be called before each run of our test file. + +We use the Angular API `TestBed` giving access to `configureTestingModule()`. +We are going to configure our service thanks to this method. + +It takes an object wherein we will push mocked dependencies into an array at providers property. We can directly push required services into providers' array. -Or we can use an object like this : ![image](https://github.com/aneoconsulting/ArmoniK.Admin.GUI/assets/136307285/c5fa29d6-d75c-4c83-a35f-0ed31d2eb05c) - The "provide" value is the required service name. +Or we can use an object like this : + +![image](https://github.com/aneoconsulting/ArmoniK.Admin.GUI/assets/136307285/c5fa29d6-d75c-4c83-a35f-0ed31d2eb05c) - The "useValue" value is the mock in charge to replicating behaviours of the real dependency. + +The "provide" value is the required service name. + +The "useValue" value is the mock in charge to replicate behaviour of the real dependency. we also need to push the real Storage Service into providers' array. -After this, we call the inject() method to inject the dependencies. We call this function because we use it our project instead of using constructor. +After this, we call the `inject()` method to inject the dependencies. We call this function because we use it our project instead of using `constructor()`. + + +![image](https://github.com/aneoconsulting/ArmoniK.Admin.GUI/assets/136307285/6d201e17-a2a8-45e3-a7bf-9f5d56d0b352) + + +Rigth thre, we test the clear method of Storage service. From 8bd2103033ec9df05a65b5f0e1b8ca3c482ec008 Mon Sep 17 00:00:00 2001 From: yannick-aneo <136307285+yannick-aneo@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:36:04 +0200 Subject: [PATCH 07/37] finish first version of unit test formation in ArmoniK --- unit_test_formation.md | 118 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 108 insertions(+), 10 deletions(-) diff --git a/unit_test_formation.md b/unit_test_formation.md index 1678fd7f0..d91838197 100644 --- a/unit_test_formation.md +++ b/unit_test_formation.md @@ -17,7 +17,6 @@ - We'll be focused on unit testing. ### What are Unit tests ? @@ -155,7 +154,7 @@ run `pnpm run test` let's go for tests !! -### Write unit tests for a simple service +### Writing unit tests for a simple service We start by setting up our environment for testing our class. @@ -200,7 +199,7 @@ In the example above, we use the well-known AAA pattern for organzing our unit t 2. Act : We invoke methods or functions. 3. Assert : We check outputs and if returned values match with the expected behaviour. -In this case, we use `jest.spyOn()`function to watch method calls. +In this case, we use `jest.spyOn()`function to watch and tracks object method calls. You can read the documentation for more informations: https://jestjs.io/docs/jest-object#jestspyonobject-methodname Once spied, we call the getter. @@ -210,15 +209,21 @@ You can read the documentation for more informations: https://jestjs.io/docs/exp -### Write unit tests for a service with dependencies +### Writing unit tests for a service with dependencies For testing a service with dependencies, we need to use more tools. We are going to work with Storage service for example. -As we can see, the Storage.service have 2 others services injected. +Ou Storage service have 2 others services injected. DefaultConfigService and Storage. -To test methods linked with these injected services and reflect dependency injection , we are going to mock them. +For testing methods linked with these injected services and reflect dependency injection , we are going to mock them. + +Mocking in unit testing is a technique used to create simulated or "mock" versions of objects, components, or services that a unit of code being tested depends on. These mock objects simulate the behavior of real objects we use but allow you to control and isolate the interactions with those dependencies. + +When writing unit tests for Angular standalone components as we use in ArmoniK Admin GUI, creating mocks objects ease components and services isolation from their dependencies. We can then only be focused on testing specific units of code we are interested in. + + ![image](https://github.com/aneoconsulting/ArmoniK.Admin.GUI/assets/136307285/5bb526c8-a55d-42d3-bfc9-008378036cd9) @@ -237,7 +242,10 @@ We use the Angular API `TestBed` giving access to `configureTestingModule()`. We are going to configure our service thanks to this method. It takes an object wherein we will push mocked dependencies into an array at providers property. -We can directly push required services into providers' array. +We can directly push required services into providers' array. + +As we work with Angular standalone components (https://angular.io/guide/standalone-components), we don't need to declare ngModule in declarations' array because we don't use it. + Or we can use an object like this : @@ -248,15 +256,105 @@ The "provide" value is the required service name. The "useValue" value is the mock in charge to replicate behaviour of the real dependency. -we also need to push the real Storage Service into providers' array. +We also need to push the real Storage Service into providers' array. + +After this, we call the `inject()` method to instantiate our service. We call it because we mainly use it our project for dependency injection. -After this, we call the `inject()` method to inject the dependencies. We call this function because we use it our project instead of using `constructor()`. ![image](https://github.com/aneoconsulting/ArmoniK.Admin.GUI/assets/136307285/6d201e17-a2a8-45e3-a7bf-9f5d56d0b352) -Rigth thre, we test the clear method of Storage service. +Rigth there, we test the clear method of Storage service. + +First, we call clear() function from the service. Then, we test whether local storage clear function implemented by our function has been called. + +Run `pnpm run test` and you will see your test result : + +![image](https://github.com/aneoconsulting/ArmoniK.Admin.GUI/assets/136307285/4bd5066f-8d30-425a-92d5-0ef50c4ae3b9) + + +We can see the name of the service we test written in our first describe(). We also can see that our test is good. The behaviour of the tested function is really what we expect from it. + +When failing, Jest throws an error showing us exactly the failing test suite with the failing unit test within. + + +### Writing unit test for a component + + + +When writing unit tests for a component, we have to configure our component in another way. +Let's see with `view-tasks-by-status.component.spec.ts` : + +``` + + +describe('ViewTasksByStatusComponent', () => { + let component: ViewTasksByStatusComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [ + ViewTasksByStatusComponent, + TasksStatusesService + ] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ViewTasksByStatusComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); +... + +} + +``` + + +In the example above, we provide dependencies as usual. But we use `compileComponents()` for setting up our component. + +We create a fixture based on the component we created with `TestBed.createComponent()`. In unit testing, a fixture consists in a fixed state or set of inputs that serves as the basis for testing a particular unit of code, such as a function or method. The fixture ensures that the unit of code being tested has a consistent and known environment. + +We call `detectChange()`function on our fixture for triggering change detection. (https://angular.io/api/core/testing/ComponentFixture#detectChanges) + + + +Then, we can test our component as we are used to. + + + +### Add coverage + +Code coverage brings us useful metric for monitoring unit tests, as it provides insights into the effectiveness and completeness of your test suite. Code coverage helps you assess how much of your code is exercised by your tests and can be valuable. + +You can add coverage for your unit test with adding this in scripts in `package.json`: + +``` +... + "test-coverage": "jest --coverage" +``` + +It will show you the proprtion of uncovered and covered code segements of all your tests in the app. + + + + + + + + + + + + + + + + + From 094ba8ef127c45043caf409416b8b628613c4056 Mon Sep 17 00:00:00 2001 From: Faust1 <117363666+Faust1-2was-Aneo@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:46:49 +0200 Subject: [PATCH 08/37] fix: http to https in tests (#745) Co-authored-by: yannick-aneo <136307285+yannick-aneo@users.noreply.github.com> --- src/app/dashboard/index.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/dashboard/index.component.spec.ts b/src/app/dashboard/index.component.spec.ts index c7b4e7d1d..69b317f8e 100644 --- a/src/app/dashboard/index.component.spec.ts +++ b/src/app/dashboard/index.component.spec.ts @@ -14,7 +14,7 @@ describe('IndexComponent', () => { let dialogRef$: Observable; - const defaultUrl = 'http://some-url/'; + const defaultUrl = 'https://some-url/'; const defaultLines: Line[] = [ { name: 'line1', From 8ff764529e18a14a38dc4f5e709e632201528971 Mon Sep 17 00:00:00 2001 From: Faust1 <117363666+Faust1-2was-Aneo@users.noreply.github.com> Date: Tue, 17 Oct 2023 14:33:35 +0200 Subject: [PATCH 09/37] test: dashboard storage service (#737) Co-authored-by: yannick-aneo <136307285+yannick-aneo@users.noreply.github.com> --- .../dashboard-storage.service.spec.ts | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/app/dashboard/services/dashboard-storage.service.spec.ts diff --git a/src/app/dashboard/services/dashboard-storage.service.spec.ts b/src/app/dashboard/services/dashboard-storage.service.spec.ts new file mode 100644 index 000000000..f33102fb8 --- /dev/null +++ b/src/app/dashboard/services/dashboard-storage.service.spec.ts @@ -0,0 +1,82 @@ +import { TaskStatus } from '@aneoconsultingfr/armonik.api.angular'; +import { TestBed } from '@angular/core/testing'; +import { StorageService } from '@services/storage.service'; +import { DashboardStorageService } from './dashboard-storage.service'; +import { Line } from '../types'; + +describe('DashboardStorageService', () => { + let service: DashboardStorageService; + + const mockStorageService = { + setItem: jest.fn(), + getItem: jest.fn() + }; + + const lines: Line[] = [ + { + name: 'line1', + interval: 10, + hideGroupsHeader: false, + filters: [], + taskStatusesGroups: [ + { name: 'Success', color: 'green', statuses: [TaskStatus.TASK_STATUS_COMPLETED, TaskStatus.TASK_STATUS_PROCESSED]}, + { name: 'Running', color: 'yellow', statuses: [TaskStatus.TASK_STATUS_CREATING, TaskStatus.TASK_STATUS_PROCESSING]}, + { name: 'Error', color: 'red', statuses: [TaskStatus.TASK_STATUS_CANCELLED, TaskStatus.TASK_STATUS_TIMEOUT]} + ], + }, + { + name: 'line2', + interval: 20, + hideGroupsHeader: true, + filters: [], + taskStatusesGroups: [ + { name: 'Success', color: 'green', statuses: [TaskStatus.TASK_STATUS_COMPLETED, TaskStatus.TASK_STATUS_PROCESSED]}, + { name: 'Running', color: 'yellow', statuses: [TaskStatus.TASK_STATUS_CREATING, TaskStatus.TASK_STATUS_PROCESSING]}, + { name: 'Unspecified', color: 'grey', statuses: [TaskStatus.TASK_STATUS_UNSPECIFIED, TaskStatus.TASK_STATUS_RETRIED]} + ], + } + ]; + + beforeEach(() => { + service = TestBed.configureTestingModule({ + providers: [ + DashboardStorageService, + { provide: StorageService, useValue: mockStorageService } + ] + }).inject(DashboardStorageService); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('shoud save lines', () => { + service.saveLines(lines); + expect(mockStorageService.setItem).toHaveBeenCalledWith('dashboard-lines', lines); + }); + + it('should restore lines', () => { + mockStorageService.getItem.mockImplementationOnce(() => lines); + expect(service.restoreLines()).toEqual(lines); + }); + + it('should return null if there is no lines', () => { + mockStorageService.getItem.mockImplementation(() => undefined); + expect(service.restoreLines()).toEqual(null); + }); + + it('should save splitted lines', () => { + service.saveSplitLines(4); + expect(mockStorageService.setItem).toHaveBeenCalledWith('dashboard-split-lines', 4); + }); + + it('should restore splitted lines', () => { + mockStorageService.getItem.mockImplementationOnce(() => 2); + expect(service.restoreSplitLines()).toEqual(2); + }); + + it('should return null if there is no lines', () => { + mockStorageService.getItem.mockImplementation(() => undefined); + expect(service.restoreSplitLines()).toEqual(null); + }); +}); \ No newline at end of file From f8b24b2636fd906ed09817be47a72135f8218894 Mon Sep 17 00:00:00 2001 From: Faust1 <117363666+Faust1-2was-Aneo@users.noreply.github.com> Date: Wed, 18 Oct 2023 08:50:47 +0200 Subject: [PATCH 10/37] chore: fix sort unreliability (#715) Co-authored-by: yannick-aneo <136307285+yannick-aneo@users.noreply.github.com> --- src/app/components/columns-modify-dialog.component.ts | 4 ++-- src/app/components/show-card-content.component.ts | 2 +- .../components/view-tasks-by-status-dialog.component.spec.ts | 4 ++-- src/app/dashboard/services/dashboard-index.service.ts | 4 ++-- src/app/settings/index.component.ts | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/components/columns-modify-dialog.component.ts b/src/app/components/columns-modify-dialog.component.ts index d1ed92daa..fb7578994 100644 --- a/src/app/components/columns-modify-dialog.component.ts +++ b/src/app/components/columns-modify-dialog.component.ts @@ -86,13 +86,13 @@ export class ColumnsModifyDialogComponent imp * Sort the columns alphabetically */ availableColumns(): (keyof T | 'actions')[] { - const columns = this.data.availableColumns.filter(column => !column.toString().startsWith('options.')).sort() as (keyof T | 'actions')[]; + const columns = this.data.availableColumns.filter(column => !column.toString().startsWith('options.')).sort((a, b) => a.toString().localeCompare(b.toString())) as (keyof T | 'actions')[]; return columns; } availableOptionsColumns(): PrefixedOptions[] { - const columns = this.data.availableColumns.filter(column => column.toString().startsWith('options.')).sort() as PrefixedOptions[]; + const columns = this.data.availableColumns.filter(column => column.toString().startsWith('options.')).sort((a, b) => a.toString().localeCompare(b.toString())) as PrefixedOptions[]; return columns; } diff --git a/src/app/components/show-card-content.component.ts b/src/app/components/show-card-content.component.ts index cfa8d1679..b1e0f987c 100644 --- a/src/app/components/show-card-content.component.ts +++ b/src/app/components/show-card-content.component.ts @@ -87,7 +87,7 @@ export class ShowCardContentComponent implements OnChanges { ngOnChanges() { if (this.data) { - this.keys = Object.keys(this.data).sort(); + this.keys = Object.keys(this.data).sort((a, b) => a.toString().localeCompare(b.toString())); } } diff --git a/src/app/components/view-tasks-by-status-dialog.component.spec.ts b/src/app/components/view-tasks-by-status-dialog.component.spec.ts index 33b36a9f7..b934cf0b7 100644 --- a/src/app/components/view-tasks-by-status-dialog.component.spec.ts +++ b/src/app/components/view-tasks-by-status-dialog.component.spec.ts @@ -51,7 +51,7 @@ describe('ViewTasksByStatusDialogComponent', () => { }); it('should retrieve tasks keys with tasksStatuses', () => { - expect(component.tasksStatuses().sort()).toEqual([ + expect(component.tasksStatuses().sort((a, b) => a.toString().localeCompare(b.toString()))).toEqual([ TaskStatus.TASK_STATUS_UNSPECIFIED.toString(), TaskStatus.TASK_STATUS_DISPATCHED.toString(), TaskStatus.TASK_STATUS_CREATING.toString(), @@ -64,7 +64,7 @@ describe('ViewTasksByStatusDialogComponent', () => { TaskStatus.TASK_STATUS_ERROR.toString(), TaskStatus.TASK_STATUS_TIMEOUT.toString(), TaskStatus.TASK_STATUS_RETRIED.toString() - ].sort()); + ].sort((a, b) => a.toString().localeCompare(b.toString()))); }); it('should retrieve a label by its corresponding status', () => { diff --git a/src/app/dashboard/services/dashboard-index.service.ts b/src/app/dashboard/services/dashboard-index.service.ts index 9f684650a..4855f6abb 100644 --- a/src/app/dashboard/services/dashboard-index.service.ts +++ b/src/app/dashboard/services/dashboard-index.service.ts @@ -16,8 +16,8 @@ export class DashboardIndexService { // TODO: move to TasksStatusesService statuses(): { value: string, name: string }[] { - const values = Object.values(this.#tasksStatusesService.statuses).sort(); - const keys = Object.keys(this.#tasksStatusesService.statuses).sort(); + const values = Object.values(this.#tasksStatusesService.statuses).sort((a, b) => a.toString().localeCompare(b.toString())); + const keys = Object.keys(this.#tasksStatusesService.statuses).sort((a, b) => a.toString().localeCompare(b.toString())); const sortedKeys = values.map((value) => { return keys.find((key) => { return this.#tasksStatusesService.statuses[Number(key) as TaskStatus] === value; diff --git a/src/app/settings/index.component.ts b/src/app/settings/index.component.ts index 684cf6d02..a1f7261ab 100644 --- a/src/app/settings/index.component.ts +++ b/src/app/settings/index.component.ts @@ -455,6 +455,6 @@ export class IndexComponent implements OnInit { } #sortKeys(keys: Set): Set { - return new Set([...keys].sort()); + return new Set([...keys].sort((a, b) => a.localeCompare(b))); } } From 95dbad6379dcfa782e914d809152ed002cd4bf0f Mon Sep 17 00:00:00 2001 From: Faust1 <117363666+Faust1-2was-Aneo@users.noreply.github.com> Date: Wed, 18 Oct 2023 08:55:59 +0200 Subject: [PATCH 11/37] feat: lock and unlock columns for tables (#742) Co-authored-by: yannick-aneo <136307285+yannick-aneo@users.noreply.github.com> --- src/app/applications/index.component.ts | 11 +++++++++- .../services/applications-index.service.ts | 13 ++++++++++++ .../components/columns-button.component.ts | 3 ++- .../table-actions-toolbar.component.spec.ts | 6 ++++++ .../table-actions-toolbar.component.ts | 16 ++++++++++++++ src/app/partitions/index.component.ts | 14 +++++++++++-- .../services/partitions-index.service.ts | 13 ++++++++++++ src/app/results/index.component.ts | 14 +++++++++++-- .../results/services/results-index.service.ts | 13 ++++++++++++ src/app/services/default-config.service.ts | 10 +++++++++ src/app/services/icons.service.ts | 2 ++ src/app/services/table.service.spec.ts | 10 +++++++++ src/app/services/table.service.ts | 8 +++++++ src/app/sessions/index.component.ts | 11 +++++++++- .../services/sessions-index.service.ts | 13 ++++++++++++ src/app/tasks/index.component.ts | 15 +++++++++++-- .../services/tasks-index.service.spec.ts | 21 ++++++++++++++++++- src/app/tasks/services/tasks-index.service.ts | 13 ++++++++++++ src/app/types/config.ts | 3 ++- 19 files changed, 198 insertions(+), 11 deletions(-) diff --git a/src/app/applications/index.component.ts b/src/app/applications/index.component.ts index 5d535a127..e2d1c9186 100644 --- a/src/app/applications/index.component.ts +++ b/src/app/applications/index.component.ts @@ -62,11 +62,13 @@ import { ApplicationRaw, ApplicationRawColumnKey, ApplicationRawFieldKey, Applic [columnsLabels]="columnsLabels()" [displayedColumns]="displayedColumns" [availableColumns]="availableColumns" + [lockColumns]="lockColumns" (refresh)="onRefresh()" (intervalValueChange)="onIntervalValueChange($event)" (displayedColumnsChange)="onColumnsChange($event)" (resetColumns)="onColumnsReset()" (resetFilters)="onFiltersReset()" + (lockColumnsChange)="onLockColumnsChange()" >