mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-29 11:09:27 -05:00
356 lines
12 KiB
TypeScript
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()
|
|
})
|
|
})
|
|
})
|