From b0970adacafca1dee3f1a3a7da3f759259516ac0 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 14 Sep 2024 23:43:31 -0700 Subject: [PATCH] Full coverage, almost lost it --- .../filter-editor.component.spec.ts | 60 +++++---- .../utils/custom-field-query-element.spec.ts | 121 ++++++++++++++++-- .../app/utils/custom-field-query-element.ts | 59 ++++----- 3 files changed, 168 insertions(+), 72 deletions(-) diff --git a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.spec.ts b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.spec.ts index a84146838..489beff96 100644 --- a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.spec.ts +++ b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.spec.ts @@ -98,10 +98,10 @@ import { SearchService } from 'src/app/services/rest/search.service' import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' import { CustomFieldsQueryDropdownComponent } from '../../common/custom-fields-query-dropdown/custom-fields-query-dropdown.component' import { - CustomFieldQueryAtom, CustomFieldQueryLogicalOperator, CustomFieldQueryOperator, } from 'src/app/data/custom-field-query' +import { CustomFieldQueryAtom } from 'src/app/utils/custom-field-query-element' const tags: Tag[] = [ { @@ -886,31 +886,39 @@ describe('FilterEditorComponent', () => { ).toEqual(['42', CustomFieldQueryOperator.Exists, 'true']) })) - it('should ingest filter rules for exclude tag(s)', fakeAsync(() => { - // expect(component.customFieldQueriesModel.getExcludedItems()).toHaveLength(0) - // component.filterRules = [ - // { - // rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS, - // value: '42', - // }, - // { - // rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS, - // value: '43', - // }, - // ] - // expect(component.customFieldQueriesModel.logicalOperator).toEqual( - // LogicalOperator.And - // ) - // expect(component.customFieldQueriesModel.getExcludedItems()).toEqual( - // custom_fields - // ) - // // coverage - // component.filterRules = [ - // { - // rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS, - // value: null, - // }, - // ] + it('should ingest filter rules for custom field lookup', fakeAsync(() => { + expect(component.customFieldQueriesModel.isEmpty()).toBeTruthy() + component.filterRules = [ + { + rule_type: FILTER_CUSTOM_FIELDS_LOOKUP, + value: '["AND", [[42, "exists", "true"],[43, "exists", "true"]]]', + }, + ] + expect(component.customFieldQueriesModel.queries[0].operator).toEqual( + CustomFieldQueryLogicalOperator.And + ) + expect(component.customFieldQueriesModel.queries[0].value.length).toEqual(2) + expect( + ( + component.customFieldQueriesModel.queries[0] + .value[0] as CustomFieldQueryAtom + ).serialize() + ).toEqual([42, CustomFieldQueryOperator.Exists, 'true']) + + // atom + component.filterRules = [ + { + rule_type: FILTER_CUSTOM_FIELDS_LOOKUP, + value: '[42, "exists", "true"]', + }, + ] + expect(component.customFieldQueriesModel.queries[0].value.length).toEqual(1) + expect( + ( + component.customFieldQueriesModel.queries[0] + .value[0] as CustomFieldQueryAtom + ).serialize() + ).toEqual([42, CustomFieldQueryOperator.Exists, 'true']) })) it('should ingest filter rules for owner', fakeAsync(() => { diff --git a/src-ui/src/app/utils/custom-field-query-element.spec.ts b/src-ui/src/app/utils/custom-field-query-element.spec.ts index 2150f1560..65be3738a 100644 --- a/src-ui/src/app/utils/custom-field-query-element.spec.ts +++ b/src-ui/src/app/utils/custom-field-query-element.spec.ts @@ -8,6 +8,7 @@ import { CustomFieldQueryLogicalOperator, CustomFieldQueryOperator, } from '../data/custom-field-query' +import { fakeAsync, tick } from '@angular/core/testing' describe('CustomFieldQueryElement', () => { it('should initialize with correct type and id', () => { @@ -86,23 +87,95 @@ describe('CustomFieldQueryAtom', () => { expect(atom.value).toEqual([]) }) + it('should try to set existing value to number if new type is number', () => { + const atom = new CustomFieldQueryAtom() + atom.value = '42' + atom.operator = CustomFieldQueryOperator.GreaterThan + expect(atom.value).toBe('42') + + // fallback to null if value is not parseable + atom.value = 'not_a_number' + atom.operator = CustomFieldQueryOperator.GreaterThan + expect(atom.value).toBeNull() + }) + + it('should change boolean values to empty string if operator is not boolean', () => { + const atom = new CustomFieldQueryAtom() + atom.value = 'true' + atom.operator = CustomFieldQueryOperator.Exact + expect(atom.value).toBe('') + }) + it('should serialize correctly', () => { const atom = new CustomFieldQueryAtom([1, 'operator', 'value']) expect(atom.serialize()).toEqual([1, 'operator', 'value']) }) + + it('should emit changed on value change after debounce', fakeAsync(() => { + const atom = new CustomFieldQueryAtom() + const changeSpy = jest.spyOn(atom.changed, 'next') + atom.value = 'new value' + tick(1000) + expect(changeSpy).toHaveBeenCalled() + })) }) describe('CustomFieldQueryExpression', () => { - it('should initialize with correct operator and value', () => { - const expression = new CustomFieldQueryExpression([ - CustomFieldQueryLogicalOperator.And, - [], - ]) - expect(expression.operator).toBe(CustomFieldQueryLogicalOperator.And) + it('should initialize with default operator and empty value', () => { + const expression = new CustomFieldQueryExpression() + expect(expression.operator).toBe(CustomFieldQueryLogicalOperator.Or) expect(expression.value).toEqual([]) }) - it('should add atom correctly', () => { + it('should initialize with correct operator and value, propagate changes', () => { + const expression = new CustomFieldQueryExpression([ + CustomFieldQueryLogicalOperator.And, + [ + [1, 'exists', 'true'], + [2, 'exists', 'true'], + ], + ]) + expect(expression.operator).toBe(CustomFieldQueryLogicalOperator.And) + expect(expression.value.length).toBe(2) + + // propagate changes + const expressionChangeSpy = jest.spyOn(expression.changed, 'next') + ;(expression.value[0] as CustomFieldQueryAtom).changed.next( + expression.value[0] as any + ) + expect(expressionChangeSpy).toHaveBeenCalled() + + const expression2 = new CustomFieldQueryExpression([ + CustomFieldQueryLogicalOperator.Not, + [[CustomFieldQueryLogicalOperator.Or, []]], + ]) + const expressionChangeSpy2 = jest.spyOn(expression2.changed, 'next') + ;(expression2.value[0] as CustomFieldQueryExpression).changed.next( + expression2.value[0] as any + ) + expect(expressionChangeSpy2).toHaveBeenCalled() + }) + + it('should initialize with a sub-expression i.e. NOT', () => { + const expression = new CustomFieldQueryExpression([ + CustomFieldQueryLogicalOperator.Not, + [ + 'AND', + [ + [1, 'exists', 'true'], + [2, 'exists', 'true'], + ], + ], + ]) + expect(expression.value).toHaveLength(1) + const changedSpy = jest.spyOn(expression.changed, 'next') + ;(expression.value[0] as CustomFieldQueryExpression).changed.next( + expression.value[0] as any + ) + expect(changedSpy).toHaveBeenCalled() + }) + + it('should add atom correctly, propagate changes', () => { const expression = new CustomFieldQueryExpression() const atom = new CustomFieldQueryAtom([ 1, @@ -111,9 +184,14 @@ describe('CustomFieldQueryExpression', () => { ]) expression.addAtom(atom) expect(expression.value).toContain(atom) + const changeSpy = jest.spyOn(expression.changed, 'next') + atom.changed.next(atom) + expect(changeSpy).toHaveBeenCalled() + // coverage + expression.addAtom() }) - it('should add expression correctly', () => { + it('should add expression correctly, propagate changes', () => { const expression = new CustomFieldQueryExpression() const subExpression = new CustomFieldQueryExpression([ CustomFieldQueryLogicalOperator.Or, @@ -121,19 +199,42 @@ describe('CustomFieldQueryExpression', () => { ]) expression.addExpression(subExpression) expect(expression.value).toContain(subExpression) + const changeSpy = jest.spyOn(expression.changed, 'next') + subExpression.changed.next(subExpression) + expect(changeSpy).toHaveBeenCalled() + // coverage + expression.addExpression() }) it('should serialize correctly', () => { const expression = new CustomFieldQueryExpression([ CustomFieldQueryLogicalOperator.And, - [[1, 'operator', 'value']], + [[1, 'exists', 'true']], ]) expect(expression.serialize()).toEqual([ CustomFieldQueryLogicalOperator.And, - [[1, 'operator', 'value']], + [[1, 'exists', 'true']], ]) }) + it('should serialize NOT expressions correctly', () => { + const expression = new CustomFieldQueryExpression() + expression.addExpression( + new CustomFieldQueryExpression([ + CustomFieldQueryLogicalOperator.And, + [ + [1, 'exists', 'true'], + [2, 'exists', 'true'], + ], + ]) + ) + expression.operator = CustomFieldQueryLogicalOperator.Not + const serialized = expression.serialize() + expect(serialized[0]).toBe(CustomFieldQueryLogicalOperator.Not) + expect(serialized[1][0]).toBe(CustomFieldQueryLogicalOperator.And) + expect(serialized[1][1].length).toBe(2) + }) + it('should be negatable if it has one child which is an expression', () => { const expression = new CustomFieldQueryExpression([ CustomFieldQueryLogicalOperator.Not, diff --git a/src-ui/src/app/utils/custom-field-query-element.ts b/src-ui/src/app/utils/custom-field-query-element.ts index 90bab9c52..696853f12 100644 --- a/src-ui/src/app/utils/custom-field-query-element.ts +++ b/src-ui/src/app/utils/custom-field-query-element.ts @@ -73,31 +73,20 @@ export class CustomFieldQueryAtom extends CustomFieldQueryElement { this.value = null } else { if (!newTypes.includes(typeof this.value)) { - if (newTypes.length === 1) { - switch (newTypes[0]) { - case 'string': - this.value = '' - break - case 'boolean': - this.value = 'true' - break - case 'array': - this.value = [] - break - default: - this.value = null - break - } - } else { - if (newTypes.includes('number')) { - try { - this.value = parseFloat(this.value as string).toString() - } catch (e) { - this.value = null - } - } else { - this.value = null - } + switch (newTypes[0]) { + case 'string': + this.value = '' + break + case 'boolean': + this.value = 'true' + break + case 'array': + this.value = [] + break + case 'number': + const num = parseFloat(this.value as string) + this.value = isNaN(num) ? null : num.toString() + break } } else if ( ['true', 'false'].includes(this.value as string) && @@ -133,6 +122,8 @@ export class CustomFieldQueryAtom extends CustomFieldQueryElement { } export class CustomFieldQueryExpression extends CustomFieldQueryElement { + protected _value: string[] | number[] | CustomFieldQueryElement[] + constructor( expressionArray: [CustomFieldQueryLogicalOperator, any[]] = [ CustomFieldQueryLogicalOperator.Or, @@ -174,17 +165,13 @@ export class CustomFieldQueryExpression extends CustomFieldQueryElement { public override serialize() { let value - if (this._value instanceof Array) { - value = this._value.map((atom) => atom.serialize()) - // If the expression is negated it should have only one child which is an expression - if ( - this._operator === CustomFieldQueryLogicalOperator.Not && - value.length === 1 - ) { - value = value[0] - } - } else { - value = value.serialize() + value = this._value.map((element) => element.serialize()) + // If the expression is negated it should have only one child which is an expression + if ( + this._operator === CustomFieldQueryLogicalOperator.Not && + value.length === 1 + ) { + value = value[0] } return [this._operator, value] }