mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
feat: enhance filter input mappings with detailed documentation and schema validation
- Added comprehensive JSDoc comments to mapping functions for checkbox and text filter inputs, improving code readability and maintainability. - Refactored checkboxFilterInputMapping and checkboxOptionMapping functions to enhance clarity and structure. - Removed unused data-range-filter-input mapping files and tests to streamline the codebase. - Introduced a new dateRangeFilterInputMapping function with detailed mapping logic for date range inputs. - Updated filter input schemas to include descriptive comments for better understanding of properties. - Implemented unit tests for date range filter input mapping to ensure correct functionality. - Enhanced existing filter mapping functions with improved error handling and validation. - Updated index exports to reflect the removal and addition of mapping files.
This commit is contained in:
@@ -9,12 +9,24 @@
|
||||
- Mock external dependencies to isolate the unit under test
|
||||
- Mock child components to ensure true unit testing isolation
|
||||
|
||||
## Spectator Overview
|
||||
|
||||
Spectator is a powerful library that simplifies Angular testing by:
|
||||
|
||||
- Reducing boilerplate code in tests
|
||||
- Providing easy DOM querying utilities
|
||||
- Offering a clean API for triggering events
|
||||
- Supporting testing of components, directives, and services
|
||||
- Including custom matchers for clearer assertions
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Component Testing
|
||||
|
||||
- Use `createComponentFactory` for standalone components
|
||||
- Use `createHostFactory` when testing components with templates
|
||||
- Use `createHostFactory` when testing components with templates and inputs/outputs
|
||||
- Use `createDirectiveFactory` for testing directives
|
||||
- Use `createServiceFactory` for testing services
|
||||
- Test component inputs, outputs, and lifecycle hooks
|
||||
- Verify DOM rendering and component behavior separately
|
||||
|
||||
@@ -39,6 +51,143 @@ describe('ParentComponent', () => {
|
||||
});
|
||||
```
|
||||
|
||||
## Spectator API Reference
|
||||
|
||||
### Core Factory Methods
|
||||
|
||||
1. **For Components**:
|
||||
|
||||
```typescript
|
||||
const createComponent = createComponentFactory({
|
||||
component: MyComponent,
|
||||
imports: [SomeModule],
|
||||
declarations: [SomeDirective],
|
||||
providers: [SomeService],
|
||||
componentProviders: [], // Providers specific to the component
|
||||
componentViewProviders: [], // ViewProviders for the component
|
||||
mocks: [ServiceToMock], // Automatically mocks the service
|
||||
detectChanges: false, // Whether to run change detection initially
|
||||
});
|
||||
```
|
||||
|
||||
2. **For Components with Host**:
|
||||
|
||||
```typescript
|
||||
const createHost = createHostFactory({
|
||||
component: MyComponent,
|
||||
template: `<app-my [prop]="value" (event)="handle()"></app-my>`,
|
||||
// ...other options similar to createComponentFactory
|
||||
});
|
||||
```
|
||||
|
||||
3. **For Directives**:
|
||||
|
||||
```typescript
|
||||
const createDirective = createDirectiveFactory({
|
||||
directive: MyDirective,
|
||||
template: `<div myDirective [prop]="value"></div>`,
|
||||
// ...other options
|
||||
});
|
||||
```
|
||||
|
||||
4. **For Services**:
|
||||
|
||||
```typescript
|
||||
const createService = createServiceFactory({
|
||||
service: MyService,
|
||||
providers: [DependencyService],
|
||||
mocks: [HttpClient],
|
||||
entryComponents: [],
|
||||
});
|
||||
```
|
||||
|
||||
5. **For HTTP Services**:
|
||||
```typescript
|
||||
const createHttpService = createHttpFactory({
|
||||
service: MyHttpService,
|
||||
providers: [SomeService],
|
||||
mocks: [TokenService],
|
||||
});
|
||||
```
|
||||
|
||||
### Querying Elements
|
||||
|
||||
Spectator offers multiple ways to query the DOM:
|
||||
|
||||
```typescript
|
||||
// Basic CSS selectors
|
||||
const button = spectator.query('button.submit');
|
||||
const inputs = spectator.queryAll('input');
|
||||
|
||||
// By directive/component type
|
||||
const childComponent = spectator.query(ChildComponent);
|
||||
const directives = spectator.queryAll(MyDirective);
|
||||
|
||||
// Advanced text-based selectors
|
||||
spectator.query(byText('Submit'));
|
||||
spectator.query(byLabel('Username'));
|
||||
spectator.query(byPlaceholder('Enter your email'));
|
||||
spectator.query(byValue('Some value'));
|
||||
spectator.query(byTitle('Click here'));
|
||||
spectator.query(byAltText('Logo image'));
|
||||
spectator.query(byRole('button', { pressed: true }));
|
||||
|
||||
// Accessing native elements
|
||||
const { nativeElement } = spectator.query('.class-name');
|
||||
```
|
||||
|
||||
### Working with Inputs and Outputs
|
||||
|
||||
```typescript
|
||||
// Setting component inputs
|
||||
spectator.setInput('username', 'JohnDoe');
|
||||
spectator.setInput({
|
||||
username: 'JohnDoe',
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
// For host components
|
||||
spectator.setHostInput('propName', value);
|
||||
|
||||
// Working with outputs
|
||||
const outputSpy = jest.fn();
|
||||
spectator.output('statusChange').subscribe(outputSpy);
|
||||
|
||||
// Trigger and verify outputs
|
||||
spectator.click('button.submit');
|
||||
expect(outputSpy).toHaveBeenCalledWith({ status: 'submitted' });
|
||||
```
|
||||
|
||||
### Event Triggering API
|
||||
|
||||
Spectator provides a rich API for simulating user interactions:
|
||||
|
||||
```typescript
|
||||
// Mouse events
|
||||
spectator.click('.button');
|
||||
spectator.doubleClick('#item');
|
||||
spectator.hover('.tooltip');
|
||||
spectator.mouseEnter('.dropdown');
|
||||
spectator.mouseLeave('.dropdown');
|
||||
|
||||
// Keyboard events
|
||||
spectator.keyboard.pressEscape();
|
||||
spectator.keyboard.pressEnter();
|
||||
spectator.keyboard.pressKey('A');
|
||||
spectator.keyboard.pressKeys('ctrl.a');
|
||||
|
||||
// Form interactions
|
||||
spectator.typeInElement('New value', 'input.username');
|
||||
spectator.blur('input.username');
|
||||
spectator.focus('input.password');
|
||||
spectator.selectOption(selectEl, 'Option 2');
|
||||
|
||||
// Custom events
|
||||
spectator.triggerEventHandler(MyComponent, 'customEvent', eventObj);
|
||||
spectator.dispatchFakeEvent(element, 'mouseover');
|
||||
spectator.dispatchTouchEvent(element, 'touchstart');
|
||||
```
|
||||
|
||||
## Example Test Structures
|
||||
|
||||
### Basic Component Test
|
||||
@@ -128,6 +277,190 @@ it('should emit when button is clicked', () => {
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Services
|
||||
|
||||
```typescript
|
||||
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
|
||||
import { UserService } from './user.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
describe('UserService', () => {
|
||||
let spectator: SpectatorService<UserService>;
|
||||
let httpClient: HttpClient;
|
||||
|
||||
const createService = createServiceFactory({
|
||||
service: UserService,
|
||||
mocks: [HttpClient],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createService();
|
||||
httpClient = spectator.inject(HttpClient);
|
||||
});
|
||||
|
||||
it('should fetch users', () => {
|
||||
// Arrange
|
||||
const mockUsers = [{ id: 1, name: 'John' }];
|
||||
httpClient.get.mockReturnValue(of(mockUsers));
|
||||
|
||||
// Act
|
||||
let result;
|
||||
spectator.service.getUsers().subscribe((users) => {
|
||||
result = users;
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(httpClient.get).toHaveBeenCalledWith('/api/users');
|
||||
expect(result).toEqual(mockUsers);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Testing HTTP Services
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createHttpFactory,
|
||||
HttpMethod,
|
||||
SpectatorHttp,
|
||||
} from '@ngneat/spectator/jest';
|
||||
import { UserHttpService } from './user-http.service';
|
||||
|
||||
describe('UserHttpService', () => {
|
||||
let spectator: SpectatorHttp<UserHttpService>;
|
||||
|
||||
const createHttp = createHttpFactory({
|
||||
service: UserHttpService,
|
||||
});
|
||||
|
||||
beforeEach(() => (spectator = createHttp()));
|
||||
|
||||
it('should call the correct API endpoint when getting users', () => {
|
||||
spectator.service.getUsers().subscribe();
|
||||
spectator.expectOne('/api/users', HttpMethod.GET);
|
||||
});
|
||||
|
||||
it('should include auth token in the headers', () => {
|
||||
spectator.service.getUsers().subscribe();
|
||||
const req = spectator.expectOne('/api/users', HttpMethod.GET);
|
||||
expect(req.request.headers.get('Authorization')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Directives
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createDirectiveFactory,
|
||||
SpectatorDirective,
|
||||
} from '@ngneat/spectator/jest';
|
||||
import { HighlightDirective } from './highlight.directive';
|
||||
|
||||
describe('HighlightDirective', () => {
|
||||
let spectator: SpectatorDirective<HighlightDirective>;
|
||||
|
||||
const createDirective = createDirectiveFactory({
|
||||
directive: HighlightDirective,
|
||||
template: `<div highlight="yellow">Testing</div>`,
|
||||
});
|
||||
|
||||
beforeEach(() => (spectator = createDirective()));
|
||||
|
||||
it('should change the background color', () => {
|
||||
expect(spectator.element).toHaveStyle({
|
||||
backgroundColor: 'yellow',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond to mouse events', () => {
|
||||
spectator.dispatchMouseEvent(spectator.element, 'mouseover');
|
||||
expect(spectator.element).toHaveStyle({
|
||||
backgroundColor: 'yellow',
|
||||
fontWeight: 'bold',
|
||||
});
|
||||
|
||||
spectator.dispatchMouseEvent(spectator.element, 'mouseout');
|
||||
expect(spectator.element).toHaveStyle({
|
||||
backgroundColor: 'yellow',
|
||||
fontWeight: 'normal',
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Angular Standalone Components
|
||||
|
||||
```typescript
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
import { StandaloneComponent } from './standalone.component';
|
||||
|
||||
describe('StandaloneComponent', () => {
|
||||
let spectator: Spectator<StandaloneComponent>;
|
||||
|
||||
const createComponent = createComponentFactory({
|
||||
component: StandaloneComponent,
|
||||
// No need for imports as they are part of the component itself
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
});
|
||||
|
||||
it('should create standalone component', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Deferrable Views (@defer)
|
||||
|
||||
```typescript
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
import { ComponentWithDefer } from './component-with-defer.component';
|
||||
|
||||
describe('ComponentWithDefer', () => {
|
||||
let spectator: Spectator<ComponentWithDefer>;
|
||||
|
||||
const createComponent = createComponentFactory({
|
||||
component: ComponentWithDefer,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
});
|
||||
|
||||
it('should render defer block content', () => {
|
||||
// Render the completed state of the first defer block
|
||||
spectator.deferBlock().renderComplete();
|
||||
|
||||
expect(spectator.query('.deferred-content')).toExist();
|
||||
expect(spectator.query('.placeholder-content')).not.toExist();
|
||||
});
|
||||
|
||||
it('should show loading state', () => {
|
||||
// Render the loading state of the first defer block
|
||||
spectator.deferBlock().renderLoading();
|
||||
|
||||
expect(spectator.query('.loading-indicator')).toExist();
|
||||
});
|
||||
|
||||
it('should show placeholder content', () => {
|
||||
// Render the placeholder state of the first defer block
|
||||
spectator.deferBlock().renderPlaceholder();
|
||||
|
||||
expect(spectator.query('.placeholder-content')).toExist();
|
||||
});
|
||||
|
||||
it('should work with multiple defer blocks', () => {
|
||||
// For the second defer block in the template
|
||||
spectator.deferBlock(1).renderComplete();
|
||||
|
||||
expect(spectator.query('.second-deferred-content')).toExist();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Query Elements
|
||||
@@ -157,6 +490,32 @@ spectator.typeInElement('value', 'input');
|
||||
spectator.triggerEventHandler(MyComponent, 'eventName', eventValue);
|
||||
```
|
||||
|
||||
### Custom Matchers
|
||||
|
||||
Spectator provides custom matchers to make assertions more readable:
|
||||
|
||||
```typescript
|
||||
// DOM matchers
|
||||
expect('.button').toExist();
|
||||
expect('.inactive-element').not.toExist();
|
||||
expect('.title').toHaveText('Welcome');
|
||||
expect('.username').toContainText('John');
|
||||
expect('input').toHaveValue('test');
|
||||
expect('.error').toHaveClass('visible');
|
||||
expect('button').toBeDisabled();
|
||||
expect('div').toHaveAttribute('aria-label', 'Close');
|
||||
expect('.menu').toHaveData({ testId: 'main-menu' });
|
||||
expect('img').toHaveProperty('src', 'path/to/image.jpg');
|
||||
expect('.parent').toHaveDescendant('.child');
|
||||
expect('.parent').toHaveDescendantWithText({
|
||||
selector: '.child',
|
||||
text: 'Child text',
|
||||
});
|
||||
|
||||
// Object matchers
|
||||
expect(object).toBePartial({ id: 1 });
|
||||
```
|
||||
|
||||
### Test Async Operations
|
||||
|
||||
```typescript
|
||||
@@ -189,6 +548,264 @@ it('should handle async operations', async () => {
|
||||
- Remember to clean up subscriptions
|
||||
|
||||
3. **Performance**
|
||||
|
||||
- Mock heavy dependencies
|
||||
- Keep test setup minimal
|
||||
- Use `beforeAll` for expensive operations shared across tests
|
||||
|
||||
4. **Change Detection**
|
||||
|
||||
- Use `spectator.detectChanges()` after modifying component properties
|
||||
- For OnPush components with a host, use `spectator.detectComponentChanges()`
|
||||
|
||||
5. **Injection**
|
||||
- Use `spectator.inject(Service)` to access injected services
|
||||
- Use `spectator.inject(Service, true)` to get service from the component injector
|
||||
|
||||
## Running Tests
|
||||
|
||||
When working in an Nx workspace, there are several ways to run tests:
|
||||
|
||||
### Running Tests for a Specific Project
|
||||
|
||||
```bash
|
||||
# Run all tests for a specific project
|
||||
npx nx test <project-name>
|
||||
|
||||
# Example: Run tests for the core/config library
|
||||
npx nx test core-config
|
||||
```
|
||||
|
||||
### Running Tests with Watch Mode
|
||||
|
||||
```bash
|
||||
# Run tests in watch mode for active development
|
||||
npx nx test <project-name> --watch
|
||||
```
|
||||
|
||||
### Running Tests with Coverage
|
||||
|
||||
```bash
|
||||
# Run tests with coverage reporting
|
||||
npx nx test <project-name> --code-coverage
|
||||
```
|
||||
|
||||
### Running a Specific Test File
|
||||
|
||||
```bash
|
||||
# Run a specific test file
|
||||
npx nx test <project-name> --test-file=path/to/your.spec.ts
|
||||
```
|
||||
|
||||
### Running Affected Tests
|
||||
|
||||
```bash
|
||||
# Run tests only for projects affected by recent changes
|
||||
npx nx affected:test
|
||||
```
|
||||
|
||||
These commands help you target exactly which tests to run, making your testing workflow more efficient.
|
||||
|
||||
## References
|
||||
|
||||
- [Spectator Documentation](https://github.com/ngneat/spectator) - Official documentation for the Spectator testing library
|
||||
- [Jest Documentation](https://jestjs.io/docs/getting-started) - Comprehensive guide to using Jest as a testing framework
|
||||
- [ng-mocks Documentation](https://ng-mocks.sudo.eu/) - Detailed documentation on mocking Angular dependencies effectively
|
||||
|
||||
## ng-mocks Guide
|
||||
|
||||
### Overview
|
||||
|
||||
ng-mocks is a powerful library that helps with Angular testing by:
|
||||
|
||||
- Mocking Components, Directives, Pipes, Modules, Services, and Tokens
|
||||
- Reducing boilerplate in tests
|
||||
- Providing a simple interface to access declarations
|
||||
|
||||
It's particularly useful for isolating components by mocking their dependencies, which makes tests faster and more reliable.
|
||||
|
||||
### Global Configuration
|
||||
|
||||
For optimal setup, configure ng-mocks in your test setup file:
|
||||
|
||||
```typescript
|
||||
// src/test-setup.ts or equivalent
|
||||
import { ngMocks } from 'ng-mocks';
|
||||
|
||||
// Auto-spy all methods in mock declarations and providers
|
||||
ngMocks.autoSpy('jest'); // or 'jasmine'
|
||||
|
||||
// Reset customizations after each test automatically
|
||||
ngMocks.defaultMock(AuthService, () => ({
|
||||
isLoggedIn$: EMPTY,
|
||||
currentUser$: EMPTY,
|
||||
}));
|
||||
```
|
||||
|
||||
### Key APIs
|
||||
|
||||
#### MockBuilder
|
||||
|
||||
`MockBuilder` provides a fluent API to configure TestBed with mocks:
|
||||
|
||||
```typescript
|
||||
beforeEach(() => {
|
||||
return MockBuilder(
|
||||
ComponentUnderTest, // Keep this as real
|
||||
ParentModule, // Mock everything else
|
||||
)
|
||||
.keep(ReactiveFormsModule) // Keep this as real
|
||||
.mock(SomeOtherDependency, { customConfig: true }); // Custom mock
|
||||
});
|
||||
```
|
||||
|
||||
#### MockRender
|
||||
|
||||
`MockRender` is an enhanced version of `TestBed.createComponent` that:
|
||||
|
||||
- Respects all lifecycle hooks
|
||||
- Handles OnPush change detection
|
||||
- Creates a wrapper component that binds inputs and outputs
|
||||
|
||||
```typescript
|
||||
// Simple rendering
|
||||
const fixture = MockRender(ComponentUnderTest);
|
||||
|
||||
// With inputs
|
||||
const fixture = MockRender(ComponentUnderTest, {
|
||||
name: 'Test User',
|
||||
id: 123,
|
||||
});
|
||||
|
||||
// Access the component instance
|
||||
const component = fixture.point.componentInstance;
|
||||
```
|
||||
|
||||
#### MockInstance
|
||||
|
||||
`MockInstance` helps configure mocks before they're initialized:
|
||||
|
||||
```typescript
|
||||
// Adding a spy
|
||||
const saveSpy = MockInstance(StorageService, 'save', jest.fn());
|
||||
|
||||
// Verify the spy was called
|
||||
expect(saveSpy).toHaveBeenCalledWith(expectedData);
|
||||
```
|
||||
|
||||
#### ngMocks Helpers
|
||||
|
||||
The library provides several helper functions:
|
||||
|
||||
```typescript
|
||||
// Change form control values
|
||||
ngMocks.change('[name=email]', 'test@example.com');
|
||||
|
||||
// Trigger events
|
||||
ngMocks.trigger(element, 'click');
|
||||
ngMocks.trigger(element, 'keyup.control.s'); // Complex events
|
||||
|
||||
// Find elements
|
||||
const emailField = ngMocks.find('[name=email]');
|
||||
const submitBtn = ngMocks.findAll('button[type="submit"]');
|
||||
```
|
||||
|
||||
### Complete Example
|
||||
|
||||
Here's a full example of testing a component with ng-mocks:
|
||||
|
||||
```typescript
|
||||
describe('ProfileComponent', () => {
|
||||
// Reset customizations after each test
|
||||
MockInstance.scope();
|
||||
|
||||
beforeEach(() => {
|
||||
return MockBuilder(ProfileComponent, ProfileModule).keep(
|
||||
ReactiveFormsModule,
|
||||
);
|
||||
});
|
||||
|
||||
it('saves profile data on ctrl+s hotkey', () => {
|
||||
// Prepare test data
|
||||
const profile = {
|
||||
email: 'test@email.com',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
};
|
||||
|
||||
// Mock service method
|
||||
const saveSpy = MockInstance(StorageService, 'save', jest.fn());
|
||||
|
||||
// Render with inputs
|
||||
const { point } = MockRender(ProfileComponent, { profile });
|
||||
|
||||
// Change form value
|
||||
ngMocks.change('[name=email]', 'updated@email.com');
|
||||
|
||||
// Trigger hotkey
|
||||
ngMocks.trigger(point, 'keyup.control.s');
|
||||
|
||||
// Verify behavior
|
||||
expect(saveSpy).toHaveBeenCalledWith({
|
||||
email: 'updated@email.com',
|
||||
firstName: profile.firstName,
|
||||
lastName: profile.lastName,
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Integration with Spectator
|
||||
|
||||
While Spectator and ng-mocks have some overlapping functionality, they can be used together effectively:
|
||||
|
||||
```typescript
|
||||
describe('CombinedExample', () => {
|
||||
let spectator: Spectator<MyComponent>;
|
||||
|
||||
const createComponent = createComponentFactory({
|
||||
component: MyComponent,
|
||||
declarations: [
|
||||
// Use ng-mocks to mock child components
|
||||
MockComponent(ComplexChildComponent),
|
||||
MockDirective(ComplexDirective),
|
||||
],
|
||||
providers: [
|
||||
// Use ng-mocks to mock a service with default behavior
|
||||
MockProvider(ComplexService),
|
||||
],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Configure a mock instance before the component is created
|
||||
MockInstance(
|
||||
ComplexService,
|
||||
'getData',
|
||||
jest.fn().mockReturnValue(of(['test'])),
|
||||
);
|
||||
|
||||
// Create the component with Spectator
|
||||
spectator = createComponent();
|
||||
});
|
||||
|
||||
it('should work with mocked dependencies', () => {
|
||||
// Use Spectator for interactions
|
||||
spectator.click('button');
|
||||
|
||||
// Use ng-mocks to verify interactions with mocked dependencies
|
||||
const service = ngMocks.get(ComplexService);
|
||||
expect(service.getData).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### When to Use ng-mocks
|
||||
|
||||
ng-mocks is particularly useful when:
|
||||
|
||||
1. You need to mock complex Angular artifacts like components, directives, or modules
|
||||
2. You want to customize mock behavior at the instance level
|
||||
3. You need to simulate complex user interactions
|
||||
4. You're testing parent components that depend on multiple child components
|
||||
|
||||
For more details and advanced usage, refer to the [official ng-mocks documentation](https://ng-mocks.sudo.eu/).
|
||||
|
||||
Reference in New Issue
Block a user