Browse Source

update: documents for typos and small enhancements

pull/25507/head
sumeyye 4 months ago
parent
commit
c44c2ad4f7
  1. 2
      docs/en/framework/ui/angular/authorization.md
  2. 1
      docs/en/framework/ui/angular/checkbox-component.md
  3. 18
      docs/en/framework/ui/angular/data-table-column-extensions.md
  4. 19
      docs/en/framework/ui/angular/dynamic-form-extensions.md
  5. 20
      docs/en/framework/ui/angular/entity-action-extensions.md
  6. 2
      docs/en/framework/ui/angular/extensions-overall.md
  7. 16
      docs/en/framework/ui/angular/feature-libraries.md
  8. 2
      docs/en/framework/ui/angular/features.md
  9. 2
      docs/en/framework/ui/angular/how-replaceable-components-work-with-extensions.md
  10. 2
      docs/en/framework/ui/angular/internet-connection-service.md
  11. 2
      docs/en/framework/ui/angular/oauth-module.md
  12. 41
      docs/en/framework/ui/angular/page-toolbar-extensions.md
  13. 5
      docs/en/framework/ui/angular/quick-start.md
  14. 2
      docs/en/framework/ui/angular/settings.md
  15. 411
      docs/en/framework/ui/angular/testing.md

2
docs/en/framework/ui/angular/authorization.md

@ -105,7 +105,7 @@ function configureAuthFilter() {
} }
``` ```
- `AuthErrorFilter:` is a model for filter object and it have 3 properties - `AuthErrorFilter:` is a model for filter object and it has 3 properties
- `id:` a unique key in the list for the filter object - `id:` a unique key in the list for the filter object
- `executable:` a status for the filter object. If it's false then it won't work, yet it'll stay in the list - `executable:` a status for the filter object. If it's false then it won't work, yet it'll stay in the list
- `execute:` a function that stores the skip logic - `execute:` a function that stores the skip logic

1
docs/en/framework/ui/angular/checkbox-component.md

@ -14,7 +14,6 @@ The ABP Checkbox Component is a reusable form input component for the checkbox t
- `label` - `label`
- `labelClass (default form-check-label)` - `labelClass (default form-check-label)`
- `checkboxId` - `checkboxId`
- `checkboxReadonly`
- `checkboxReadonly (default form-check-input)` - `checkboxReadonly (default form-check-input)`
- `checkboxStyle` - `checkboxStyle`

18
docs/en/framework/ui/angular/data-table-column-extensions.md

