diff --git a/docs/guidelines/testing.md b/docs/guidelines/testing.md
index 1dd0e665c..6a8079a1b 100644
--- a/docs/guidelines/testing.md
+++ b/docs/guidelines/testing.md
@@ -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: ``,
+ // ...other options similar to createComponentFactory
+ });
+ ```
+
+3. **For Directives**:
+
+ ```typescript
+ const createDirective = createDirectiveFactory({
+ directive: MyDirective,
+ template: `
`,
+ // ...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;
+ 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;
+
+ 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;
+
+ const createDirective = createDirectiveFactory({
+ directive: HighlightDirective,
+ template: `Testing
`,
+ });
+
+ 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;
+
+ 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;
+
+ 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
+
+# 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 --watch
+```
+
+### Running Tests with Coverage
+
+```bash
+# Run tests with coverage reporting
+npx nx test --code-coverage
+```
+
+### Running a Specific Test File
+
+```bash
+# Run a specific test file
+npx nx test --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;
+
+ 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/).
diff --git a/libs/shared/filter/src/lib/actions/filter-actions.component.ts b/libs/shared/filter/src/lib/actions/filter-actions.component.ts
index 9f382491d..9ed89171b 100644
--- a/libs/shared/filter/src/lib/actions/filter-actions.component.ts
+++ b/libs/shared/filter/src/lib/actions/filter-actions.component.ts
@@ -10,6 +10,21 @@ import {
import { ButtonComponent } from '@isa/ui/buttons';
import { FilterService } from '../core';
+/**
+ * A standalone component that manages filter action buttons (apply/reset)
+ *
+ * This component provides UI controls to apply or reset filter values
+ * within the filtering system. It communicates with the FilterService
+ * to perform filter operations.
+ *
+ * @example
+ *
+ *
+ */
@Component({
selector: 'filter-actions',
templateUrl: './filter-actions.component.html',
@@ -23,20 +38,44 @@ import { FilterService } from '../core';
},
})
export class FilterActionsComponent {
+ /** The filter service used to interact with the filter system */
readonly filterService = inject(FilterService);
+ /**
+ * Optional key specifying which filter input to apply/reset
+ * If not provided, all filter inputs will be affected
+ */
inputKey = input();
+ /**
+ * Controls whether the Apply button should be displayed
+ * @default true
+ */
canApply = input(true);
+ /**
+ * Computed signal that filters inputs to only include those with 'filter' group
+ */
filterInputs = computed(() =>
this.filterService.inputs().filter((input) => input.group === 'filter'),
);
+ /**
+ * Event emitted when filters are applied
+ */
applied = output();
+ /**
+ * Event emitted when filters are reset
+ */
reseted = output();
+ /**
+ * Applies the current filter values
+ *
+ * If inputKey is provided, only that specific filter input is committed.
+ * Otherwise, all filter inputs are committed.
+ */
onApply() {
const inputKey = this.inputKey();
@@ -49,6 +88,13 @@ export class FilterActionsComponent {
this.applied.emit();
}
+ /**
+ * Resets filter values to their defaults
+ *
+ * If inputKey is provided, only that specific filter input is reset.
+ * Otherwise, all filter inputs in the 'filter' group are reset.
+ * After resetting, all changes are committed.
+ */
onReset() {
const inputKey = this.inputKey();
diff --git a/libs/shared/filter/src/lib/core/mappings/checkbox-filter-input.mapping.ts b/libs/shared/filter/src/lib/core/mappings/checkbox-filter-input.mapping.ts
index 28aa43dd2..057aef600 100644
--- a/libs/shared/filter/src/lib/core/mappings/checkbox-filter-input.mapping.ts
+++ b/libs/shared/filter/src/lib/core/mappings/checkbox-filter-input.mapping.ts
@@ -2,7 +2,21 @@ import { Input, InputType } from '../../types';
import { CheckboxFilterInput, CheckboxFilterInputSchema } from '../schemas';
import { checkboxOptionMapping } from './checkbox-option.mapping';
-export function checkboxFilterInputMapping(group: string, input: Input): CheckboxFilterInput {
+/**
+ * Maps an Input object to a CheckboxFilterInput object
+ *
+ * This function takes an input of type Checkbox and maps it to a strongly-typed
+ * CheckboxFilterInput object, validating it against a schema. It also maps all child
+ * options and tracks which options are selected.
+ *
+ * @param group - The group identifier that this input belongs to
+ * @param input - The source input object to map
+ * @returns A validated CheckboxFilterInput object
+ */
+export function checkboxFilterInputMapping(
+ group: string,
+ input: Input,
+): CheckboxFilterInput {
return CheckboxFilterInputSchema.parse({
group,
key: input.key,
@@ -13,7 +27,8 @@ export function checkboxFilterInputMapping(group: string, input: Input): Checkbo
maxOptions: input.options?.max,
options: input.options?.values?.map(checkboxOptionMapping),
selected:
- input.options?.values?.filter((option) => option.selected).map((option) => option.value) ||
- [],
+ input.options?.values
+ ?.filter((option) => option.selected)
+ .map((option) => option.value) || [],
});
}
diff --git a/libs/shared/filter/src/lib/core/mappings/checkbox-option.mapping.ts b/libs/shared/filter/src/lib/core/mappings/checkbox-option.mapping.ts
index ab182154f..d665e115d 100644
--- a/libs/shared/filter/src/lib/core/mappings/checkbox-option.mapping.ts
+++ b/libs/shared/filter/src/lib/core/mappings/checkbox-option.mapping.ts
@@ -1,7 +1,21 @@
-import { CheckboxFilterInputOption, CheckboxFilterInputOptionSchema } from '../schemas';
import { Option } from '../../types';
+import {
+ CheckboxFilterInputOption,
+ CheckboxFilterInputOptionSchema,
+} from '../schemas';
-export function checkboxOptionMapping(option: Option): CheckboxFilterInputOption {
+/**
+ * Maps an Option object to a CheckboxFilterInputOption object
+ *
+ * This function converts a generic Option to a strongly-typed
+ * CheckboxFilterInputOption, validating it against a schema.
+ *
+ * @param option - The source option object to map
+ * @returns A validated CheckboxFilterInputOption object
+ */
+export function checkboxOptionMapping(
+ option: Option,
+): CheckboxFilterInputOption {
return CheckboxFilterInputOptionSchema.parse({
label: option.label,
value: option.value,
diff --git a/libs/shared/filter/src/lib/core/mappings/data-range-filter-input.mapping.ts b/libs/shared/filter/src/lib/core/mappings/data-range-filter-input.mapping.ts
deleted file mode 100644
index a3a44ee99..000000000
--- a/libs/shared/filter/src/lib/core/mappings/data-range-filter-input.mapping.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { Input, InputType } from '../../types';
-import { DateRangeFilterInput, DateRangeFilterInputSchema } from '../schemas';
-
-export function dateRangeFilterInputMapping(group: string, input: Input): DateRangeFilterInput {
- return DateRangeFilterInputSchema.parse({
- group,
- key: input.key,
- label: input.label,
- description: input.description,
- type: InputType.DateRange,
- start: input.options?.values?.[0].value,
- stop: input.options?.values?.[1].value,
- });
-}
diff --git a/libs/shared/filter/src/lib/core/mappings/data-range-filter-input.mapping.spec.ts b/libs/shared/filter/src/lib/core/mappings/date-range-filter-input.mapping.spec.ts
similarity index 97%
rename from libs/shared/filter/src/lib/core/mappings/data-range-filter-input.mapping.spec.ts
rename to libs/shared/filter/src/lib/core/mappings/date-range-filter-input.mapping.spec.ts
index c7a43187e..bf6de7d08 100644
--- a/libs/shared/filter/src/lib/core/mappings/data-range-filter-input.mapping.spec.ts
+++ b/libs/shared/filter/src/lib/core/mappings/date-range-filter-input.mapping.spec.ts
@@ -1,5 +1,5 @@
import { Input, InputType } from '../../types';
-import { dateRangeFilterInputMapping } from './data-range-filter-input.mapping';
+import { dateRangeFilterInputMapping } from './date-range-filter-input.mapping';
import * as schemaModule from '../schemas/date-range-filter-input.schema';
describe('dateRangeFilterInputMapping', () => {
diff --git a/libs/shared/filter/src/lib/core/mappings/date-range-filter-input.mapping.ts b/libs/shared/filter/src/lib/core/mappings/date-range-filter-input.mapping.ts
new file mode 100644
index 000000000..25384a6b4
--- /dev/null
+++ b/libs/shared/filter/src/lib/core/mappings/date-range-filter-input.mapping.ts
@@ -0,0 +1,28 @@
+import { Input, InputType } from '../../types';
+import { DateRangeFilterInput, DateRangeFilterInputSchema } from '../schemas';
+
+/**
+ * Maps an Input object to a DateRangeFilterInput object
+ *
+ * This function takes an input of type DateRange and maps it to a strongly-typed
+ * DateRangeFilterInput object, validating it against a schema. It extracts the start
+ * and stop dates from the input's option values.
+ *
+ * @param group - The group identifier that this input belongs to
+ * @param input - The source input object to map
+ * @returns A validated DateRangeFilterInput object
+ */
+export function dateRangeFilterInputMapping(
+ group: string,
+ input: Input,
+): DateRangeFilterInput {
+ return DateRangeFilterInputSchema.parse({
+ group,
+ key: input.key,
+ label: input.label,
+ description: input.description,
+ type: InputType.DateRange,
+ start: input.options?.values?.[0]?.value,
+ stop: input.options?.values?.[1]?.value,
+ });
+}
diff --git a/libs/shared/filter/src/lib/core/mappings/filter-group.mapping.ts b/libs/shared/filter/src/lib/core/mappings/filter-group.mapping.ts
index 2ecf358a8..621241999 100644
--- a/libs/shared/filter/src/lib/core/mappings/filter-group.mapping.ts
+++ b/libs/shared/filter/src/lib/core/mappings/filter-group.mapping.ts
@@ -1,6 +1,16 @@
import { InputGroup } from '../../types';
import { FilterGroup, FilterGroupSchema } from '../schemas';
+/**
+ * Maps an InputGroup object to a FilterGroup object
+ *
+ * This function converts a generic InputGroup to a strongly-typed FilterGroup,
+ * validating it against a schema. It preserves the group identifier, label,
+ * and description.
+ *
+ * @param group - The source input group to map
+ * @returns A validated FilterGroup object
+ */
export function filterGroupMapping(group: InputGroup): FilterGroup {
return FilterGroupSchema.parse({
group: group.group,
diff --git a/libs/shared/filter/src/lib/core/mappings/filter-input.mapping.spec.ts b/libs/shared/filter/src/lib/core/mappings/filter-input.mapping.spec.ts
index 46768aaf0..ff4c67e26 100644
--- a/libs/shared/filter/src/lib/core/mappings/filter-input.mapping.spec.ts
+++ b/libs/shared/filter/src/lib/core/mappings/filter-input.mapping.spec.ts
@@ -1,31 +1,37 @@
import { Input, InputType } from '../../types';
import { filterInputMapping } from './filter-input.mapping';
import * as checkboxFilterInputMappingModule from './checkbox-filter-input.mapping';
-import * as dateRangeFilterInputMappingModule from './data-range-filter-input.mapping';
+import * as dateRangeFilterInputMappingModule from './date-range-filter-input.mapping';
import * as textFilterInputMappingModule from './text-filter-input.mapping';
describe('filterInputMapping', () => {
// Mock implementations for each specific mapping function
- const mockTextFilterInputMapping = jest.fn().mockImplementation((group, input) => ({
- type: InputType.Text,
- group,
- key: input.key,
- mapped: 'text',
- }));
+ const mockTextFilterInputMapping = jest
+ .fn()
+ .mockImplementation((group, input) => ({
+ type: InputType.Text,
+ group,
+ key: input.key,
+ mapped: 'text',
+ }));
- const mockCheckboxFilterInputMapping = jest.fn().mockImplementation((group, input) => ({
- type: InputType.Checkbox,
- group,
- key: input.key,
- mapped: 'checkbox',
- }));
+ const mockCheckboxFilterInputMapping = jest
+ .fn()
+ .mockImplementation((group, input) => ({
+ type: InputType.Checkbox,
+ group,
+ key: input.key,
+ mapped: 'checkbox',
+ }));
- const mockDateRangeFilterInputMapping = jest.fn().mockImplementation((group, input) => ({
- type: InputType.DateRange,
- group,
- key: input.key,
- mapped: 'dateRange',
- }));
+ const mockDateRangeFilterInputMapping = jest
+ .fn()
+ .mockImplementation((group, input) => ({
+ type: InputType.DateRange,
+ group,
+ key: input.key,
+ mapped: 'dateRange',
+ }));
beforeEach(() => {
jest.clearAllMocks();
@@ -124,7 +130,9 @@ describe('filterInputMapping', () => {
};
// Act & Assert
- expect(() => filterInputMapping(group, input)).toThrowError('Unknown input type: 999');
+ expect(() => filterInputMapping(group, input)).toThrowError(
+ 'Unknown input type: 999',
+ );
expect(mockTextFilterInputMapping).not.toHaveBeenCalled();
expect(mockCheckboxFilterInputMapping).not.toHaveBeenCalled();
expect(mockDateRangeFilterInputMapping).not.toHaveBeenCalled();
diff --git a/libs/shared/filter/src/lib/core/mappings/filter-input.mapping.ts b/libs/shared/filter/src/lib/core/mappings/filter-input.mapping.ts
index e8dd789cf..1889f56d3 100644
--- a/libs/shared/filter/src/lib/core/mappings/filter-input.mapping.ts
+++ b/libs/shared/filter/src/lib/core/mappings/filter-input.mapping.ts
@@ -1,9 +1,21 @@
import { Input, InputType } from '../../types';
import { FilterInput } from '../schemas';
import { checkboxFilterInputMapping } from './checkbox-filter-input.mapping';
-import { dateRangeFilterInputMapping } from './data-range-filter-input.mapping';
+import { dateRangeFilterInputMapping } from './date-range-filter-input.mapping';
import { textFilterInputMapping } from './text-filter-input.mapping';
+/**
+ * Maps an Input object to the appropriate FilterInput type based on its input type
+ *
+ * This function serves as a router that delegates to the specific mapping function
+ * based on the input type (Text, Checkbox, DateRange). It ensures that each input
+ * is converted to its corresponding strongly-typed filter input object.
+ *
+ * @param group - The group identifier that this input belongs to
+ * @param input - The source input object to map
+ * @returns A validated FilterInput object of the appropriate subtype
+ * @throws Error if the input type is not supported
+ */
export function filterInputMapping(group: string, input: Input): FilterInput {
switch (input.type) {
case InputType.Text:
diff --git a/libs/shared/filter/src/lib/core/mappings/filter.mapping.ts b/libs/shared/filter/src/lib/core/mappings/filter.mapping.ts
index cdd9b32d6..0322da705 100644
--- a/libs/shared/filter/src/lib/core/mappings/filter.mapping.ts
+++ b/libs/shared/filter/src/lib/core/mappings/filter.mapping.ts
@@ -4,6 +4,21 @@ import { filterGroupMapping } from './filter-group.mapping';
import { filterInputMapping } from './filter-input.mapping';
import { orderByOptionMapping } from './order-by-option.mapping';
+/**
+ * Maps a QuerySettings object to a Filter object
+ *
+ * This is the main mapping function that transforms query settings into a
+ * complete Filter object structure. It:
+ * 1. Creates filter groups from both filter and input settings
+ * 2. Maps all inputs from each group to their corresponding filter inputs
+ * 3. Maps order by options if present
+ *
+ * The resulting Filter object can be used by filter components to render
+ * the appropriate UI and handle user interactions.
+ *
+ * @param settings - The source query settings to map
+ * @returns A fully populated Filter object with groups, inputs, and ordering options
+ */
export function filterMapping(settings: QuerySettings): Filter {
const filter: Filter = {
groups: [],
diff --git a/libs/shared/filter/src/lib/core/mappings/index.ts b/libs/shared/filter/src/lib/core/mappings/index.ts
index 2ca2d54a5..50463c081 100644
--- a/libs/shared/filter/src/lib/core/mappings/index.ts
+++ b/libs/shared/filter/src/lib/core/mappings/index.ts
@@ -1,6 +1,6 @@
export * from './checkbox-filter-input.mapping';
export * from './checkbox-option.mapping';
-export * from './data-range-filter-input.mapping';
+export * from './date-range-filter-input.mapping';
export * from './filter-group.mapping';
export * from './filter-input.mapping';
export * from './filter.mapping';
diff --git a/libs/shared/filter/src/lib/core/mappings/order-by-option.mapping.ts b/libs/shared/filter/src/lib/core/mappings/order-by-option.mapping.ts
index b44e6e588..57694529d 100644
--- a/libs/shared/filter/src/lib/core/mappings/order-by-option.mapping.ts
+++ b/libs/shared/filter/src/lib/core/mappings/order-by-option.mapping.ts
@@ -1,6 +1,17 @@
import { OrderBy } from '../../types';
import { OrderByOption, OrderByOptionSchema } from '../schemas';
+/**
+ * Maps an OrderBy object to an OrderByOption object
+ *
+ * This function transforms an OrderBy input definition into a strongly-typed
+ * OrderByOption that can be used by the filter component. It converts the
+ * desc boolean flag to a direction string ('asc' or 'desc') and initializes
+ * the selected state to false.
+ *
+ * @param orderBy - The source OrderBy object to map
+ * @returns A validated OrderByOption object
+ */
export function orderByOptionMapping(orderBy: OrderBy): OrderByOption {
return OrderByOptionSchema.parse({
label: orderBy.label,
diff --git a/libs/shared/filter/src/lib/core/mappings/text-filter-input.mapping.ts b/libs/shared/filter/src/lib/core/mappings/text-filter-input.mapping.ts
index bdbccb87a..3fdbbf3e4 100644
--- a/libs/shared/filter/src/lib/core/mappings/text-filter-input.mapping.ts
+++ b/libs/shared/filter/src/lib/core/mappings/text-filter-input.mapping.ts
@@ -1,7 +1,20 @@
import { Input, InputType } from '../../types';
import { TextFilterInput, TextFilterInputSchema } from '../schemas';
-export function textFilterInputMapping(group: string, input: Input): TextFilterInput {
+/**
+ * Maps an Input object to a TextFilterInput object
+ *
+ * This function takes an input of type Text and maps it to a strongly-typed
+ * TextFilterInput object, validating it against a schema.
+ *
+ * @param group - The group identifier that this input belongs to
+ * @param input - The source input object to map
+ * @returns A validated TextFilterInput object
+ */
+export function textFilterInputMapping(
+ group: string,
+ input: Input,
+): TextFilterInput {
return TextFilterInputSchema.parse({
group,
key: input.key,
diff --git a/libs/shared/filter/src/lib/core/schemas/base-filter-input.schema.ts b/libs/shared/filter/src/lib/core/schemas/base-filter-input.schema.ts
index de8f5645f..0dddbac18 100644
--- a/libs/shared/filter/src/lib/core/schemas/base-filter-input.schema.ts
+++ b/libs/shared/filter/src/lib/core/schemas/base-filter-input.schema.ts
@@ -1,13 +1,43 @@
import { z } from 'zod';
import { InputType } from '../../types';
+/**
+ * Base schema for all filter input types.
+ * Contains common properties that all filter inputs must have.
+ *
+ * @property group - Group identifier that this input belongs to
+ * @property key - Unique identifier for the input within its group
+ * @property label - Optional display name for the input
+ * @property description - Optional detailed explanation of the input
+ * @property type - The type of input control (Text, Checkbox, DateRange)
+ */
export const BaseFilterInputSchema = z
.object({
- group: z.string(),
- key: z.string(),
- label: z.string().optional(),
- description: z.string().optional(),
- type: z.nativeEnum(InputType),
+ group: z
+ .string()
+ .describe(
+ 'Identifier for the group this filter input belongs to. Used for organizing related filters.',
+ ),
+ key: z
+ .string()
+ .describe(
+ 'Unique identifier for this input within its group. Used as a key in requests and state management.',
+ ),
+ label: z
+ .string()
+ .optional()
+ .describe('Human-readable display name shown to users in the UI.'),
+ description: z
+ .string()
+ .optional()
+ .describe(
+ 'Detailed explanation of what this filter does, displayed as helper text in the UI.',
+ ),
+ type: z
+ .nativeEnum(InputType)
+ .describe(
+ 'Determines the type of input control and its behavior (Text, Checkbox, DateRange, etc.).',
+ ),
})
.describe('BaseFilterInput');
diff --git a/libs/shared/filter/src/lib/core/schemas/checkbox-filter-input-option.schema.ts b/libs/shared/filter/src/lib/core/schemas/checkbox-filter-input-option.schema.ts
index 6f3e92089..8b9bc2c14 100644
--- a/libs/shared/filter/src/lib/core/schemas/checkbox-filter-input-option.schema.ts
+++ b/libs/shared/filter/src/lib/core/schemas/checkbox-filter-input-option.schema.ts
@@ -1,10 +1,24 @@
import { z } from 'zod';
+/**
+ * Represents a checkbox option within a CheckboxFilterInput.
+ *
+ * @property label - Display text for the checkbox option
+ * @property value - The value to be used when this option is selected
+ */
export const CheckboxFilterInputOptionSchema = z
.object({
- label: z.string(),
- value: z.string(),
+ label: z
+ .string()
+ .describe('Display text shown next to the checkbox in the UI.'),
+ value: z
+ .string()
+ .describe(
+ 'Underlying value that will be sent in requests when this option is selected.',
+ ),
})
.describe('CheckboxFilterInputOption');
-export type CheckboxFilterInputOption = z.infer;
+export type CheckboxFilterInputOption = z.infer<
+ typeof CheckboxFilterInputOptionSchema
+>;
diff --git a/libs/shared/filter/src/lib/core/schemas/checkbox-filter-input.schema.ts b/libs/shared/filter/src/lib/core/schemas/checkbox-filter-input.schema.ts
index 17bf9f12f..7d915f73c 100644
--- a/libs/shared/filter/src/lib/core/schemas/checkbox-filter-input.schema.ts
+++ b/libs/shared/filter/src/lib/core/schemas/checkbox-filter-input.schema.ts
@@ -3,11 +3,35 @@ import { BaseFilterInputSchema } from './base-filter-input.schema';
import { InputType } from '../../types';
import { CheckboxFilterInputOptionSchema } from './checkbox-filter-input-option.schema';
+/**
+ * Schema for checkbox-based filter inputs that allow users to select from multiple options.
+ * Extends the BaseFilterInputSchema with checkbox-specific properties.
+ *
+ * @property type - Must be InputType.Checkbox
+ * @property maxOptions - Optional limit on how many options can be selected
+ * @property options - Array of selectable checkbox options
+ * @property selected - Array of string values representing the currently selected options
+ */
export const CheckboxFilterInputSchema = BaseFilterInputSchema.extend({
- type: z.literal(InputType.Checkbox),
- maxOptions: z.number().optional(),
- options: z.array(CheckboxFilterInputOptionSchema),
- selected: z.array(z.string()),
+ type: z
+ .literal(InputType.Checkbox)
+ .describe(
+ 'Specifies this as a checkbox input type. Must be InputType.Checkbox.',
+ ),
+ maxOptions: z
+ .number()
+ .optional()
+ .describe(
+ 'Optional maximum number of options that can be selected simultaneously. If not provided, all options can be selected.',
+ ),
+ options: z
+ .array(CheckboxFilterInputOptionSchema)
+ .describe('List of available checkbox options that users can select from.'),
+ selected: z
+ .array(z.string())
+ .describe(
+ 'Array of values representing which options are currently selected. Each value corresponds to the value property of an option.',
+ ),
}).describe('CheckboxFilterInput');
export type CheckboxFilterInput = z.infer;
diff --git a/libs/shared/filter/src/lib/core/schemas/date-range-filter-input.schema.ts b/libs/shared/filter/src/lib/core/schemas/date-range-filter-input.schema.ts
index 4f2812cdd..ee1761170 100644
--- a/libs/shared/filter/src/lib/core/schemas/date-range-filter-input.schema.ts
+++ b/libs/shared/filter/src/lib/core/schemas/date-range-filter-input.schema.ts
@@ -2,10 +2,32 @@ import { z } from 'zod';
import { BaseFilterInputSchema } from './base-filter-input.schema';
import { InputType } from '../../types';
+/**
+ * Schema for date range inputs that allow filtering by a time period.
+ * Extends BaseFilterInputSchema with date range specific properties.
+ *
+ * @property type - Must be InputType.DateRange
+ * @property start - Optional ISO string representing the start date of the range
+ * @property stop - Optional ISO string representing the end date of the range
+ */
export const DateRangeFilterInputSchema = BaseFilterInputSchema.extend({
- type: z.literal(InputType.DateRange),
- start: z.string().optional(),
- stop: z.string().optional(),
+ type: z
+ .literal(InputType.DateRange)
+ .describe(
+ 'Specifies this as a date range input type. Must be InputType.DateRange.',
+ ),
+ start: z
+ .string()
+ .optional()
+ .describe(
+ 'ISO date string representing the beginning of the date range. Optional if only an end date is needed.',
+ ),
+ stop: z
+ .string()
+ .optional()
+ .describe(
+ 'ISO date string representing the end of the date range. Optional if only a start date is needed.',
+ ),
}).describe('DateRangeFilterInput');
export type DateRangeFilterInput = z.infer;
diff --git a/libs/shared/filter/src/lib/core/schemas/filter-group.schema.ts b/libs/shared/filter/src/lib/core/schemas/filter-group.schema.ts
index 221c2c856..ef1e3d779 100644
--- a/libs/shared/filter/src/lib/core/schemas/filter-group.schema.ts
+++ b/libs/shared/filter/src/lib/core/schemas/filter-group.schema.ts
@@ -1,10 +1,32 @@
import { z } from 'zod';
+/**
+ * Schema for filter groups that organize filter inputs into logical sections.
+ * Groups provide a way to categorize related filters together for better organization.
+ *
+ * @property group - Unique identifier for the filter group
+ * @property label - Optional display name for the filter group in the UI
+ * @property description - Optional detailed explanation of what this filter group represents
+ */
export const FilterGroupSchema = z
.object({
- group: z.string(),
- label: z.string().optional(),
- description: z.string().optional(),
+ group: z
+ .string()
+ .describe(
+ 'Unique identifier for the filter group, used for referencing in code.',
+ ),
+ label: z
+ .string()
+ .optional()
+ .describe(
+ 'Human-readable name for the filter group displayed in the UI.',
+ ),
+ description: z
+ .string()
+ .optional()
+ .describe(
+ "Detailed explanation of the filter group's purpose or contents, may be shown as helper text.",
+ ),
})
.describe('FilterGroup');
diff --git a/libs/shared/filter/src/lib/core/schemas/filter-input.schema.ts b/libs/shared/filter/src/lib/core/schemas/filter-input.schema.ts
index e70f05421..acb9be8ad 100644
--- a/libs/shared/filter/src/lib/core/schemas/filter-input.schema.ts
+++ b/libs/shared/filter/src/lib/core/schemas/filter-input.schema.ts
@@ -3,6 +3,15 @@ import { CheckboxFilterInputSchema } from './checkbox-filter-input.schema';
import { DateRangeFilterInputSchema } from './date-range-filter-input.schema';
import { TextFilterInputSchema } from './text-filter-input.schema';
+/**
+ * A union schema representing all possible filter input types in the system.
+ * This schema allows for type discrimination based on the `type` property.
+ *
+ * Supported filter input types:
+ * - TextFilterInput: Simple text input fields
+ * - CheckboxFilterInput: Multiple-choice checkbox selections
+ * - DateRangeFilterInput: Date range selectors for time-based filtering
+ */
export const FilterInputSchema = z.union([
TextFilterInputSchema,
CheckboxFilterInputSchema,
diff --git a/libs/shared/filter/src/lib/core/schemas/filter.schema.ts b/libs/shared/filter/src/lib/core/schemas/filter.schema.ts
index ed333fa0d..bf813056e 100644
--- a/libs/shared/filter/src/lib/core/schemas/filter.schema.ts
+++ b/libs/shared/filter/src/lib/core/schemas/filter.schema.ts
@@ -3,11 +3,31 @@ import { FilterGroupSchema } from './filter-group.schema';
import { FilterInputSchema } from './filter-input.schema';
import { OrderByOptionSchema } from './order-by-option.schema';
+/**
+ * Top-level schema representing the complete filter configuration.
+ * Combines filter groups, input fields, and ordering options into a unified structure.
+ *
+ * @property groups - Collection of filter groups for organizing inputs into categories
+ * @property inputs - All filter input controls available to the user
+ * @property orderBy - Available sorting options for the filtered results
+ */
export const FilterSchema = z
.object({
- groups: z.array(FilterGroupSchema),
- inputs: z.array(FilterInputSchema),
- orderBy: z.array(OrderByOptionSchema),
+ groups: z
+ .array(FilterGroupSchema)
+ .describe(
+ 'Collection of filter groups that organize inputs into logical categories for better user experience.',
+ ),
+ inputs: z
+ .array(FilterInputSchema)
+ .describe(
+ 'Array of all filter input controls available to the user across all groups.',
+ ),
+ orderBy: z
+ .array(OrderByOptionSchema)
+ .describe(
+ 'Available sorting options that users can apply to the filtered results.',
+ ),
})
.describe('Filter');
diff --git a/libs/shared/filter/src/lib/core/schemas/order-by-direction.schema.ts b/libs/shared/filter/src/lib/core/schemas/order-by-direction.schema.ts
index 63103fe73..cf1a1419f 100644
--- a/libs/shared/filter/src/lib/core/schemas/order-by-direction.schema.ts
+++ b/libs/shared/filter/src/lib/core/schemas/order-by-direction.schema.ts
@@ -1,5 +1,16 @@
import { z } from 'zod';
-export const OrderByDirectionSchema = z.enum(['asc', 'desc']);
+/**
+ * Enum schema for sort directions in ordering operations.
+ * Provides type-safe options for ascending or descending sorting.
+ *
+ * - 'asc': Ascending order (A-Z, 0-9, oldest to newest)
+ * - 'desc': Descending order (Z-A, 9-0, newest to oldest)
+ */
+export const OrderByDirectionSchema = z
+ .enum(['asc', 'desc'])
+ .describe(
+ 'Direction for sorting operations, either ascending (asc) or descending (desc).',
+ );
export type OrderByDirection = z.infer;
diff --git a/libs/shared/filter/src/lib/core/schemas/order-by-option.schema.ts b/libs/shared/filter/src/lib/core/schemas/order-by-option.schema.ts
index 3e23bb14a..15caf815d 100644
--- a/libs/shared/filter/src/lib/core/schemas/order-by-option.schema.ts
+++ b/libs/shared/filter/src/lib/core/schemas/order-by-option.schema.ts
@@ -1,12 +1,34 @@
import { z } from 'zod';
import { OrderByDirectionSchema } from './order-by-direction.schema';
+/**
+ * Schema for defining sort options available to the user.
+ * Each option represents a different field or property that can be used for ordering results.
+ *
+ * @property by - Field identifier to sort by (corresponds to a property in the data)
+ * @property label - Human-readable name for this sort option to display in the UI
+ * @property dir - Sort direction ('asc' for ascending or 'desc' for descending)
+ * @property selected - Whether this ordering option is currently active
+ */
export const OrderByOptionSchema = z
.object({
- by: z.string(),
- label: z.string(),
- dir: OrderByDirectionSchema,
- selected: z.boolean().default(false),
+ by: z
+ .string()
+ .describe(
+ 'Field identifier to sort by, matching a property in the data model.',
+ ),
+ label: z
+ .string()
+ .describe(
+ 'Human-readable name for this sort option to display in the UI.',
+ ),
+ dir: OrderByDirectionSchema.describe(
+ 'Sort direction, either "asc" for ascending or "desc" for descending.',
+ ),
+ selected: z
+ .boolean()
+ .default(false)
+ .describe('Indicates whether this ordering option is currently active.'),
})
.describe('OrderByOption');
diff --git a/libs/shared/filter/src/lib/core/schemas/query-order.schema.ts b/libs/shared/filter/src/lib/core/schemas/query-order.schema.ts
index f9694a178..660d51d01 100644
--- a/libs/shared/filter/src/lib/core/schemas/query-order.schema.ts
+++ b/libs/shared/filter/src/lib/core/schemas/query-order.schema.ts
@@ -1,10 +1,33 @@
import { z } from 'zod';
+/**
+ * Schema representing a sorting criterion in a query.
+ * This defines how results should be ordered when returned from a data source.
+ *
+ * @property by - Field identifier to sort by (corresponds to a property in the data)
+ * @property label - Human-readable name for this sort option
+ * @property desc - Whether the sort should be in descending order (true) or ascending (false)
+ * @property selected - Whether this ordering option is currently active
+ */
export const QueryOrderBySchema = z.object({
- by: z.string(),
- label: z.string(),
- desc: z.boolean(),
- selected: z.boolean(),
+ by: z
+ .string()
+ .describe(
+ 'Field identifier to sort by, matching a property name in the data model.',
+ ),
+ label: z
+ .string()
+ .describe('Human-readable name for this sort option to display in the UI.'),
+ desc: z
+ .boolean()
+ .describe(
+ 'Sort direction flag: true for descending order (Z-A, newest first), false for ascending (A-Z, oldest first).',
+ ),
+ selected: z
+ .boolean()
+ .describe(
+ 'Indicates whether this ordering option is currently active in the query.',
+ ),
});
export type QueryOrderBy = z.infer;
diff --git a/libs/shared/filter/src/lib/core/schemas/query.schema.ts b/libs/shared/filter/src/lib/core/schemas/query.schema.ts
index 0d205a5ad..980a92d50 100644
--- a/libs/shared/filter/src/lib/core/schemas/query.schema.ts
+++ b/libs/shared/filter/src/lib/core/schemas/query.schema.ts
@@ -1,12 +1,47 @@
import { z } from 'zod';
import { QueryOrderBySchema } from './query-order.schema';
+/**
+ * Schema representing a complete query for retrieving filtered and sorted data.
+ * This is the core schema used when making requests to APIs or data sources.
+ *
+ * @property filter - Record of filter criteria to apply when querying data
+ * @property input - Record of user input values from filter controls
+ * @property orderBy - Array of sort criteria to determine result ordering
+ * @property skip - Number of items to skip (for pagination)
+ * @property take - Maximum number of items to return (page size)
+ */
export const QuerySchema = z.object({
- filter: z.record(z.any()).default({}),
- input: z.record(z.any()).default({}),
- orderBy: z.array(QueryOrderBySchema).default([]),
- skip: z.number().default(0),
- take: z.number().default(25),
+ filter: z
+ .record(z.any())
+ .default({})
+ .describe(
+ 'Key-value pairs of filter criteria to apply when querying data. Keys correspond to data properties, values are the filtering constraints.',
+ ),
+ input: z
+ .record(z.any())
+ .default({})
+ .describe(
+ 'Key-value pairs representing user input from filter controls. Used to store and restore filter state.',
+ ),
+ orderBy: z
+ .array(QueryOrderBySchema)
+ .default([])
+ .describe(
+ 'Array of sorting criteria that determine how results should be ordered. Applied in sequence for multi-level sorting.',
+ ),
+ skip: z
+ .number()
+ .default(0)
+ .describe(
+ 'Number of items to skip from the beginning of the result set. Used for implementing pagination.',
+ ),
+ take: z
+ .number()
+ .default(25)
+ .describe(
+ 'Maximum number of items to return in a single query. Defines the page size for paginated results.',
+ ),
});
export type Query = z.infer;
diff --git a/libs/shared/filter/src/lib/core/schemas/text-filter-input.schema.ts b/libs/shared/filter/src/lib/core/schemas/text-filter-input.schema.ts
index 3a610b992..021a48dbe 100644
--- a/libs/shared/filter/src/lib/core/schemas/text-filter-input.schema.ts
+++ b/libs/shared/filter/src/lib/core/schemas/text-filter-input.schema.ts
@@ -2,11 +2,37 @@ import { z } from 'zod';
import { BaseFilterInputSchema } from './base-filter-input.schema';
import { InputType } from '../../types';
+/**
+ * Schema for text-based filter inputs that allow free-form text entry.
+ * Extends BaseFilterInputSchema with text input specific properties.
+ *
+ * @property type - Must be InputType.Text
+ * @property placeholder - Optional hint text to display when the input is empty
+ * @property defaultValue - Optional initial value to populate the input with
+ * @property value - Current value of the text input
+ */
export const TextFilterInputSchema = BaseFilterInputSchema.extend({
- type: z.literal(InputType.Text),
- placeholder: z.string().optional(),
- defaultValue: z.string().optional(),
- value: z.string().optional(),
+ type: z
+ .literal(InputType.Text)
+ .describe('Specifies this as a text input type. Must be InputType.Text.'),
+ placeholder: z
+ .string()
+ .optional()
+ .describe(
+ 'Hint text displayed when the input is empty to guide users on what to enter.',
+ ),
+ defaultValue: z
+ .string()
+ .optional()
+ .describe(
+ 'Initial value to populate the text field with when first rendered or reset.',
+ ),
+ value: z
+ .string()
+ .optional()
+ .describe(
+ 'Current value of the text input field, reflecting what the user has entered.',
+ ),
}).describe('TextFilterInput');
export type TextFilterInput = z.infer;
diff --git a/libs/shared/filter/src/lib/menus/filter-menu/filter-menu-button.component.spec.ts b/libs/shared/filter/src/lib/menus/filter-menu/filter-menu-button.component.spec.ts
index e69de29bb..f9266787c 100644
--- a/libs/shared/filter/src/lib/menus/filter-menu/filter-menu-button.component.spec.ts
+++ b/libs/shared/filter/src/lib/menus/filter-menu/filter-menu-button.component.spec.ts
@@ -0,0 +1,93 @@
+import {
+ createComponentFactory,
+ Spectator,
+ mockProvider,
+} from '@ngneat/spectator/jest';
+import { MockComponents } from 'ng-mocks';
+import { FilterMenuButtonComponent } from './filter-menu-button.component';
+import { FilterService } from '../../core';
+import { Overlay } from '@angular/cdk/overlay';
+import { FilterMenuComponent } from './filter-menu.component';
+import { NgIconComponent } from '@ng-icons/core';
+import { IconButtonComponent } from '@isa/ui/buttons';
+import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
+
+describe('FilterMenuButtonComponent', () => {
+ let spectator: Spectator;
+ let filterService: jest.Mocked;
+
+ const createComponent = createComponentFactory({
+ component: FilterMenuButtonComponent,
+ declarations: [
+ MockComponents(NgIconComponent, FilterMenuComponent, IconButtonComponent),
+ ],
+ componentProviders: [
+ mockProvider(Overlay, { scrollStrategies: { block: jest.fn() } }),
+ ],
+ providers: [
+ mockProvider(FilterService, {
+ isDefaultFilter: jest.fn().mockReturnValue(true),
+ rollback: jest.fn(),
+ }),
+ ],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
+ detectChanges: false,
+ });
+
+ beforeEach(() => {
+ spectator = createComponent({
+ props: {
+ rollbackOnClose: false,
+ },
+ });
+ spectator.detectChanges();
+
+ filterService = spectator.inject(FilterService);
+ });
+
+ it('should create the component', () => {
+ expect(spectator.component).toBeTruthy();
+ });
+
+ it('should toggle open state and emit events', () => {
+ // Arrange
+ const closedSpy = jest.spyOn(spectator.component.closed, 'emit');
+ const openedSpy = jest.spyOn(spectator.component.opened, 'emit');
+
+ // Act - Open
+ spectator.component.toggle();
+
+ // Assert - Open
+ expect(spectator.component.open()).toBe(true);
+ expect(openedSpy).toHaveBeenCalled();
+
+ // Act - Close
+ spectator.component.toggle();
+
+ // Assert - Close
+ expect(spectator.component.open()).toBe(false);
+ expect(closedSpy).toHaveBeenCalled();
+ });
+
+ it('should rollback on close when rollbackOnClose is true', () => {
+ // Arrange
+ spectator.setInput('rollbackOnClose', true);
+
+ // Act
+ spectator.component.closed.emit();
+
+ // Assert
+ expect(filterService.rollback).toHaveBeenCalled();
+ });
+
+ it('should close menu when applied is emitted', () => {
+ // Arrange
+ spectator.component.open.set(true);
+
+ // Act
+ spectator.component.applied.emit();
+
+ // Assert
+ expect(spectator.component.open()).toBe(false);
+ });
+});
diff --git a/libs/shared/filter/src/lib/menus/filter-menu/filter-menu.component.spec.ts b/libs/shared/filter/src/lib/menus/filter-menu/filter-menu.component.spec.ts
deleted file mode 100644
index e69de29bb..000000000
diff --git a/libs/shared/filter/src/lib/menus/input-menu/input-menu-button.component.spec.ts b/libs/shared/filter/src/lib/menus/input-menu/input-menu-button.component.spec.ts
index 5f1b4cabd..ce543e887 100644
--- a/libs/shared/filter/src/lib/menus/input-menu/input-menu-button.component.spec.ts
+++ b/libs/shared/filter/src/lib/menus/input-menu/input-menu-button.component.spec.ts
@@ -1,10 +1,19 @@
-import { createComponentFactory, Spectator, mockProvider } from '@ngneat/spectator/jest';
+import {
+ createComponentFactory,
+ Spectator,
+ mockProvider,
+} from '@ngneat/spectator/jest';
import { MockComponents, MockDirectives } from 'ng-mocks';
import { FilterInputMenuButtonComponent } from './input-menu-button.component';
import { FilterInput, FilterService } from '../../core';
-import { CdkConnectedOverlay, CdkOverlayOrigin, Overlay } from '@angular/cdk/overlay';
+import {
+ CdkConnectedOverlay,
+ CdkOverlayOrigin,
+ Overlay,
+} from '@angular/cdk/overlay';
import { FilterInputMenuComponent } from './input-menu.component';
import { NgIconComponent } from '@ng-icons/core';
+import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
describe('FilterInputMenuButtonComponent', () => {
let spectator: Spectator;
@@ -19,13 +28,16 @@ describe('FilterInputMenuButtonComponent', () => {
MockComponents(NgIconComponent, FilterInputMenuComponent),
MockDirectives(CdkOverlayOrigin, CdkConnectedOverlay),
],
- componentProviders: [mockProvider(Overlay, { scrollStrategies: { block: jest.fn() } })],
+ componentProviders: [
+ mockProvider(Overlay, { scrollStrategies: { block: jest.fn() } }),
+ ],
providers: [
mockProvider(FilterService, {
isDefaultFilterInput: jest.fn().mockReturnValue(true),
commit: jest.fn(),
}),
],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
detectChanges: false,
});