paperless-ngx/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.spec.ts
2025-01-01 21:52:45 -08:00

356 lines
12 KiB
TypeScript

import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import {
ComponentFixture,
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { of } from 'rxjs'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
import {
CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP,
CustomFieldQueryLogicalOperator,
CustomFieldQueryOperatorGroups,
} from 'src/app/data/custom-field-query'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import {
CustomFieldQueryAtom,
CustomFieldQueryElement,
CustomFieldQueryExpression,
} from 'src/app/utils/custom-field-query-element'
import {
CustomFieldQueriesModel,
CustomFieldsQueryDropdownComponent,
} from './custom-fields-query-dropdown.component'
const customFields = [
{
id: 1,
name: 'Test Field',
data_type: CustomFieldDataType.String,
extra_data: {},
},
{
id: 2,
name: 'Test Select Field',
data_type: CustomFieldDataType.Select,
extra_data: {
select_options: [
{ label: 'Option 1', id: 'abc-123' },
{ label: 'Option 2', id: 'def-456' },
],
},
},
]
describe('CustomFieldsQueryDropdownComponent', () => {
let component: CustomFieldsQueryDropdownComponent
let fixture: ComponentFixture<CustomFieldsQueryDropdownComponent>
let customFieldsService: CustomFieldsService
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
NgbDropdownModule,
NgxBootstrapIconsModule.pick(allIcons),
NgSelectModule,
FormsModule,
ReactiveFormsModule,
CustomFieldsQueryDropdownComponent,
],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents()
customFieldsService = TestBed.inject(CustomFieldsService)
jest.spyOn(customFieldsService, 'listAll').mockReturnValue(
of({
count: customFields.length,
all: customFields.map((f) => f.id),
results: customFields,
})
)
fixture = TestBed.createComponent(CustomFieldsQueryDropdownComponent)
component = fixture.componentInstance
component.icon = 'ui-radios'
fixture.detectChanges()
})
it('should initialize custom fields on creation', () => {
expect(component.customFields).toEqual(customFields)
})
it('should add an expression when opened if queries are empty', () => {
component.selectionModel.clear()
component.onOpenChange(true)
expect(component.selectionModel.queries.length).toBe(1)
})
it('should support reset the selection model', () => {
component.selectionModel.addExpression()
component.reset()
expect(component.selectionModel.isEmpty()).toBeTruthy()
})
it('should get operators for a field', () => {
const field: CustomField = {
id: 1,
name: 'Test Field',
data_type: CustomFieldDataType.String,
extra_data: {},
}
component.customFields = [field]
const operators = component.getOperatorsForField(1)
expect(operators.length).toEqual(
[
...CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[
CustomFieldQueryOperatorGroups.Basic
],
...CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[
CustomFieldQueryOperatorGroups.String
],
].length
)
// Fallback to basic operators if field is not found
const operators2 = component.getOperatorsForField(2)
expect(operators2.length).toEqual(
CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[
CustomFieldQueryOperatorGroups.Basic
].length
)
})
it('should get select options for a field', () => {
const field: CustomField = {
id: 1,
name: 'Test Field',
data_type: CustomFieldDataType.Select,
extra_data: {
select_options: [
{ label: 'Option 1', id: 'abc-123' },
{ label: 'Option 2', id: 'def-456' },
],
},
}
component.customFields = [field]
const options = component.getSelectOptionsForField(1)
expect(options).toEqual([
{ label: 'Option 1', id: 'abc-123' },
{ label: 'Option 2', id: 'def-456' },
])
// Fallback to empty array if field is not found
const options2 = component.getSelectOptionsForField(2)
expect(options2).toEqual([])
})
it('should remove an element from the selection model', () => {
const expression = new CustomFieldQueryExpression()
const atom = new CustomFieldQueryAtom()
;(expression.value as CustomFieldQueryElement[]).push(atom)
component.selectionModel.addExpression(expression)
component.removeElement(atom)
expect(component.selectionModel.isEmpty()).toBeTruthy()
const expression2 = new CustomFieldQueryExpression([
CustomFieldQueryLogicalOperator.And,
[
[1, 'icontains', 'test'],
[2, 'icontains', 'test'],
],
])
component.selectionModel.addExpression(expression2)
component.removeElement(expression2)
expect(component.selectionModel.isEmpty()).toBeTruthy()
})
it('should emit selectionModelChange when model changes', () => {
const nextSpy = jest.spyOn(component.selectionModelChange, 'next')
const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
component.selectionModel.addAtom(atom)
atom.changed.next(atom)
expect(nextSpy).toHaveBeenCalled()
})
it('should complete selection model subscription when new selection model is set', () => {
const completeSpy = jest.spyOn(component.selectionModel.changed, 'complete')
const selectionModel = new CustomFieldQueriesModel()
component.selectionModel = selectionModel
expect(completeSpy).toHaveBeenCalled()
})
it('should support adding an atom', () => {
const expression = new CustomFieldQueryExpression()
component.addAtom(expression)
expect(expression.value.length).toBe(1)
})
it('should support adding an expression', () => {
const expression = new CustomFieldQueryExpression()
component.addExpression(expression)
expect(expression.value.length).toBe(1)
})
it('should support getting a custom field by ID', () => {
expect(component.getCustomFieldByID(1)).toEqual(customFields[0])
})
it('should sanitize name from title', () => {
component.title = 'Test Title'
expect(component.name).toBe('test_title')
})
it('should add a default atom on open and focus the select field', fakeAsync(() => {
expect(component.selectionModel.queries.length).toBe(0)
component.onOpenChange(true)
fixture.detectChanges()
tick()
expect(component.selectionModel.queries.length).toBe(1)
expect(window.document.activeElement.tagName).toBe('INPUT')
}))
describe('CustomFieldQueriesModel', () => {
let model: CustomFieldQueriesModel
beforeEach(() => {
model = new CustomFieldQueriesModel()
})
it('should initialize with empty queries', () => {
expect(model.queries).toEqual([])
})
it('should clear queries and fire event', () => {
const nextSpy = jest.spyOn(model.changed, 'next')
model.addExpression()
model.clear()
expect(model.queries).toEqual([])
expect(nextSpy).toHaveBeenCalledWith(model)
})
it('should clear queries without firing event', () => {
const nextSpy = jest.spyOn(model.changed, 'next')
model.addExpression()
model.clear(false)
expect(model.queries).toEqual([])
expect(nextSpy).not.toHaveBeenCalled()
})
it('should validate an empty model as invalid', () => {
expect(model.isValid()).toBeFalsy()
})
it('should validate a model with valid expression as valid', () => {
const expression = new CustomFieldQueryExpression()
const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
const atom2 = new CustomFieldQueryAtom([2, 'icontains', 'test'])
const expression2 = new CustomFieldQueryExpression()
expression2.addAtom(atom)
expression2.addAtom(atom2)
expression.addExpression(expression2)
model.addExpression(expression)
expect(model.isValid()).toBeTruthy()
})
it('should validate a model with invalid expression as invalid', () => {
const expression = new CustomFieldQueryExpression()
model.addExpression(expression)
expect(model.isValid()).toBeFalsy()
})
it('should validate an atom with in or contains operator', () => {
const atom = new CustomFieldQueryAtom([1, 'in', '[1,2,3]'])
expect(model['validateAtom'].apply(null, [atom])).toBeTruthy()
atom.operator = 'contains'
atom.value = [1, 2, 3]
expect(model['validateAtom'].apply(null, [atom])).toBeTruthy()
atom.value = null
expect(model['validateAtom'].apply(null, [atom])).toBeFalsy()
})
it('should check if model is empty', () => {
expect(model.isEmpty()).toBeTruthy()
model.addExpression()
expect(model.isEmpty()).toBeTruthy()
const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
model.addAtom(atom)
expect(model.isEmpty()).toBeFalsy()
})
it('should add an atom to the model', () => {
const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
model.addAtom(atom)
expect(model.queries.length).toBe(1)
expect(
(model.queries[0] as CustomFieldQueryExpression).value.length
).toBe(1)
})
it('should add an expression to the model, propagate changes', () => {
const expression = new CustomFieldQueryExpression()
model.addExpression(expression)
expect(model.queries.length).toBe(1)
const expression2 = new CustomFieldQueryExpression([
CustomFieldQueryLogicalOperator.And,
[
[1, 'icontains', 'test'],
[2, 'icontains', 'test'],
],
])
model.addExpression(expression2)
const nextSpy = jest.spyOn(model.changed, 'next')
expression2.changed.next(expression2)
expect(nextSpy).toHaveBeenCalled()
})
it('should remove an element from the model', () => {
const expression = new CustomFieldQueryExpression([
CustomFieldQueryLogicalOperator.And,
[
[1, 'icontains', 'test'],
[2, 'icontains', 'test'],
],
])
const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
const expression2 = new CustomFieldQueryExpression([
CustomFieldQueryLogicalOperator.And,
[
[3, 'icontains', 'test'],
[4, 'icontains', 'test'],
],
])
expression.addAtom(atom)
expression2.addExpression(expression)
model.addExpression(expression2)
model.removeElement(atom)
expect(model.queries.length).toBe(1)
model.removeElement(expression2)
})
it('should fire changed event when an atom changes', () => {
const nextSpy = jest.spyOn(model.changed, 'next')
const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
model.addAtom(atom)
atom.changed.next(atom)
expect(nextSpy).toHaveBeenCalledWith(model)
})
it('should complete changed subject when element is removed', () => {
const expression = new CustomFieldQueryExpression()
const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
;(expression.value as CustomFieldQueryElement[]).push(atom)
model.addExpression(expression)
const completeSpy = jest.spyOn(atom.changed, 'complete')
model.removeElement(atom)
expect(completeSpy).toHaveBeenCalled()
})
})
})