@ -15,6 +15,8 @@ Entity prop extension system allows you to add a new column to the data table fo
You will have access to the current entity in your code and display its value, make the column sortable, perform visibility checks, and more. You can also render custom HTML in table cells. You will have access to the current entity in your code and display its value, make the column sortable, perform visibility checks, and more. You can also render custom HTML in table cells.
> **Standalone-first:** Current ABP templates use standalone APIs. The `loadChildren` examples below lazy-load routes from `createRoutes({ ... })` — they do not require NgModules. Legacy NgModule projects can pass the same options to `IdentityModule.forLazy({ ... })` instead. See [ABP Now Supports Angular Standalone Applications](https://abp.io/community/articles/abp-now-supports-angular-standalone-applications-zzi2rr2z).
## How to Set Up ## How to Set Up
In this example, we will add a "Name" column and display the value of the `name` field in the user management page of the [Identity Module](../../../modules/identity.md). In this example, we will add a "Name" column and display the value of the `name` field in the user management page of the [Identity Module](../../../modules/identity.md).
@ -64,7 +66,7 @@ Import `identityEntityPropContributors` in your routing configuration and pass i
```js ```js
// src/app/app.routes.ts // src/app/app.routes.ts
// other imports import { Routes } from '@angular/router';
import { identityEntityPropContributors } from './entity-prop-contributors'; import { identityEntityPropContributors } from './entity-prop-contributors';
export const APP_ROUTES: Routes = [ export const APP_ROUTES: Routes = [
@ -84,6 +86,20 @@ export const APP_ROUTES: Routes = [
]; ];
``` ```
#### Legacy NgModule projects
```js
{
path: 'identity',
loadChildren: () =>
import('@abp/ng.identity').then(m =>
m.IdentityModule.forLazy({
entityPropContributors: identityEntityPropContributors,
}),
),
},
```
That is it, `nameProp` entity prop will be added, and you will see the "Name" column next to the usernames on the grid in the users page (`UsersComponent`) of the `identity` package. That is it, `nameProp` entity prop will be added, and you will see the "Name" column next to the usernames on the grid in the users page (`UsersComponent`) of the `identity` package.
## How to Render Custom HTML in Cells ## How to Render Custom HTML in Cells

19
docs/en/framework/ui/angular/dynamic-form-extensions.md

@ -16,6 +16,8 @@ Form prop extension system allows you to add a new field to the create and/or ed
You can validate the field, perform visibility checks, and do more. You will also have access to the current entity when creating a contributor for an edit form. You can validate the field, perform visibility checks, and do more. You will also have access to the current entity when creating a contributor for an edit form.
> **Standalone-first:** Current ABP templates use standalone APIs. The `loadChildren` examples below lazy-load routes from `createRoutes({ ... })` — they do not require NgModules. Legacy NgModule projects can pass the same options to `IdentityModule.forLazy({ ... })` instead. See [ABP Now Supports Angular Standalone Applications](https://abp.io/community/articles/abp-now-supports-angular-standalone-applications-zzi2rr2z).
## How to Set Up ## How to Set Up
In this example, we will add a "Date of Birth" field in the user management page of the [Identity Module](../../../modules/identity.md) and validate it. In this example, we will add a "Date of Birth" field in the user management page of the [Identity Module](../../../modules/identity.md) and validate it.
@ -69,7 +71,7 @@ Import `identityCreateFormPropContributors` and `identityEditFormPropContributor
```js ```js
// src/app/app.routes.ts // src/app/app.routes.ts
// other imports import { Routes } from '@angular/router';
import { import {
identityCreateFormPropContributors, identityCreateFormPropContributors,
identityEditFormPropContributors, identityEditFormPropContributors,
@ -93,6 +95,21 @@ export const APP_ROUTES: Routes = [
]; ];
``` ```
#### Legacy NgModule projects
```js
{
path: 'identity',
loadChildren: () =>
import('@abp/ng.identity').then(m =>
m.IdentityModule.forLazy({
createFormPropContributors: identityCreateFormPropContributors,
editFormPropContributors: identityEditFormPropContributors,
}),
),
},
```
That is it, `birthdayProp` form prop will be added, and you will see the datepicker for the "Date of Birth" field right before the "Email address" in the forms of the users page in the `identity` package. That is it, `birthdayProp` form prop will be added, and you will see the datepicker for the "Date of Birth" field right before the "Email address" in the forms of the users page in the `identity` package.
## Object Extensions ## Object Extensions

20
docs/en/framework/ui/angular/entity-action-extensions.md

@ -15,6 +15,8 @@ Entity action extension system allows you to add a new action to the action menu
You can take any action (open a modal, make an HTTP API call, redirect to another page... etc) by writing your custom code. You can also access the current entity in your code. You can take any action (open a modal, make an HTTP API call, redirect to another page... etc) by writing your custom code. You can also access the current entity in your code.
> **Standalone-first:** Current ABP templates use standalone APIs. The `loadChildren` examples below lazy-load routes from `createRoutes({ ... })` — they do not require NgModules. Legacy NgModule projects can pass the same options to `IdentityModule.forLazy({ ... })` instead. See [ABP Now Supports Angular Standalone Applications](https://abp.io/community/articles/abp-now-supports-angular-standalone-applications-zzi2rr2z).
## How to Set Up ## How to Set Up
In this example, we will add a "Click Me!" action and alert the current row's `userName` in the user management page of the [Identity Module](../../../modules/identity.md). In this example, we will add a "Click Me!" action and alert the current row's `userName` in the user management page of the [Identity Module](../../../modules/identity.md).
@ -56,12 +58,12 @@ The list of actions, conveniently named as `actionList`, is a **doubly linked li
### Step 2. Import and Use Entity Action Contributors ### Step 2. Import and Use Entity Action Contributors
Import `identityEntityActionContributors` in your routing configuration and pass it to the static `configureRoutes` method for `identity` routes as seen below: Import `identityEntityActionContributors` in your routing configuration and pass it to the static `createRoutes` method for `identity` routes as seen below:
```js ```js
// src/app/app.routes.ts // src/app/app.routes.ts
// other imports import { Routes } from '@angular/router';
import { identityEntityActionContributors } from './entity-action-contributors'; import { identityEntityActionContributors } from './entity-action-contributors';
export const APP_ROUTES: Routes = [ export const APP_ROUTES: Routes = [
@ -81,6 +83,20 @@ export const APP_ROUTES: Routes = [
]; ];
``` ```
#### Legacy NgModule projects
```js
{
path: 'identity',
loadChildren: () =>
import('@abp/ng.identity').then(m =>
m.IdentityModule.forLazy({
entityActionContributors: identityEntityActionContributors,
}),
),
},
```
That is it, `alertUserName` entity action will be added as the last action on the grid dropdown in the "Users" page (`UsersComponent`) of the `identity` package. That is it, `alertUserName` entity action will be added as the last action on the grid dropdown in the "Users" page (`UsersComponent`) of the `identity` package.
## How to Place a Custom Modal and Trigger It by Entity Actions ## How to Place a Custom Modal and Trigger It by Entity Actions

2
docs/en/framework/ui/angular/extensions-overall.md

@ -16,6 +16,8 @@ See the documents below for the details:
* [Page Toolbar Extension](page-toolbar-extensions.md) * [Page Toolbar Extension](page-toolbar-extensions.md)
* [Dynamic Form (or Form Prop) Extensions](dynamic-form-extensions.md) * [Dynamic Form (or Form Prop) Extensions](dynamic-form-extensions.md)
> **Standalone-first:** Current ABP templates use standalone APIs (`app.config.ts`, `app.routes.ts`, and `createRoutes()`). Extension examples register contributors through route-level lazy loading — `loadChildren` returns routes from `createRoutes({ ... })`, not NgModules. Legacy NgModule projects can pass the same contributor options to `SomeModule.forLazy({ ... })` instead. See [ABP Now Supports Angular Standalone Applications](https://abp.io/community/articles/abp-now-supports-angular-standalone-applications-zzi2rr2z) for the `forLazy()` → `createRoutes()` migration.
## Extensible Table Component ## Extensible Table Component
Using [ngx-datatable](https://github.com/swimlane/ngx-datatable) in extensible table. Using [ngx-datatable](https://github.com/swimlane/ngx-datatable) in extensible table.

16
docs/en/framework/ui/angular/feature-libraries.md

@ -9,6 +9,8 @@
ABP has an ever-growing number of feature modules and [introducing a new one](../../architecture/modularity/basics.md) is always possible. When the UI is Angular, these features have modular Angular libraries accompanying them. ABP has an ever-growing number of feature modules and [introducing a new one](../../architecture/modularity/basics.md) is always possible. When the UI is Angular, these features have modular Angular libraries accompanying them.
> **Standalone-first:** Current templates use standalone APIs. Configuration providers such as `provideIdentityConfig()` belong in `app.config.ts`, and features are lazy-loaded via `createRoutes()` in `app.routes.ts`. The `loadChildren` pattern below loads **route definitions**, not NgModules. In legacy NgModule projects, replace `createRoutes()` with `IdentityModule.forLazy()` (or the equivalent `forLazy()` method on the feature module). See [ABP Now Supports Angular Standalone Applications](https://abp.io/community/articles/abp-now-supports-angular-standalone-applications-zzi2rr2z).
## Feature Library Content ## Feature Library Content
Each library has at least two key elements: Each library has at least two key elements:
@ -85,6 +87,20 @@ When you load the identity feature like this, the "Users" page, for example, wil
Depending on the library, the `.createRoutes` static method may also receive some options that configure how the feature works. Depending on the library, the `.createRoutes` static method may also receive some options that configure how the feature works.
#### Legacy NgModule projects
If your application still uses NgModules, lazy-load the feature with `forLazy()` instead:
```js
{
path: "identity",
loadChildren: () =>
import("@abp/ng.identity").then((m) => m.IdentityModule.forLazy()),
},
```
Pass the same options object to `forLazy({ ... })` that you would pass to `createRoutes({ ... })` when configuring extensions or other feature options.
--- ---
<sup id="f-modify-route"><b>1</b></sup> _Libraries expect to work at a predefined path. Please check [how to patch a navigation element](./modifying-the-menu.md#how-to-patch-or-remove-a-navigation-element), if you want to use a different path from the default one (e.g. '/identity')._ <sup>[↩](#a-modify-route)</sup> <sup id="f-modify-route"><b>1</b></sup> _Libraries expect to work at a predefined path. Please check [how to patch a navigation element](./modifying-the-menu.md#how-to-patch-or-remove-a-navigation-element), if you want to use a different path from the default one (e.g. '/identity')._ <sup>[↩](#a-modify-route)</sup>

2
docs/en/framework/ui/angular/features.md

@ -7,7 +7,7 @@
# Features # Features
You can get the value of a feature on the client-side using the [config state service](./config-state.md) if it is allowed by the feature definition on the server-side. You can get the value of a feature on the client-side using the [config state service](./config-state-service.md) if it is allowed by the feature definition on the server-side.
> This document explains how to get feature values in an Angular application. See the [Features document](../../infrastructure/features.md) to learn the feature system. > This document explains how to get feature values in an Angular application. See the [Features document](../../infrastructure/features.md) to learn the feature system.

2
docs/en/framework/ui/angular/how-replaceable-components-work-with-extensions.md

@ -9,6 +9,8 @@
Additional UI extensibility points ([Entity action extensions](../angular/entity-action-extensions.md), [data table column extensions](../angular/data-table-column-extensions.md), [page toolbar extensions](../angular/page-toolbar-extensions.md) and others) are used in ABP pages to allow to control entity actions, table columns and page toolbar of a page. If you replace a page, you need to apply some configurations to be able to work extension components in your component. Let's see how to do this by replacing the roles page. Additional UI extensibility points ([Entity action extensions](../angular/entity-action-extensions.md), [data table column extensions](../angular/data-table-column-extensions.md), [page toolbar extensions](../angular/page-toolbar-extensions.md) and others) are used in ABP pages to allow to control entity actions, table columns and page toolbar of a page. If you replace a page, you need to apply some configurations to be able to work extension components in your component. Let's see how to do this by replacing the roles page.
> **Standalone-first:** The example below uses standalone components with an `imports` array and `inject()`. Current ABP templates follow this pattern. Module-based examples in older docs remain valid for legacy projects — see [ABP Now Supports Angular Standalone Applications](https://abp.io/community/articles/abp-now-supports-angular-standalone-applications-zzi2rr2z).
Create a new component called `MyRolesComponent`: Create a new component called `MyRolesComponent`:
```bash ```bash

2
docs/en/framework/ui/angular/internet-connection-service.md

@ -17,7 +17,7 @@ When you inject the InternetConnectionService you can get the current internet s
# How To Use # How To Use
İt's easy, just inject the service and get the network status. It's easy, just inject the service and get the network status.
**You can get via signal** **You can get via signal**
```ts ```ts

2
docs/en/framework/ui/angular/oauth-module.md

@ -7,7 +7,7 @@
# ABP OAuth Package # ABP OAuth Package
The authentication functionality has been moved from @abp/ng.core to @abp/ng.ouath since v7.0. The authentication functionality has been moved from @abp/ng.core to @abp/ng.oauth since v7.0.
If your app is version 8.3 or higher, you should include "provideAbpOAuth()" after "provideAbpCore()" in the `appConfig` array of your `app.config.ts`. If your app is version 8.3 or higher, you should include "provideAbpOAuth()" after "provideAbpCore()" in the `appConfig` array of your `app.config.ts`.

41
docs/en/framework/ui/angular/page-toolbar-extensions.md

@ -15,6 +15,8 @@ Page toolbar extension system allows you to add a new action to the toolbar of a
You can take any action (open a modal, make an HTTP API call, redirect to another page... etc) by writing your custom code. You can also access to page data (the main record, usually an entity list) in your code. Additionally, you can pass in custom components instead of using the default button. You can take any action (open a modal, make an HTTP API call, redirect to another page... etc) by writing your custom code. You can also access to page data (the main record, usually an entity list) in your code. Additionally, you can pass in custom components instead of using the default button.
> **Standalone-first:** Current ABP templates use standalone APIs. The `loadChildren` examples below lazy-load routes from `createRoutes({ ... })` — they do not require NgModules. Legacy NgModule projects can pass the same options to `IdentityModule.forLazy({ ... })` instead. See [ABP Now Supports Angular Standalone Applications](https://abp.io/community/articles/abp-now-supports-angular-standalone-applications-zzi2rr2z).
## How to Add an Action to Page Toolbar ## How to Add an Action to Page Toolbar
In this example, we will add a "Click Me!" action and log `userName` of all users in the user management page of the [Identity Module](../../../modules/identity.md) to the console. In this example, we will add a "Click Me!" action and log `userName` of all users in the user management page of the [Identity Module](../../../modules/identity.md) to the console.
@ -65,7 +67,7 @@ Import `identityToolbarActionContributors` in your routing configuration and pas
```js ```js
// src/app/app.routes.ts // src/app/app.routes.ts
// other imports import { Routes } from '@angular/router';
import { identityToolbarActionContributors } from './toolbar-action-contributors'; import { identityToolbarActionContributors } from './toolbar-action-contributors';
export const APP_ROUTES: Routes = [ export const APP_ROUTES: Routes = [
@ -85,6 +87,20 @@ export const APP_ROUTES: Routes = [
]; ];
``` ```
#### Legacy NgModule projects
```js
{
path: 'identity',
loadChildren: () =>
import('@abp/ng.identity').then(m =>
m.IdentityModule.forLazy({
toolbarActionContributors: identityToolbarActionContributors,
}),
),
},
```
That is it, `logUserNames` toolbar action will be added as the first action on the page toolbar in the users page (`UsersComponent`) of the `identity` package. That is it, `logUserNames` toolbar action will be added as the first action on the page toolbar in the users page (`UsersComponent`) of the `identity` package.
## How to Add a Custom Component to Page Toolbar ## How to Add a Custom Component to Page Toolbar
@ -100,7 +116,7 @@ We need to have a component before we can pass it to the toolbar action contribu
```js ```js
// src/app/click-me-button.component.ts // src/app/click-me-button.component.ts
import { Component, Inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import { IdentityUserDto } from '@abp/ng.identity/proxy'; import { IdentityUserDto } from '@abp/ng.identity/proxy';
import { ActionData, EXTENSIONS_ACTION_DATA } from '@abp/ng.components/extensible'; import { ActionData, EXTENSIONS_ACTION_DATA } from '@abp/ng.components/extensible';
@ -109,10 +125,7 @@ import { ActionData, EXTENSIONS_ACTION_DATA } from '@abp/ng.components/extensibl
template: `<button class="btn btn-warning" (click)="handleClick()">Click Me!</button>`, template: `<button class="btn btn-warning" (click)="handleClick()">Click Me!</button>`,
}) })
export class ClickMeButtonComponent { export class ClickMeButtonComponent {
constructor( private data = inject<ActionData<IdentityUserDto[]>>(EXTENSIONS_ACTION_DATA);
@Inject(EXTENSIONS_ACTION_DATA)
private data: ActionData<IdentityUserDto[]>
) {}
handleClick() { handleClick() {
this.data.record.forEach(user => console.log(user.userName)); this.data.record.forEach(user => console.log(user.userName));
@ -168,7 +181,7 @@ Import `identityToolbarActionContributors` in your routing configuration and pas
```js ```js
// src/app/app.routes.ts // src/app/app.routes.ts
// other imports import { Routes } from '@angular/router';
import { identityToolbarActionContributors } from './toolbar-action-contributors'; import { identityToolbarActionContributors } from './toolbar-action-contributors';
export const APP_ROUTES: Routes = [ export const APP_ROUTES: Routes = [
@ -188,6 +201,20 @@ export const APP_ROUTES: Routes = [
]; ];
``` ```
#### Legacy NgModule projects
```js
{
path: 'identity',
loadChildren: () =>
import('@abp/ng.identity').then(m =>
m.IdentityModule.forLazy({
toolbarActionContributors: identityToolbarActionContributors,
}),
),
},
```
That is it, `logUserNames` toolbar action will be added as the first action on the page toolbar in the users page (`UsersComponent`) of the `identity` package and it will be triggered by a custom button, i.e. `ClickMeButtonComponent`. Please note that **component projection is not limited to buttons** and you may use other UI components. That is it, `logUserNames` toolbar action will be added as the first action on the page toolbar in the users page (`UsersComponent`) of the `identity` package and it will be triggered by a custom button, i.e. `ClickMeButtonComponent`. Please note that **component projection is not limited to buttons** and you may use other UI components.
## How to Place a Custom Modal and Trigger It by Toolbar Actions ## How to Place a Custom Modal and Trigger It by Toolbar Actions

5
docs/en/framework/ui/angular/quick-start.md

@ -7,7 +7,7 @@
# ABP Angular Quick Start # ABP Angular Quick Start
**In this version ABP uses Angular [21.0.x](https://github.com/angular/angular/tree/21.0.x) version. You don't have to install Angular CLI globally** **In this version ABP uses Angular [21.2.x](https://github.com/angular/angular/tree/21.2.x) version. You don't have to install Angular CLI globally**
## How to Prepare Development Environment ## How to Prepare Development Environment
@ -22,7 +22,6 @@ Please follow the steps below to prepare your development environment for Angula
- [Visual Studio IntelliCode](https://marketplace.visualstudio.com/items?itemName=visualstudioexptteam.vscodeintellicode) - [Visual Studio IntelliCode](https://marketplace.visualstudio.com/items?itemName=visualstudioexptteam.vscodeintellicode)
- [Path Intellisense](https://marketplace.visualstudio.com/items?itemName=christian-kohler.path-intellisense) - [Path Intellisense](https://marketplace.visualstudio.com/items?itemName=christian-kohler.path-intellisense)
- [npm Intellisense](https://marketplace.visualstudio.com/items?itemName=christian-kohler.npm-intellisense) - [npm Intellisense](https://marketplace.visualstudio.com/items?itemName=christian-kohler.npm-intellisense)
- [Angular 10 Snippets - TypeScript, Html, Angular Material, ngRx, RxJS & Flex Layout](https://marketplace.visualstudio.com/items?itemName=Mikael.Angular-BeastCode)
- [JavaScript (ES6) code snippets](https://marketplace.visualstudio.com/items?itemName=xabikos.JavaScriptSnippets) - [JavaScript (ES6) code snippets](https://marketplace.visualstudio.com/items?itemName=xabikos.JavaScriptSnippets)
- [JavaScript Debugger](https://marketplace.visualstudio.com/items?itemName=ms-vscode.js-debug) (built-in, usually pre-installed) - [JavaScript Debugger](https://marketplace.visualstudio.com/items?itemName=ms-vscode.js-debug) (built-in, usually pre-installed)
- [Git History](https://marketplace.visualstudio.com/items?itemName=donjayamanne.githistory) - [Git History](https://marketplace.visualstudio.com/items?itemName=donjayamanne.githistory)
@ -167,7 +166,7 @@ When you run the development server, variables defined in _environment.ts_ take
2. Run `yarn` or `npm install` if you have not installed dependencies already. 2. Run `yarn` or `npm install` if you have not installed dependencies already.
3. Run `yarn build:prod` or `npm run build:prod`. 3. Run `yarn build:prod` or `npm run build:prod`.
<img alt="Angular compiler optimizing the build using Terser" src="./images/quick-start---self-signed-certificate-error.png" width="400px" style="max-width:100%"> <img alt="Browser blocking access to backend API due to self-signed certificate error" src="./images/quick-start---self-signed-certificate-error.png" width="400px" style="max-width:100%">
Depending on project size, the compilation may take a few minutes. When it is finished, the compiled output will be placed inside the _/dist_ folder. Voila! You have deployment-ready build artifacts. Depending on project size, the compilation may take a few minutes. When it is finished, the compiled output will be placed inside the _/dist_ folder. Voila! You have deployment-ready build artifacts.

2
docs/en/framework/ui/angular/settings.md

@ -7,7 +7,7 @@
# Settings # Settings
You can get settings on the client-side using the [config state service](./config-state.md) if they are allowed by their setting definition on the server-side. You can get settings on the client-side using the [config state service](./config-state-service.md) if they are allowed by their setting definition on the server-side.
> This document only explains how settings work in the Angular UI projects. See the [settings document](../../infrastructure/settings.md) to understand the ABP setting system. > This document only explains how settings work in the Angular UI projects. See the [settings document](../../infrastructure/settings.md) to understand the ABP setting system.

411
docs/en/framework/ui/angular/testing.md

@ -1,7 +1,7 @@
```json ```json
//[doc-seo] //[doc-seo]
{ {
"Description": "Learn how to unit test your ABP Angular UI applications with preconfigured Karma and Jasmine, plus ABP-specific testing topics." "Description": "Learn how to unit test your ABP Angular UI applications with preconfigured Vitest and TestBed, plus ABP-specific testing topics."
} }
``` ```
@ -9,89 +9,105 @@
ABP Angular UI is tested like any other Angular application. So, [the guide here](https://angular.dev/guide/testing) applies to ABP too. That said, we would like to point out some **unit testing topics specific to ABP Angular applications**. ABP Angular UI is tested like any other Angular application. So, [the guide here](https://angular.dev/guide/testing) applies to ABP too. That said, we would like to point out some **unit testing topics specific to ABP Angular applications**.
## Setup ## Test Stack
In Angular, unit tests use [Karma](https://karma-runner.github.io/) and [Jasmine](https://jasmine.github.io) by default. Although we like Jest more, we chose not to deviate from these defaults, so **the application template you download will have Karma and Jasmine preconfigured**. You can find the Karma configuration inside the _karma.conf.js_ file in the root folder. You don't have to do anything. Adding a spec file and running `npm test` will work. The application template you download is preconfigured for unit testing. You can add a `*.spec.ts` file and run `yarn test` without adding extra test infrastructure.
## Basics | Package / API | Purpose |
| --- | --- |
| [Vitest](https://vitest.dev/) | Test runner and assertion library. |
| [jsdom](https://github.com/jsdom/jsdom) | Browser-like DOM environment for component tests. |
| `@angular/core/testing` (`TestBed`) | The standard testing utilities of Angular for components, services, and pipes. |
| `@abp/ng.core/testing` | ABP testing module and helpers that replace real ABP services with mocks. |
| `@abp/ng.theme.shared/testing` | Testing module for shared theme features such as validation. |
An over-simplified spec file looks like this: ABP Angular packages in the [framework repository](https://github.com/abpframework/abp/tree/dev/npm/ng-packs) use the same Vitest setup. Library tests there also use [`@ngneat/spectator/vitest`](https://github.com/ngneat/spectator) for HTTP and component tests, but the application template uses `TestBed` directly.
```js ## Configuration
import { CoreTestingModule } from "@abp/ng.core/testing";
import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing";
import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing";
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { NgxValidateCoreModule } from "@ngx-validate/core";
import { MyComponent } from "./my.component";
describe("MyComponent", () => { The test target in _angular.json_ uses Angular's built-in Vitest builder:
let fixture: ComponentFixture<MyComponent>;
beforeEach( ```json
waitForAsync(() => { // angular.json
TestBed.configureTestingModule({
declarations: [MyComponent],
imports: [
CoreTestingModule.withConfig(),
ThemeSharedTestingModule.withConfig(),
ThemeBasicTestingModule.withConfig(),
NgxValidateCoreModule,
],
providers: [
/* mock providers here */
],
}).compileComponents();
})
);
beforeEach(() => { "test": {
fixture = TestBed.createComponent(MyComponent); "builder": "@angular/build:unit-test"
fixture.detectChanges(); }
}); ```
it("should be initiated", () => { Spec files are compiled with _tsconfig.spec.json_, which enables Vitest globals:
expect(fixture.componentInstance).toBeTruthy();
}); ```json
}); // tsconfig.spec.json
{
"compilerOptions": {
"types": ["vitest/globals"]
},
"include": ["src/**/*.spec.ts"]
}
``` ```
If you take a look at the imports, you will notice that we have prepared some testing modules to replace built-in ABP modules. This is necessary for providing mocks for some features which otherwise would break your tests. Please remember to **use testing modules** and **call their `withConfig` static method**. You do not need a _karma.conf.js_ file. Angular CLI wires Vitest and jsdom for you.
## Tips ## Running Tests
### Angular Testing Library Run tests in watch mode:
```bash
yarn test
```
Although you can test your code with Angular TestBed, you may find [Angular Testing Library](https://testing-library.com/docs/angular-testing-library/intro) a good alternative. Run tests once, which is useful for CI:
The simple example above can be written with Angular Testing Library as follows: ```bash
ng test --watch=false
```
Vitest exits with a non-zero status code when a test fails, so the command above works in pipelines.
## Basics
```js An over-simplified spec file looks like this:
```ts
import { CoreTestingModule } from "@abp/ng.core/testing"; import { CoreTestingModule } from "@abp/ng.core/testing";
import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing";
import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing"; import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing";
import { ComponentFixture } from "@angular/core/testing"; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { NgxValidateCoreModule } from "@ngx-validate/core"; import { NgxValidateCoreModule } from "@ngx-validate/core";
import { render } from "@testing-library/angular"; import { AuthService } from "@abp/ng.core";
import { vi } from "vitest";
import { MyComponent } from "./my.component"; import { MyComponent } from "./my.component";
describe("MyComponent", () => { describe("MyComponent", () => {
let fixture: ComponentFixture<MyComponent>; let fixture: ComponentFixture<MyComponent>;
let mockAuthService: { isAuthenticated: boolean; navigateToLogin: ReturnType<typeof vi.fn> };
beforeEach(async () => { beforeEach(async () => {
const result = await render(MyComponent, { mockAuthService = {
isAuthenticated: false,
navigateToLogin: vi.fn(),
};
await TestBed.configureTestingModule({
imports: [ imports: [
CoreTestingModule.withConfig(), CoreTestingModule.withConfig(),
ThemeSharedTestingModule.withConfig(), ThemeSharedTestingModule.withConfig(),
ThemeBasicTestingModule.withConfig(),
NgxValidateCoreModule, NgxValidateCoreModule,
MyComponent,
], ],
providers: [ providers: [
/* mock providers here */ {
provide: AuthService,
useValue: mockAuthService,
},
], ],
}); }).compileComponents();
});
fixture = result.fixture; beforeEach(() => {
fixture = TestBed.createComponent(MyComponent);
fixture.detectChanges();
}); });
it("should be initiated", () => { it("should be initiated", () => {
@ -100,45 +116,36 @@ describe("MyComponent", () => {
}); });
``` ```
Very similar, as you can see. The real difference kicks in when we use queries and fire events. If you take a look at the imports, you will notice that we have prepared some testing modules to replace built-in ABP modules. This is necessary for providing mocks for some features which otherwise would break your tests. Please remember to **use testing modules** and **call their `withConfig` static method**.
```js Current templates use standalone components, so put the component under test in the `imports` array instead of `declarations`.
// other imports
import { getByLabelText, screen } from "@testing-library/angular";
import userEvent from "@testing-library/user-event";
describe("MyComponent", () => { If your application uses `@abp/ng.theme.basic`, also import `ThemeBasicTestingModule.withConfig()` from `@abp/ng.theme.basic/testing`.
beforeEach(/* removed for sake of brevity */);
it("should display advanced filters", () => { ### Mocking Dependencies
const filters = screen.getByTestId("author-filters");
const nameInput = getByLabelText(filters, /name/i) as HTMLInputElement;
expect(nameInput.offsetWidth).toBe(0);
const advancedFiltersBtn = screen.getByRole("link", { name: /advanced/i }); Use Vitest mocks instead of Jasmine spies:
userEvent.click(advancedFiltersBtn);
expect(nameInput.offsetWidth).toBeGreaterThan(0); ```ts
import { vi } from "vitest";
userEvent.type(nameInput, "fooo{backspace}"); const deleteSpy = vi.fn().mockReturnValue(of(null));
expect(nameInput.value).toBe("foo"); fixture.componentInstance.service.delete = deleteSpy;
});
}); expect(deleteSpy).toHaveBeenCalledWith("some-id");
``` ```
The **queries in Angular Testing Library follow practices for maintainable tests**, the user event package provides a **human-like interaction** with the DOM, and the library in general has **a clear API** that simplifies component testing. Please find some useful links below: The template's `home.component.spec.ts` is a good reference for mocking ABP services and asserting DOM behavior with `TestBed`.
- [Queries](https://testing-library.com/docs/dom-testing-library/api-queries) ## Tips
- [User Event](https://testing-library.com/docs/ecosystem-user-event)
- [Examples](https://github.com/testing-library/angular-testing-library/tree/main/apps/example-app/src/app/examples)
### Clearing DOM After Each Spec ### Clearing DOM After Each Spec
One thing to remember is that Karma runs tests in real browser instances. That means, you will be able to see the result of your test code, but also have problems with components attached to the document body which may not get cleared after each test, even when you configure Karma to do so. Tests run in jsdom, not a real browser. Components attached to `document.body` — such as modals, confirmation dialogs, and toasts — may not be removed automatically between specs.
We have prepared a simple function with which you can clear any leftover DOM elements after each test. We have prepared a simple function with which you can clear leftover DOM elements after each test:
```js ```ts
// other imports // other imports
import { clearPage } from "@abp/ng.core/testing"; import { clearPage } from "@abp/ng.core/testing";
@ -147,239 +154,189 @@ describe("MyComponent", () => {
afterEach(() => clearPage(fixture)); afterEach(() => clearPage(fixture));
beforeEach(async () => {
const result = await render(MyComponent, {
/* removed for sake of brevity */
});
fixture = result.fixture;
});
// specs here // specs here
}); });
``` ```
Please make sure you use it because Karma will fail to remove dialogs otherwise and you will have multiple copies of modals, confirmation boxes, and alike. Please use it when you test features that render into the document body. Otherwise you may end up with multiple copies of modals, confirmation boxes, and similar elements.
### Waiting ### Waiting
Some components, modals, in particular, work off-detection-cycle. In other words, you cannot reach DOM elements inserted by these components immediately after opening them. Similarly, inserted elements are not immediately destroyed upon closing them. Some components, modals in particular, work off the change-detection cycle. In other words, you cannot reach DOM elements inserted by these components immediately after opening them. Similarly, inserted elements are not immediately destroyed upon closing them.
For this purpose, we have prepared a `wait` function. For this purpose, we have prepared a `wait` function:
```js ```ts
// other imports // other imports
import { wait } from "@abp/ng.core/testing"; import { wait } from "@abp/ng.core/testing";
describe("MyComponent", () => { describe("MyComponent", () => {
beforeEach(/* removed for sake of brevity */); let fixture: ComponentFixture<MyComponent>;
it("should open a modal", async () => { it("should open a modal", async () => {
const openModalBtn = screen.getByRole("button", { name: "Open Modal" }); const openModalBtn = fixture.nativeElement.querySelector('[role="button"]');
userEvent.click(openModalBtn); openModalBtn.click();
await wait(fixture); await wait(fixture);
const modal = screen.getByRole("dialog"); const modal = fixture.nativeElement.ownerDocument.querySelector('[role="dialog"]');
expect(modal).toBeTruthy(); expect(modal).toBeTruthy();
/* wait again after closing the modal */
}); });
}); });
``` ```
The `wait` function takes a second parameter, i.e. timeout (default: `0`). Try not to use it though. Using a timeout bigger than `0` is usually a signal that something is not quite right. The `wait` function takes a second parameter, i.e. timeout (default: `0`). Try not to use it though. Using a timeout bigger than `0` is usually a signal that something is not quite right.
## Testing Example ### Angular Testing Library
Although you can test your code with Angular TestBed, you may find [Angular Testing Library](https://testing-library.com/docs/angular-testing-library/intro) a good alternative. It is not included in the application template by default, but you can add `@testing-library/angular` and `@testing-library/user-event` if you prefer that style.
Here is an example test suite. It doesn't cover all, but gives quite a good idea about what the testing experience will be like. The ABP testing modules work the same way with Testing Library:
```js ```ts
import { clearPage, CoreTestingModule, wait } from "@abp/ng.core/testing"; import { CoreTestingModule } from "@abp/ng.core/testing";
import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing";
import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing"; import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing";
import { ComponentFixture } from "@angular/core/testing"; import { ComponentFixture } from "@angular/core/testing";
import {
NgbCollapseModule,
NgbDatepickerModule,
NgbDropdownModule,
} from "@ng-bootstrap/ng-bootstrap";
import { NgxValidateCoreModule } from "@ngx-validate/core"; import { NgxValidateCoreModule } from "@ngx-validate/core";
import { CountryService } from "@proxy/countries"; import { render, screen } from "@testing-library/angular";
import { import { MyComponent } from "./my.component";
findByText,
getByLabelText,
getByRole,
getByText,
queryByRole,
render,
screen,
} from "@testing-library/angular";
import userEvent from "@testing-library/user-event";
import { BehaviorSubject, of } from "rxjs";
import { CountryComponent } from "./country.component";
const list$ = new BehaviorSubject({
items: [{ id: "ID_US", name: "United States of America" }],
totalCount: 1,
});
describe("Country", () => {
let fixture: ComponentFixture<CountryComponent>;
afterEach(() => clearPage(fixture)); describe("MyComponent", () => {
let fixture: ComponentFixture<MyComponent>;
beforeEach(async () => { beforeEach(async () => {
const result = await render(CountryComponent, { const result = await render(MyComponent, {
imports: [ imports: [
CoreTestingModule.withConfig(), CoreTestingModule.withConfig(),
ThemeSharedTestingModule.withConfig(), ThemeSharedTestingModule.withConfig(),
ThemeBasicTestingModule.withConfig(),
NgxValidateCoreModule, NgxValidateCoreModule,
NgbCollapseModule,
NgbDatepickerModule,
NgbDropdownModule,
], ],
providers: [ providers: [
{ /* mock providers here */
provide: CountryService,
useValue: {
getList: () => list$,
},
},
], ],
}); });
fixture = result.fixture; fixture = result.fixture;
}); });
it("should display advanced filters", () => { it("should be initiated", () => {
const filters = screen.getByTestId("country-filters"); expect(fixture.componentInstance).toBeTruthy();
const nameInput = getByLabelText(filters, /name/i) as HTMLInputElement;
expect(nameInput.offsetWidth).toBe(0);
const advancedFiltersBtn = screen.getByRole("link", { name: /advanced/i });
userEvent.click(advancedFiltersBtn);
expect(nameInput.offsetWidth).toBeGreaterThan(0);
userEvent.type(nameInput, "fooo{backspace}");
expect(nameInput.value).toBe("foo");
userEvent.click(advancedFiltersBtn);
expect(nameInput.offsetWidth).toBe(0);
});
it("should have a heading", () => {
const heading = screen.getByRole("heading", { name: "Countries" });
expect(heading).toBeTruthy();
}); });
});
```
it("should render list in table", async () => { The **queries in Angular Testing Library follow practices for maintainable tests**, the user event package provides a **human-like interaction** with the DOM, and the library in general has **a clear API** that simplifies component testing. Please find some useful links below:
const table = await screen.findByTestId("country-table");
const name = getByText(table, "United States of America"); - [Queries](https://testing-library.com/docs/dom-testing-library/api-queries)
expect(name).toBeTruthy(); - [User Event](https://testing-library.com/docs/ecosystem-user-event)
}); - [Examples](https://github.com/testing-library/angular-testing-library/tree/main/apps/example-app/src/app/examples)
it("should display edit modal", async () => { When you use Testing Library with modals or confirmation dialogs, combine it with `clearPage` and `wait` from `@abp/ng.core/testing` as shown above.
const actionsBtn = screen.queryByRole("button", { name: /actions/i });
userEvent.click(actionsBtn);
const editBtn = screen.getByRole("button", { name: /edit/i }); ## Testing Example
userEvent.click(editBtn);
await wait(fixture); Here is an example based on the application template's `home.component.spec.ts`. It shows how to mock an ABP service and assert component state and DOM output:
const modal = screen.getByRole("dialog"); ```ts
const modalHeading = queryByRole(modal, "heading", { name: /edit/i }); import { CoreTestingModule } from "@abp/ng.core/testing";
expect(modalHeading).toBeTruthy(); import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { NgxValidateCoreModule } from "@ngx-validate/core";
import { AuthService } from "@abp/ng.core";
import { vi } from "vitest";
import { HomeComponent } from "./home.component";
const closeBtn = getByText(modal, "×"); describe("HomeComponent", () => {
userEvent.click(closeBtn); let fixture: ComponentFixture<HomeComponent>;
let mockAuthService: { isAuthenticated: boolean; navigateToLogin: ReturnType<typeof vi.fn> };
await wait(fixture); beforeEach(async () => {
mockAuthService = {
isAuthenticated: false,
navigateToLogin: vi.fn(),
};
expect(screen.queryByRole("dialog")).toBeFalsy(); await TestBed.configureTestingModule({
imports: [
CoreTestingModule.withConfig(),
ThemeSharedTestingModule.withConfig(),
NgxValidateCoreModule,
HomeComponent,
],
providers: [
{
provide: AuthService,
useValue: mockAuthService,
},
],
}).compileComponents();
}); });
it("should display create modal", async () => { it("should be initiated", () => {
const newBtn = screen.getByRole("button", { name: /new/i }); fixture = TestBed.createComponent(HomeComponent);
userEvent.click(newBtn); fixture.detectChanges();
expect(fixture.componentInstance).toBeTruthy();
await wait(fixture);
const modal = screen.getByRole("dialog");
const modalHeading = queryByRole(modal, "heading", { name: /new/i });
expect(modalHeading).toBeTruthy();
}); });
it("should validate required name field", async () => { describe("when login state is false", () => {
const newBtn = screen.getByRole("button", { name: /new/i }); beforeEach(() => {
userEvent.click(newBtn); mockAuthService.isAuthenticated = false;
fixture = TestBed.createComponent(HomeComponent);
fixture.detectChanges();
});
await wait(fixture); it("hasLoggedIn should be false", () => {
expect(fixture.componentInstance.hasLoggedIn).toBe(false);
});
const modal = screen.getByRole("dialog"); it("button should exist", () => {
const nameInput = getByRole(modal, "textbox", { const button = fixture.nativeElement.querySelector('[role="button"]');
name: /^name/i, expect(button).toBeDefined();
}) as HTMLInputElement; });
userEvent.type(nameInput, "x"); describe("when button clicked", () => {
userEvent.type(nameInput, "{backspace}"); beforeEach(() => {
const button = fixture.nativeElement.querySelector('[role="button"]');
button.click();
});
const nameError = await findByText(modal, /required/i); it("navigateToLogin should have been called", () => {
expect(nameError).toBeTruthy(); expect(mockAuthService.navigateToLogin).toHaveBeenCalled();
});
});
}); });
});
```
it("should delete a country", () => { For list pages with modals, confirmations, and service proxies, keep using the ABP testing modules, mock your generated proxy services with `vi.fn()`, and use `clearPage` / `wait` when body-level UI is involved.
const getSpy = spyOn(fixture.componentInstance.list, "get");
const deleteSpy = jasmine.createSpy().and.returnValue(of(null));
fixture.componentInstance.service.delete = deleteSpy;
const actionsBtn = screen.queryByRole("button", { name: /actions/i });
userEvent.click(actionsBtn);
const deleteBtn = screen.getByRole("button", { name: /delete/i });
userEvent.click(deleteBtn);
const confirmText = screen.getByText("AreYouSure"); ## CI Configuration
expect(confirmText).toBeTruthy();
const confirmBtn = screen.getByRole("button", { name: "Yes" }); Run unit tests once in CI with:
userEvent.click(confirmBtn);
expect(deleteSpy).toHaveBeenCalledWith(list$.value.items[0].id); ```sh
expect(getSpy).toHaveBeenCalledTimes(1); ng test --watch=false
});
});
``` ```
## CI Configuration If you need a dedicated CI configuration, add one under the `test` target in _angular.json_:
You would need a different configuration for your CI environment. To set up a new configuration for your unit tests, find the test project in _angular.json_ file and add one as seen below:
```json ```json
// angular.json // angular.json
"test": { "test": {
"builder": "@angular-devkit/build-angular:karma", "builder": "@angular/build:unit-test",
"options": { /* several options here */ },
"configurations": { "configurations": {
"production": { "ci": {
"karmaConfig": "karma.conf.prod.js" "watch": false
} }
} }
} }
``` ```
Now you can copy the _karma.conf.js_ as _karma.conf.prod.js_ and use any configuration you like in it. Please check [Karma configuration file document](http://karma-runner.github.io/5.2/config/configuration-file.html) for config options. Then run:
Finally, don't forget to run your CI tests with the following command:
```sh ```sh
npm test -- --prod ng test --configuration=ci
``` ```
## See Also ## See Also

Loading…
Cancel
Save