]> git.ipfire.org Git - thirdparty/paperless-ngx.git/blob
7afb5fc1c5b56d9cc27307f18e1ac167a8ba4536
[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.String
118 ],
119 ].length
120 )
121
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
127 ].length
128 )
129 })
130
131 it('should get select options for a field', () => {
132 const field: CustomField = {
133 id: 1,
134 name: 'Test Field',
135 data_type: CustomFieldDataType.Select,
136 extra_data: {
137 select_options: [
138 { label: 'Option 1', id: 'abc-123' },
139 { label: 'Option 2', id: 'def-456' },
140 ],
141 },
142 }
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' },
148 ])
149
150 // Fallback to empty array if field is not found
151 const options2 = component.getSelectOptionsForField(2)
152 expect(options2).toEqual([])
153 })
154
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,
164 [
165 [1, 'icontains', 'test'],
166 [2, 'icontains', 'test'],
167 ],
168 ])
169 component.selectionModel.addExpression(expression2)
170 component.removeElement(expression2)
171 expect(component.selectionModel.isEmpty()).toBeTruthy()
172 })
173
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()
180 })
181
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()
187 })
188
189 it('should support adding an atom', () => {
190 const expression = new CustomFieldQueryExpression()
191 component.addAtom(expression)
192 expect(expression.value.length).toBe(1)
193 })
194
195 it('should support adding an expression', () => {
196 const expression = new CustomFieldQueryExpression()
197 component.addExpression(expression)
198 expect(expression.value.length).toBe(1)
199 })
200
201 it('should support getting a custom field by ID', () => {
202 expect(component.getCustomFieldByID(1)).toEqual(customFields[0])
203 })
204
205 it('should sanitize name from title', () => {
206 component.title = 'Test Title'
207 expect(component.name).toBe('test_title')
208 })
209
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()
214 tick()
215 expect(component.selectionModel.queries.length).toBe(1)
216 expect(window.document.activeElement.tagName).toBe('INPUT')
217 }))
218
219 describe('CustomFieldQueriesModel', () => {
220 let model: CustomFieldQueriesModel
221
222 beforeEach(() => {
223 model = new CustomFieldQueriesModel()
224 })
225
226 it('should initialize with empty queries', () => {
227 expect(model.queries).toEqual([])
228 })
229
230 it('should clear queries and fire event', () => {
231 const nextSpy = jest.spyOn(model.changed, 'next')
232 model.addExpression()
233 model.clear()
234 expect(model.queries).toEqual([])
235 expect(nextSpy).toHaveBeenCalledWith(model)
236 })
237
238 it('should clear queries without firing event', () => {
239 const nextSpy = jest.spyOn(model.changed, 'next')
240 model.addExpression()
241 model.clear(false)
242 expect(model.queries).toEqual([])
243 expect(nextSpy).not.toHaveBeenCalled()
244 })
245
246 it('should validate an empty model as invalid', () => {
247 expect(model.isValid()).toBeFalsy()
248 })
249
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()
260 })
261
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()
266 })
267
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()
274 atom.value = null
275 expect(model['validateAtom'].apply(null, [atom])).toBeFalsy()
276 })
277
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'])
283 model.addAtom(atom)
284 expect(model.isEmpty()).toBeFalsy()
285 })
286
287 it('should add an atom to the model', () => {
288 const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
289 model.addAtom(atom)
290 expect(model.queries.length).toBe(1)
291 expect(
292 (model.queries[0] as CustomFieldQueryExpression).value.length
293 ).toBe(1)
294 })
295
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,
302 [
303 [1, 'icontains', 'test'],
304 [2, 'icontains', 'test'],
305 ],
306 ])
307 model.addExpression(expression2)
308 const nextSpy = jest.spyOn(model.changed, 'next')
309 expression2.changed.next(expression2)
310 expect(nextSpy).toHaveBeenCalled()
311 })
312
313 it('should remove an element from the model', () => {
314 const expression = new CustomFieldQueryExpression([
315 CustomFieldQueryLogicalOperator.And,
316 [
317 [1, 'icontains', 'test'],
318 [2, 'icontains', 'test'],
319 ],
320 ])
321 const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
322 const expression2 = new CustomFieldQueryExpression([
323 CustomFieldQueryLogicalOperator.And,
324 [
325 [3, 'icontains', 'test'],
326 [4, 'icontains', 'test'],
327 ],
328 ])
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)
335 })
336
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'])
340 model.addAtom(atom)
341 atom.changed.next(atom)
342 expect(nextSpy).toHaveBeenCalledWith(model)
343 })
344
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()
353 })
354 })
355 })