]> git.ipfire.org Git - thirdparty/paperless-ngx.git/blob
4dcbceb13aa5ea2595aed4402bbabba873fcf91b
[thirdparty/paperless-ngx.git] /
1 import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
2 import { provideHttpClientTesting } from '@angular/common/http/testing'
3 import {
4 ComponentFixture,
5 fakeAsync,
6 TestBed,
7 tick,
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'
15 import {
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'
21 import {
22 CustomFieldQueryAtom,
23 CustomFieldQueryElement,
24 CustomFieldQueryExpression,
25 } from 'src/app/utils/custom-field-query-element'
26 import {
27 CustomFieldQueriesModel,
28 CustomFieldsQueryDropdownComponent,
29 } from './custom-fields-query-dropdown.component'
30
31 const customFields = [
32 {
33 id: 1,
34 name: 'Test Field',
35 data_type: CustomFieldDataType.String,
36 extra_data: {},
37 },
38 {
39 id: 2,
40 name: 'Test Select Field',
41 data_type: CustomFieldDataType.Select,
42 extra_data: {
43 select_options: [
44 { label: 'Option 1', id: 'abc-123' },
45 { label: 'Option 2', id: 'def-456' },
46 ],
47 },
48 },
49 ]
50
51 describe('CustomFieldsQueryDropdownComponent', () => {
52 let component: CustomFieldsQueryDropdownComponent
53 let fixture: ComponentFixture<CustomFieldsQueryDropdownComponent>
54 let customFieldsService: CustomFieldsService
55
56 beforeEach(async () => {
57 await TestBed.configureTestingModule({
58 imports: [
59 NgbDropdownModule,
60 NgxBootstrapIconsModule.pick(allIcons),
61 NgSelectModule,
62 FormsModule,
63 ReactiveFormsModule,
64 CustomFieldsQueryDropdownComponent,
65 ],
66 providers: [
67 provideHttpClient(withInterceptorsFromDi()),
68 provideHttpClientTesting(),
69 ],
70 }).compileComponents()
71
72 customFieldsService = TestBed.inject(CustomFieldsService)
73 jest.spyOn(customFieldsService, 'listAll').mockReturnValue(
74 of({
75 count: customFields.length,
76 all: customFields.map((f) => f.id),
77 results: customFields,
78 })
79 )
80 fixture = TestBed.createComponent(CustomFieldsQueryDropdownComponent)
81 component = fixture.componentInstance
82 component.icon = 'ui-radios'
83 fixture.detectChanges()
84 })
85
86 it('should initialize custom fields on creation', () => {
87 expect(component.customFields).toEqual(customFields)
88 })
89
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)
94 })
95
96 it('should support reset the selection model', () => {
97 component.selectionModel.addExpression()
98 component.reset()
99 expect(component.selectionModel.isEmpty()).toBeTruthy()
100 })
101
102 it('should get operators for a field', () => {
103 const field: CustomField = {
104 id: 1,
105 name: 'Test Field',
106 data_type: CustomFieldDataType.String,
107 extra_data: {},
108 }
109 component.customFields = [field]
110 const operators = component.getOperatorsForField(1)
111 expect(operators.length).toEqual(
112 [
113 ...CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[
114 CustomFieldQueryOperatorGroups.Basic
115 ],
116 ...CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[
117 CustomFieldQueryOperatorGroups.Exact
118 ],
119 ...CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[
120 CustomFieldQueryOperatorGroups.String
121 ],
122 ].length
123 )
124
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
130 ].length
131 )
132 })
133
134 it('should get select options for a field', () => {
135 const field: CustomField = {
136 id: 1,
137 name: 'Test Field',
138 data_type: CustomFieldDataType.Select,
139 extra_data: {
140 select_options: [
141 { label: 'Option 1', id: 'abc-123' },
142 { label: 'Option 2', id: 'def-456' },
143 ],
144 },
145 }
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' },
151 ])
152
153 // Fallback to empty array if field is not found
154 const options2 = component.getSelectOptionsForField(2)
155 expect(options2).toEqual([])
156 })
157
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,
167 [
168 [1, 'icontains', 'test'],
169 [2, 'icontains', 'test'],
170 ],
171 ])
172 component.selectionModel.addExpression(expression2)
173 component.removeElement(expression2)
174 expect(component.selectionModel.isEmpty()).toBeTruthy()
175 })
176
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()
183 })
184
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()
190 })
191
192 it('should support adding an atom', () => {
193 const expression = new CustomFieldQueryExpression()
194 component.addAtom(expression)
195 expect(expression.value.length).toBe(1)
196 })
197
198 it('should support adding an expression', () => {
199 const expression = new CustomFieldQueryExpression()
200 component.addExpression(expression)
201 expect(expression.value.length).toBe(1)
202 })
203
204 it('should support getting a custom field by ID', () => {
205 expect(component.getCustomFieldByID(1)).toEqual(customFields[0])
206 })
207
208 it('should sanitize name from title', () => {
209 component.title = 'Test Title'
210 expect(component.name).toBe('test_title')
211 })
212
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()
217 tick()
218 expect(component.selectionModel.queries.length).toBe(1)
219 expect(window.document.activeElement.tagName).toBe('INPUT')
220 }))
221
222 describe('CustomFieldQueriesModel', () => {
223 let model: CustomFieldQueriesModel
224
225 beforeEach(() => {
226 model = new CustomFieldQueriesModel()
227 })
228
229 it('should initialize with empty queries', () => {
230 expect(model.queries).toEqual([])
231 })
232
233 it('should clear queries and fire event', () => {
234 const nextSpy = jest.spyOn(model.changed, 'next')
235 model.addExpression()
236 model.clear()
237 expect(model.queries).toEqual([])
238 expect(nextSpy).toHaveBeenCalledWith(model)
239 })
240
241 it('should clear queries without firing event', () => {
242 const nextSpy = jest.spyOn(model.changed, 'next')
243 model.addExpression()
244 model.clear(false)
245 expect(model.queries).toEqual([])
246 expect(nextSpy).not.toHaveBeenCalled()
247 })
248
249 it('should validate an empty model as invalid', () => {
250 expect(model.isValid()).toBeFalsy()
251 })
252
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()
263 })
264
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()
269 })
270
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()
277 atom.value = null
278 expect(model['validateAtom'].apply(null, [atom])).toBeFalsy()
279 })
280
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'])
286 model.addAtom(atom)
287 expect(model.isEmpty()).toBeFalsy()
288 })
289
290 it('should add an atom to the model', () => {
291 const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
292 model.addAtom(atom)
293 expect(model.queries.length).toBe(1)
294 expect(
295 (model.queries[0] as CustomFieldQueryExpression).value.length
296 ).toBe(1)
297 })
298
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,
305 [
306 [1, 'icontains', 'test'],
307 [2, 'icontains', 'test'],
308 ],
309 ])
310 model.addExpression(expression2)
311 const nextSpy = jest.spyOn(model.changed, 'next')
312 expression2.changed.next(expression2)
313 expect(nextSpy).toHaveBeenCalled()
314 })
315
316 it('should remove an element from the model', () => {
317 const expression = new CustomFieldQueryExpression([
318 CustomFieldQueryLogicalOperator.And,
319 [
320 [1, 'icontains', 'test'],
321 [2, 'icontains', 'test'],
322 ],
323 ])
324 const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
325 const expression2 = new CustomFieldQueryExpression([
326 CustomFieldQueryLogicalOperator.And,
327 [
328 [3, 'icontains', 'test'],
329 [4, 'icontains', 'test'],
330 ],
331 ])
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)
338 })
339
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'])
343 model.addAtom(atom)
344 atom.changed.next(atom)
345 expect(nextSpy).toHaveBeenCalledWith(model)
346 })
347
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()
356 })
357 })
358 })