1 import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
2 import { provideHttpClientTesting } from '@angular/common/http/testing'
8 } from '@angular/core/testing'
9 import { FormsModule, ReactiveFormsModule } from '@angular/forms'
10 import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
11 import { NgSelectModule } from '@ng-select/ng-select'
12 import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
13 import { of } from 'rxjs'
14 import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
16 CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP,
17 CustomFieldQueryLogicalOperator,
18 CustomFieldQueryOperatorGroups,
19 } from 'src/app/data/custom-field-query'
20 import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
23 CustomFieldQueryElement,
24 CustomFieldQueryExpression,
25 } from 'src/app/utils/custom-field-query-element'
27 CustomFieldQueriesModel,
28 CustomFieldsQueryDropdownComponent,
29 } from './custom-fields-query-dropdown.component'
31 const customFields = [
35 data_type: CustomFieldDataType.String,
40 name: 'Test Select Field',
41 data_type: CustomFieldDataType.Select,
44 { label: 'Option 1', id: 'abc-123' },
45 { label: 'Option 2', id: 'def-456' },
51 describe('CustomFieldsQueryDropdownComponent', () => {
52 let component: CustomFieldsQueryDropdownComponent
53 let fixture: ComponentFixture<CustomFieldsQueryDropdownComponent>
54 let customFieldsService: CustomFieldsService
56 beforeEach(async () => {
57 await TestBed.configureTestingModule({
60 NgxBootstrapIconsModule.pick(allIcons),
64 CustomFieldsQueryDropdownComponent,
67 provideHttpClient(withInterceptorsFromDi()),
68 provideHttpClientTesting(),
70 }).compileComponents()
72 customFieldsService = TestBed.inject(CustomFieldsService)
73 jest.spyOn(customFieldsService, 'listAll').mockReturnValue(
75 count: customFields.length,
76 all: customFields.map((f) => f.id),
77 results: customFields,
80 fixture = TestBed.createComponent(CustomFieldsQueryDropdownComponent)
81 component = fixture.componentInstance
82 component.icon = 'ui-radios'
83 fixture.detectChanges()
86 it('should initialize custom fields on creation', () => {
87 expect(component.customFields).toEqual(customFields)
90 it('should add an expression when opened if queries are empty', () => {
91 component.selectionModel.clear()
92 component.onOpenChange(true)
93 expect(component.selectionModel.queries.length).toBe(1)
96 it('should support reset the selection model', () => {
97 component.selectionModel.addExpression()
99 expect(component.selectionModel.isEmpty()).toBeTruthy()
102 it('should get operators for a field', () => {
103 const field: CustomField = {
106 data_type: CustomFieldDataType.String,
109 component.customFields = [field]
110 const operators = component.getOperatorsForField(1)
111 expect(operators.length).toEqual(
113 ...CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[
114 CustomFieldQueryOperatorGroups.Basic
116 ...CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[
117 CustomFieldQueryOperatorGroups.String
122 // Fallback to basic operators if field is not found
123 const operators2 = component.getOperatorsForField(2)
124 expect(operators2.length).toEqual(
125 CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[
126 CustomFieldQueryOperatorGroups.Basic
131 it('should get select options for a field', () => {
132 const field: CustomField = {
135 data_type: CustomFieldDataType.Select,
138 { label: 'Option 1', id: 'abc-123' },
139 { label: 'Option 2', id: 'def-456' },
143 component.customFields = [field]
144 const options = component.getSelectOptionsForField(1)
145 expect(options).toEqual([
146 { label: 'Option 1', id: 'abc-123' },
147 { label: 'Option 2', id: 'def-456' },
150 // Fallback to empty array if field is not found
151 const options2 = component.getSelectOptionsForField(2)
152 expect(options2).toEqual([])
155 it('should remove an element from the selection model', () => {
156 const expression = new CustomFieldQueryExpression()
157 const atom = new CustomFieldQueryAtom()
158 ;(expression.value as CustomFieldQueryElement[]).push(atom)
159 component.selectionModel.addExpression(expression)
160 component.removeElement(atom)
161 expect(component.selectionModel.isEmpty()).toBeTruthy()
162 const expression2 = new CustomFieldQueryExpression([
163 CustomFieldQueryLogicalOperator.And,
165 [1, 'icontains', 'test'],
166 [2, 'icontains', 'test'],
169 component.selectionModel.addExpression(expression2)
170 component.removeElement(expression2)
171 expect(component.selectionModel.isEmpty()).toBeTruthy()
174 it('should emit selectionModelChange when model changes', () => {
175 const nextSpy = jest.spyOn(component.selectionModelChange, 'next')
176 const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
177 component.selectionModel.addAtom(atom)
178 atom.changed.next(atom)
179 expect(nextSpy).toHaveBeenCalled()
182 it('should complete selection model subscription when new selection model is set', () => {
183 const completeSpy = jest.spyOn(component.selectionModel.changed, 'complete')
184 const selectionModel = new CustomFieldQueriesModel()
185 component.selectionModel = selectionModel
186 expect(completeSpy).toHaveBeenCalled()
189 it('should support adding an atom', () => {
190 const expression = new CustomFieldQueryExpression()
191 component.addAtom(expression)
192 expect(expression.value.length).toBe(1)
195 it('should support adding an expression', () => {
196 const expression = new CustomFieldQueryExpression()
197 component.addExpression(expression)
198 expect(expression.value.length).toBe(1)
201 it('should support getting a custom field by ID', () => {
202 expect(component.getCustomFieldByID(1)).toEqual(customFields[0])
205 it('should sanitize name from title', () => {
206 component.title = 'Test Title'
207 expect(component.name).toBe('test_title')
210 it('should add a default atom on open and focus the select field', fakeAsync(() => {
211 expect(component.selectionModel.queries.length).toBe(0)
212 component.onOpenChange(true)
213 fixture.detectChanges()
215 expect(component.selectionModel.queries.length).toBe(1)
216 expect(window.document.activeElement.tagName).toBe('INPUT')
219 describe('CustomFieldQueriesModel', () => {
220 let model: CustomFieldQueriesModel
223 model = new CustomFieldQueriesModel()
226 it('should initialize with empty queries', () => {
227 expect(model.queries).toEqual([])
230 it('should clear queries and fire event', () => {
231 const nextSpy = jest.spyOn(model.changed, 'next')
232 model.addExpression()
234 expect(model.queries).toEqual([])
235 expect(nextSpy).toHaveBeenCalledWith(model)
238 it('should clear queries without firing event', () => {
239 const nextSpy = jest.spyOn(model.changed, 'next')
240 model.addExpression()
242 expect(model.queries).toEqual([])
243 expect(nextSpy).not.toHaveBeenCalled()
246 it('should validate an empty model as invalid', () => {
247 expect(model.isValid()).toBeFalsy()
250 it('should validate a model with valid expression as valid', () => {
251 const expression = new CustomFieldQueryExpression()
252 const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
253 const atom2 = new CustomFieldQueryAtom([2, 'icontains', 'test'])
254 const expression2 = new CustomFieldQueryExpression()
255 expression2.addAtom(atom)
256 expression2.addAtom(atom2)
257 expression.addExpression(expression2)
258 model.addExpression(expression)
259 expect(model.isValid()).toBeTruthy()
262 it('should validate a model with invalid expression as invalid', () => {
263 const expression = new CustomFieldQueryExpression()
264 model.addExpression(expression)
265 expect(model.isValid()).toBeFalsy()
268 it('should validate an atom with in or contains operator', () => {
269 const atom = new CustomFieldQueryAtom([1, 'in', '[1,2,3]'])
270 expect(model['validateAtom'].apply(null, [atom])).toBeTruthy()
271 atom.operator = 'contains'
272 atom.value = [1, 2, 3]
273 expect(model['validateAtom'].apply(null, [atom])).toBeTruthy()
275 expect(model['validateAtom'].apply(null, [atom])).toBeFalsy()
278 it('should check if model is empty', () => {
279 expect(model.isEmpty()).toBeTruthy()
280 model.addExpression()
281 expect(model.isEmpty()).toBeTruthy()
282 const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
284 expect(model.isEmpty()).toBeFalsy()
287 it('should add an atom to the model', () => {
288 const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
290 expect(model.queries.length).toBe(1)
292 (model.queries[0] as CustomFieldQueryExpression).value.length
296 it('should add an expression to the model, propagate changes', () => {
297 const expression = new CustomFieldQueryExpression()
298 model.addExpression(expression)
299 expect(model.queries.length).toBe(1)
300 const expression2 = new CustomFieldQueryExpression([
301 CustomFieldQueryLogicalOperator.And,
303 [1, 'icontains', 'test'],
304 [2, 'icontains', 'test'],
307 model.addExpression(expression2)
308 const nextSpy = jest.spyOn(model.changed, 'next')
309 expression2.changed.next(expression2)
310 expect(nextSpy).toHaveBeenCalled()
313 it('should remove an element from the model', () => {
314 const expression = new CustomFieldQueryExpression([
315 CustomFieldQueryLogicalOperator.And,
317 [1, 'icontains', 'test'],
318 [2, 'icontains', 'test'],
321 const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
322 const expression2 = new CustomFieldQueryExpression([
323 CustomFieldQueryLogicalOperator.And,
325 [3, 'icontains', 'test'],
326 [4, 'icontains', 'test'],
329 expression.addAtom(atom)
330 expression2.addExpression(expression)
331 model.addExpression(expression2)
332 model.removeElement(atom)
333 expect(model.queries.length).toBe(1)
334 model.removeElement(expression2)
337 it('should fire changed event when an atom changes', () => {
338 const nextSpy = jest.spyOn(model.changed, 'next')
339 const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
341 atom.changed.next(atom)
342 expect(nextSpy).toHaveBeenCalledWith(model)
345 it('should complete changed subject when element is removed', () => {
346 const expression = new CustomFieldQueryExpression()
347 const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
348 ;(expression.value as CustomFieldQueryElement[]).push(atom)
349 model.addExpression(expression)
350 const completeSpy = jest.spyOn(atom.changed, 'complete')
351 model.removeElement(atom)
352 expect(completeSpy).toHaveBeenCalled()