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.Exact
119 ...CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[
120 CustomFieldQueryOperatorGroups.String
125 // Fallback to basic operators if field is not found
126 const operators2 = component.getOperatorsForField(2)
127 expect(operators2.length).toEqual(
128 CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[
129 CustomFieldQueryOperatorGroups.Basic
134 it('should get select options for a field', () => {
135 const field: CustomField = {
138 data_type: CustomFieldDataType.Select,
141 { label: 'Option 1', id: 'abc-123' },
142 { label: 'Option 2', id: 'def-456' },
146 component.customFields = [field]
147 const options = component.getSelectOptionsForField(1)
148 expect(options).toEqual([
149 { label: 'Option 1', id: 'abc-123' },
150 { label: 'Option 2', id: 'def-456' },
153 // Fallback to empty array if field is not found
154 const options2 = component.getSelectOptionsForField(2)
155 expect(options2).toEqual([])
158 it('should remove an element from the selection model', () => {
159 const expression = new CustomFieldQueryExpression()
160 const atom = new CustomFieldQueryAtom()
161 ;(expression.value as CustomFieldQueryElement[]).push(atom)
162 component.selectionModel.addExpression(expression)
163 component.removeElement(atom)
164 expect(component.selectionModel.isEmpty()).toBeTruthy()
165 const expression2 = new CustomFieldQueryExpression([
166 CustomFieldQueryLogicalOperator.And,
168 [1, 'icontains', 'test'],
169 [2, 'icontains', 'test'],
172 component.selectionModel.addExpression(expression2)
173 component.removeElement(expression2)
174 expect(component.selectionModel.isEmpty()).toBeTruthy()
177 it('should emit selectionModelChange when model changes', () => {
178 const nextSpy = jest.spyOn(component.selectionModelChange, 'next')
179 const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
180 component.selectionModel.addAtom(atom)
181 atom.changed.next(atom)
182 expect(nextSpy).toHaveBeenCalled()
185 it('should complete selection model subscription when new selection model is set', () => {
186 const completeSpy = jest.spyOn(component.selectionModel.changed, 'complete')
187 const selectionModel = new CustomFieldQueriesModel()
188 component.selectionModel = selectionModel
189 expect(completeSpy).toHaveBeenCalled()
192 it('should support adding an atom', () => {
193 const expression = new CustomFieldQueryExpression()
194 component.addAtom(expression)
195 expect(expression.value.length).toBe(1)
198 it('should support adding an expression', () => {
199 const expression = new CustomFieldQueryExpression()
200 component.addExpression(expression)
201 expect(expression.value.length).toBe(1)
204 it('should support getting a custom field by ID', () => {
205 expect(component.getCustomFieldByID(1)).toEqual(customFields[0])
208 it('should sanitize name from title', () => {
209 component.title = 'Test Title'
210 expect(component.name).toBe('test_title')
213 it('should add a default atom on open and focus the select field', fakeAsync(() => {
214 expect(component.selectionModel.queries.length).toBe(0)
215 component.onOpenChange(true)
216 fixture.detectChanges()
218 expect(component.selectionModel.queries.length).toBe(1)
219 expect(window.document.activeElement.tagName).toBe('INPUT')
222 describe('CustomFieldQueriesModel', () => {
223 let model: CustomFieldQueriesModel
226 model = new CustomFieldQueriesModel()
229 it('should initialize with empty queries', () => {
230 expect(model.queries).toEqual([])
233 it('should clear queries and fire event', () => {
234 const nextSpy = jest.spyOn(model.changed, 'next')
235 model.addExpression()
237 expect(model.queries).toEqual([])
238 expect(nextSpy).toHaveBeenCalledWith(model)
241 it('should clear queries without firing event', () => {
242 const nextSpy = jest.spyOn(model.changed, 'next')
243 model.addExpression()
245 expect(model.queries).toEqual([])
246 expect(nextSpy).not.toHaveBeenCalled()
249 it('should validate an empty model as invalid', () => {
250 expect(model.isValid()).toBeFalsy()
253 it('should validate a model with valid expression as valid', () => {
254 const expression = new CustomFieldQueryExpression()
255 const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
256 const atom2 = new CustomFieldQueryAtom([2, 'icontains', 'test'])
257 const expression2 = new CustomFieldQueryExpression()
258 expression2.addAtom(atom)
259 expression2.addAtom(atom2)
260 expression.addExpression(expression2)
261 model.addExpression(expression)
262 expect(model.isValid()).toBeTruthy()
265 it('should validate a model with invalid expression as invalid', () => {
266 const expression = new CustomFieldQueryExpression()
267 model.addExpression(expression)
268 expect(model.isValid()).toBeFalsy()
271 it('should validate an atom with in or contains operator', () => {
272 const atom = new CustomFieldQueryAtom([1, 'in', '[1,2,3]'])
273 expect(model['validateAtom'].apply(null, [atom])).toBeTruthy()
274 atom.operator = 'contains'
275 atom.value = [1, 2, 3]
276 expect(model['validateAtom'].apply(null, [atom])).toBeTruthy()
278 expect(model['validateAtom'].apply(null, [atom])).toBeFalsy()
281 it('should check if model is empty', () => {
282 expect(model.isEmpty()).toBeTruthy()
283 model.addExpression()
284 expect(model.isEmpty()).toBeTruthy()
285 const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
287 expect(model.isEmpty()).toBeFalsy()
290 it('should add an atom to the model', () => {
291 const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
293 expect(model.queries.length).toBe(1)
295 (model.queries[0] as CustomFieldQueryExpression).value.length
299 it('should add an expression to the model, propagate changes', () => {
300 const expression = new CustomFieldQueryExpression()
301 model.addExpression(expression)
302 expect(model.queries.length).toBe(1)
303 const expression2 = new CustomFieldQueryExpression([
304 CustomFieldQueryLogicalOperator.And,
306 [1, 'icontains', 'test'],
307 [2, 'icontains', 'test'],
310 model.addExpression(expression2)
311 const nextSpy = jest.spyOn(model.changed, 'next')
312 expression2.changed.next(expression2)
313 expect(nextSpy).toHaveBeenCalled()
316 it('should remove an element from the model', () => {
317 const expression = new CustomFieldQueryExpression([
318 CustomFieldQueryLogicalOperator.And,
320 [1, 'icontains', 'test'],
321 [2, 'icontains', 'test'],
324 const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
325 const expression2 = new CustomFieldQueryExpression([
326 CustomFieldQueryLogicalOperator.And,
328 [3, 'icontains', 'test'],
329 [4, 'icontains', 'test'],
332 expression.addAtom(atom)
333 expression2.addExpression(expression)
334 model.addExpression(expression2)
335 model.removeElement(atom)
336 expect(model.queries.length).toBe(1)
337 model.removeElement(expression2)
340 it('should fire changed event when an atom changes', () => {
341 const nextSpy = jest.spyOn(model.changed, 'next')
342 const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
344 atom.changed.next(atom)
345 expect(nextSpy).toHaveBeenCalledWith(model)
348 it('should complete changed subject when element is removed', () => {
349 const expression = new CustomFieldQueryExpression()
350 const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
351 ;(expression.value as CustomFieldQueryElement[]).push(atom)
352 model.addExpression(expression)
353 const completeSpy = jest.spyOn(atom.changed, 'complete')
354 model.removeElement(atom)
355 expect(completeSpy).toHaveBeenCalled()