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:
Lorenz Hilpert
2025-04-11 16:13:11 +02:00
parent 8144253a18
commit aff6d18888
29 changed files with 1224 additions and 86 deletions

View File

@@ -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/).