diff --git a/build-all.ps1 b/build-all.ps1 index 023cbdeb4a..649abc5f8c 100644 --- a/build-all.ps1 +++ b/build-all.ps1 @@ -21,7 +21,6 @@ $solutionPaths = ( "modules/client-simulation", "templates/module/aspnet-core", "templates/app/aspnet-core", - "samples/MicroserviceDemo", "abp_io/AbpIoLocalization" ) diff --git a/docs/en/Tutorials/Angular/Part-I.md b/docs/en/Tutorials/Angular/Part-I.md index fdfddef683..6dadc24841 100644 --- a/docs/en/Tutorials/Angular/Part-I.md +++ b/docs/en/Tutorials/Angular/Part-I.md @@ -8,6 +8,7 @@ This is the first part of the Angular tutorial series. See all parts: - **Part I: Create the project and a book list page (this tutorial)** - [Part II: Create, Update and Delete books](Part-II.md) +- [Part III: Integration Tests](Part-III.md) You can access to the **source code** of the application from the [GitHub repository](https://github.com/abpframework/abp/tree/dev/samples/BookStore-Angular-MongoDb). diff --git a/docs/en/Tutorials/Angular/Part-II.md b/docs/en/Tutorials/Angular/Part-II.md index b322783db3..f18ec9dce1 100644 --- a/docs/en/Tutorials/Angular/Part-II.md +++ b/docs/en/Tutorials/Angular/Part-II.md @@ -6,6 +6,7 @@ This is the second part of the Angular tutorial series. See all parts: - [Part I: Create the project and a book list page](Part-I.md) - **Part II: Create, Update and Delete books (this tutorial)** +- [Part III: Integration Tests](Part-III.md) You can access to the **source code** of the application from the [GitHub repository](https://github.com/abpframework/abp/tree/dev/samples/BookStore-Angular-MongoDb). @@ -37,19 +38,19 @@ Open the `books.service.ts` and add a new method, named `create` to perform an H ```typescript create(body: Books.CreateUpdateBookInput): Observable { - const request: Rest.Request = { + return this.rest.request({ method: 'POST', url: '/api/app/book', body, - }; - - return this.rest.request(request); + }); } ``` +* `rest.request` function gets generic parameters for the types sent to and received from the server. This example sends a `CreateUpdateBookInput` object and receives a `Book` object (you can set `void` for request or return type if not used). + #### State Definitions -Add the `CreateUpdateBook` action to `books.actions.ts` as shown below: +Add the `CreateUpdateBook` action to the `books.actions.ts` as shown below: ```typescript import { Books } from '../models'; @@ -78,7 +79,7 @@ When the `SaveBook` action dispatched, the save method is executed. It call `cre #### Add a Modal to BookListComponent -Open the `book-list.component.html` and add the `abp-modal` to show/hide the book form. +Open the `book-list.component.html` and add the `abp-modal` to show/hide the modal to create a new book. ```html @@ -98,7 +99,7 @@ Open the `book-list.component.html` and add the `abp-modal` to show/hide the boo `abp-modal` is a pre-built component to show modals. While you could use another approach to show a modal, `abp-modal` provides additional benefits. -Add a button, labeled `New book` to show the modal. +Add a button, labeled `New book` to show the modal: ```html
@@ -161,8 +162,8 @@ buildForm() { } ``` -- The `group` method of `FormBuilder` creates a `FormGroup`. -- Added `Validators.required` static method that validation of form element. +- The `group` method of `FormBuilder` (`fb`) creates a `FormGroup`. +- Added `Validators.required` static method that validates the related form element. Modify the `onAdd` method as shown below: @@ -173,7 +174,7 @@ onAdd() { } ``` -### Create the DOM Elements of the Form +#### Create the DOM Elements of the Form Open `book-list.component.html` and add the form in the body template of the modal. @@ -213,28 +214,32 @@ Open `book-list.component.html` and add the form in the body template of the mod ``` -TODO: Add a short description. +* This template creates a form with Name, Price, Type and Publish date fields. > We've used [NgBootstrap datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview) in this component. -Open the `book-list.component.ts` and then add the `bookTypes`. +Open the `book-list.component.ts` and then create an array, named `bookTypeArr`: ```typescript //... form: FormGroup; -bookTypeArr = Object.keys(Books.BookType).filter(bookType => typeof this.booksType[bookType] === 'number'); +bookTypeArr = Object.keys(Books.BookType).filter( + bookType => typeof this.booksType[bookType] === 'number' +); ``` -The `bookTypes` variable added to generate array from `BookType` enum. The `bookTypes` equals like this: +The `bookTypeArr` contains the fields of the `BookType` enum. Resulting array is shown below: ```js ['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...] ``` -### Add the Datepicker Requirements +This array was used in the previous form template (in the `ngFor` loop). -Import `NgbDatepickerModule` to the `books.module.ts`. +#### Datepicker Requirements + +You need to import `NgbDatepickerModule` to the `books.module.ts`: ```typescript import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; @@ -248,25 +253,39 @@ import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; export class BooksModule {} ``` -Open the `book-list.component.html` and then add `providers` as shown below: +Then open the `book-list.component.html` and add `providers` as shown below: ```typescript @Component({ // ... - styleUrls: ['./book-list.component.scss'], providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], }) export class BookListComponent implements OnInit { // ... ``` -> The `NgbDateAdapter` convert Datepicker value type to `Date` type. See the [datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview) for more details. +> The `NgbDateAdapter` converts Datepicker value to `Date` type. See the [datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview) for more details. ![new-book-form](images/bookstore-new-book-form.png) -### Create a New Book +#### Saving the Book -Add the `save` method to `BookListComponent` +Open the `book-list.component.html` and add an `abp-button` to save the form. + +```html + + + Save + +``` + +This adds a save button to the bottom area of the modal: + +![bookstore-new-book-form-v2](images/bookstore-new-book-form-v2.png) + +Then define a `save` method in the `BookListComponent`: ```typescript save() { @@ -281,56 +300,32 @@ save() { } ``` -TODO: description ?? +### Updating An Existing Book -Then, open the `book-list.component.html` and add the `abp-button` for the run `save` method. - -```html - - - Save - -``` - -Now, You can add a new book. - -![bookstore-new-book-form-v2](images/bookstore-new-book-form-v2.png) - -### Add HTTP GET and PUT Methods - -TODO: Description +#### BooksService Open the `books.service.ts` and then add the `getById` and `update` methods. ```typescript getById(id: string): Observable { - const request: Rest.Request = { + return this.rest.request({ method: 'GET', url: `/api/app/book/${id}`, - }; - - return this.rest.request(request); + }); } update(body: Books.CreateUpdateBookInput, id: string): Observable { - const request: Rest.Request = { + return this.rest.request({ method: 'PUT', url: `/api/app/book/${id}`, body, - }; - - return this.rest.request(request); + }); } ``` -- Added the `getById` method to get the editing book by performing an HTTP request to the related endpoint. -- Added the `update` method to update a book with the `id` by performing an HTTP request to the related endpoint. - -TODO: header ?? +#### CreateUpdateBook Action -Open the `books.actins.ts` and add `id` parameter. +Open the `books.actins.ts` and add `id` parameter to the `CreateUpdateBook` action: ```typescript export class CreateUpdateBook { @@ -339,9 +334,7 @@ export class CreateUpdateBook { } ``` -Added `id` parameter to reuse the `BooksSave` while updating and creating a book. - -Open `books.state.ts` and then modify the `save` method as show below: +Open `books.state.ts` and modify the `save` method as show below: ```typescript @Action(CreateUpdateBook) @@ -358,9 +351,9 @@ save({ dispatch }: StateContext, { payload, id }: CreateUpdateBook) } ``` -### Update a Book +#### BookListComponent -Inject `BooksService` dependency by adding it to the `book-list.component.ts` constructor and add variable named `selectedBook`. +Inject `BooksService` dependency by adding it to the `book-list.component.ts` constructor and add a variable named `selectedBook`. ```typescript import { BooksService } from '../shared/books.service'; @@ -373,10 +366,7 @@ constructor( ) ``` -- Added `booksService` to get the detail of selected book by `id` before creating the form -- Added `selectedBook` variable to reuse detail of selected book. - -Modify the `buildForm` method to reuse the same form while editing a book. +`booksService` is used to get the editing book to prepare the form. Modify the `buildForm` method to reuse the same form while editing a book. ```typescript buildForm() { @@ -401,9 +391,9 @@ Add the `onEdit` method as shown below: } ``` -- Added `onEdit` method to get selected book detail, build form and then show the modal. +Added `onEdit` method to get the editing book, build the form and show the modal. -Add the `selectedBook` definition to `onAdd` method for reuse same form while adding a new book. +Now, add the `selectedBook` definition to `onAdd` method to reuse the same form while creating a new book: ```typescript onAdd() { @@ -412,7 +402,7 @@ Add the `selectedBook` definition to `onAdd` method for reuse same form while ad } ``` -Modify the `save` method as shown below: +Modify the `save` method to pass the id of the selected book as shown below: ```typescript save() { @@ -428,8 +418,6 @@ save() { } ``` -Added the `this.selectedBook.id` property to reuse the `CreateUpdateBook` action. - #### Add "Actions" Dropdown to the Table Open the `book-list.component.html` and add modify the `p-table` as shown below: @@ -471,15 +459,16 @@ Open the `book-list.component.html` and add modify the `p-table` as shown belo ``` -Actions button added to each row of the table. +* Added a `th` for the "Actions" column. +* Added `button` with `ngbDropdownToggle` to open actions when clicked the button. > We've used to [NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples) for the dropdown menu of actions. -The Actions buttons looks like this: +The final UI looks like: ![actions-buttons](images/bookstore-actions-buttons.png) -Update the modal header for reuse the same modal. +Update the modal header to change the title based on the current operation: ```html @@ -489,26 +478,24 @@ Update the modal header for reuse the same modal. ![actions-buttons](images/bookstore-edit-modal.png) -TODO: description & screenshot ?? +### Deleting an Existing Book -### Delete a Book +#### BooksService -Open `books.service.ts` and the the `delete` method for delete a book with the `id` by performing an HTTP request to the related endpoint. +Open `books.service.ts` and add a `delete` method to delete a book with the `id` by performing an HTTP request to the related endpoint: ```typescript -delete(id: string): Observable { - const request: Rest.Request = { +delete(id: string): Observable { + return this.rest.request({ method: 'DELETE', - url: `/api/app/book/${id}`, - }; - - return this.rest.request(request); + url: `/api/app/book/${id}` + }); } ``` -### State Definitions +#### DeleteBook Action -Add an action named `BooksDelete` to `books.actions.ts` +Add an action named `DeleteBook` to `books.actions.ts`: ```typescript export class DeleteBook { @@ -517,7 +504,7 @@ export class DeleteBook { } ``` -Then, open the `books.state.ts` and add the `delete` method that will listen to a `CreateUpdateBook` action to create a book +Open the `books.state.ts` and add the `delete` method that will listen to the `DeleteBook` action to delete a book: ```typescript import { ... , DeleteBook } from '../actions/books.actions'; @@ -528,11 +515,12 @@ delete({ dispatch }: StateContext, { id }: DeleteBook) { } ``` -TODO: description?? +* Added `DeleteBook` to the import list. +* Uses `bookService` to delete the book. -### Add a Delete Button +#### Add a Delete Button -Open `book-list.component.html` and modify the `ngbDropdownMenu` for add the delete button as shown below: +Open `book-list.component.html` and modify the `ngbDropdownMenu` to add the delete button as shown below: ```html
@@ -543,13 +531,13 @@ Open `book-list.component.html` and modify the `ngbDropdownMenu` for add the del
``` -The final actions dropdown UI looks like this: +The final actions dropdown UI looks like below: ![bookstore-final-actions-dropdown](images/bookstore-final-actions-dropdown.png) -### Open Confirmation Popup +#### Delete Confirmation Dialog -Open `book-list.component.ts` and inject the `ConfirmationService` for show confirmation popup. +Open `book-list.component.ts` and inject the `ConfirmationService`. ```typescript import { ConfirmationService } from '@abp/ng.theme.shared'; @@ -560,7 +548,9 @@ constructor( ) ``` -Add the following method to `BookListComponent`. +> `ConfirmationService` is a simple service provided by ABP framework that internally uses the PrimeNG. + +Add a delete method to the `BookListComponent`: ```typescript import { ... , DeleteBook } from '../../store/actions'; @@ -577,10 +567,10 @@ delete(id: string, name: string) { } ``` -The `delete` method shows confirmation popup and listens to them. When close the popup, the subscribe block runs. If confirmed this popup, it will dispatch the `DeleteBook` action. - -The confirmation popup looks like this: +The `delete` method shows a confirmation popup and subscribes for the user response. `DeleteBook` action dispatched only if user clicks to the `Yes` button. The confirmation popup looks like below: ![bookstore-confirmation-popup](images/bookstore-confirmation-popup.png) -TODO: End +### Next Part + +See the [next part](Part-III.md) of this tutorial. \ No newline at end of file diff --git a/docs/en/Tutorials/Angular/Part-III.md b/docs/en/Tutorials/Angular/Part-III.md new file mode 100644 index 0000000000..0da85f0751 --- /dev/null +++ b/docs/en/Tutorials/Angular/Part-III.md @@ -0,0 +1,178 @@ +## ASP.NET Core MVC Tutorial - Part III + +### About this Tutorial + +This is the third part of the Angular tutorial series. See all parts: + +- [Part I: Create the project and a book list page](Part-I.md) +- [Part II: Create, Update and Delete books](Part-II.md) +- **Part III: Integration Tests (this tutorial)** + +This part covers the **server side** tests. You can access to the **source code** of the application from the [GitHub repository](https://github.com/abpframework/abp/tree/dev/samples/BookStore-Angular-MongoDb). + +### Test Projects in the Solution + +There are multiple test projects in the solution: + +![bookstore-test-projects](images/bookstore-test-projects-v3.png) + +Each project is used to test the related application project. Test projects use the following libraries for testing: + +* [xunit](https://xunit.github.io/) as the main test framework. +* [Shoudly](http://shouldly.readthedocs.io/en/latest/) as an assertion library. +* [NSubstitute](http://nsubstitute.github.io/) as a mocking library. + +### Adding Test Data + +Startup template contains the `BookStoreTestDataSeedContributor` class in the `Acme.BookStore.TestBase` project that creates some data to run tests on. + +Change the `BookStoreTestDataSeedContributor` class as show below: + +````C# +using System; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Guids; + +namespace Acme.BookStore +{ + public class BookStoreTestDataSeedContributor + : IDataSeedContributor, ITransientDependency + { + private readonly IRepository _bookRepository; + private readonly IGuidGenerator _guidGenerator; + + public BookStoreTestDataSeedContributor( + IRepository bookRepository, + IGuidGenerator guidGenerator) + { + _bookRepository = bookRepository; + _guidGenerator = guidGenerator; + } + + public async Task SeedAsync(DataSeedContext context) + { + await _bookRepository.InsertAsync( + new Book + { + Id = _guidGenerator.Create(), + Name = "Test book 1", + Type = BookType.Fantastic, + PublishDate = new DateTime(2015, 05, 24), + Price = 21 + } + ); + + await _bookRepository.InsertAsync( + new Book + { + Id = _guidGenerator.Create(), + Name = "Test book 2", + Type = BookType.Science, + PublishDate = new DateTime(2014, 02, 11), + Price = 15 + } + ); + } + } +} +```` + +* Injected `IRepository` and used it in the `SeedAsync` to create two book entities as the test data. +* Used `IGuidGenerator` service to create GUIDs. While `Guid.NewGuid()` would perfectly work for testing, `IGuidGenerator` has additional features especially important while using real databases (see the [Guid generation document](../../Guid-Generation.md) for more). + +### Testing the BookAppService + +Create a test class named `BookAppService_Tests` in the `Acme.BookStore.Application.Tests` project: + +````C# +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Application.Dtos; +using Xunit; + +namespace Acme.BookStore +{ + public class BookAppService_Tests : BookStoreApplicationTestBase + { + private readonly IBookAppService _bookAppService; + + public BookAppService_Tests() + { + _bookAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_List_Of_Books() + { + //Act + var result = await _bookAppService.GetListAsync( + new PagedAndSortedResultRequestDto() + ); + + //Assert + result.TotalCount.ShouldBeGreaterThan(0); + result.Items.ShouldContain(b => b.Name == "Test book 1"); + } + } +} +```` + +* `Should_Get_List_Of_Books` test simply uses `BookAppService.GetListAsync` method to get and check the list of users. + +Add a new test that creates a valid new book: + +````C# +[Fact] +public async Task Should_Create_A_Valid_Book() +{ + //Act + var result = await _bookAppService.CreateAsync( + new CreateUpdateBookDto + { + Name = "New test book 42", + Price = 10, + PublishDate = DateTime.Now, + Type = BookType.ScienceFiction + } + ); + + //Assert + result.Id.ShouldNotBe(Guid.Empty); + result.Name.ShouldBe("New test book 42"); +} +```` + +Add a new test that tries to create an invalid book and fails: + +````C# +[Fact] +public async Task Should_Not_Create_A_Book_Without_Name() +{ + var exception = await Assert.ThrowsAsync(async () => + { + await _bookAppService.CreateAsync( + new CreateUpdateBookDto + { + Name = "", + Price = 10, + PublishDate = DateTime.Now, + Type = BookType.ScienceFiction + } + ); + }); + + exception.ValidationErrors + .ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); +} +```` + +* Since the `Name` is empty, ABP throws an `AbpValidationException`. + +Open the **Test Explorer Window** (use Test -> Windows -> Test Explorer menu if it is not visible) and **Run All** tests: + +![bookstore-appservice-tests](images/bookstore-test-explorer.png) + +Congratulations, green icons show that tests have been successfully passed! \ No newline at end of file diff --git a/docs/en/Tutorials/Angular/images/bookstore-test-explorer.png b/docs/en/Tutorials/Angular/images/bookstore-test-explorer.png new file mode 100644 index 0000000000..06e9e7d331 Binary files /dev/null and b/docs/en/Tutorials/Angular/images/bookstore-test-explorer.png differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-test-projects-v3.png b/docs/en/Tutorials/Angular/images/bookstore-test-projects-v3.png new file mode 100644 index 0000000000..32ea91b325 Binary files /dev/null and b/docs/en/Tutorials/Angular/images/bookstore-test-projects-v3.png differ diff --git a/npm/ng-packs/packages/identity/src/lib/components/users/users.component.html b/npm/ng-packs/packages/identity/src/lib/components/users/users.component.html index df4b6a2eb5..9f52238aee 100644 --- a/npm/ng-packs/packages/identity/src/lib/components/users/users.component.html +++ b/npm/ng-packs/packages/identity/src/lib/components/users/users.component.html @@ -188,6 +188,7 @@ requestURLContainSearchValue="users" iconClass="fa fa-check" (click)="save()" + [disabled]="form.invalid" >{{ 'AbpIdentity::Save' | abpLocalization }}
diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts index 86ef5f2bd4..3340a02cb0 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts @@ -6,7 +6,7 @@ import { filter } from 'rxjs/operators'; @Component({ selector: 'abp-button', template: ` - `, @@ -24,6 +24,9 @@ export class ButtonComponent implements OnInit { @Input() loading: boolean = false; + @Input() + disabled: boolean = false; + @Input() requestType: string | string[]; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.html b/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.html index 559bc6b429..48027b54a4 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.html +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.html @@ -1,6 +1,7 @@