Full coverage, almost lost it

This commit is contained in:
shamoon 2024-09-14 23:43:31 -07:00
parent a25be37c8e
commit b0970adaca
3 changed files with 168 additions and 72 deletions

View File

@ -98,10 +98,10 @@ import { SearchService } from 'src/app/services/rest/search.service'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { CustomFieldsQueryDropdownComponent } from '../../common/custom-fields-query-dropdown/custom-fields-query-dropdown.component' import { CustomFieldsQueryDropdownComponent } from '../../common/custom-fields-query-dropdown/custom-fields-query-dropdown.component'
import { import {
CustomFieldQueryAtom,
CustomFieldQueryLogicalOperator, CustomFieldQueryLogicalOperator,
CustomFieldQueryOperator, CustomFieldQueryOperator,
} from 'src/app/data/custom-field-query' } from 'src/app/data/custom-field-query'
import { CustomFieldQueryAtom } from 'src/app/utils/custom-field-query-element'
const tags: Tag[] = [ const tags: Tag[] = [
{ {
@ -886,31 +886,39 @@ describe('FilterEditorComponent', () => {
).toEqual(['42', CustomFieldQueryOperator.Exists, 'true']) ).toEqual(['42', CustomFieldQueryOperator.Exists, 'true'])
})) }))
it('should ingest filter rules for exclude tag(s)', fakeAsync(() => { it('should ingest filter rules for custom field lookup', fakeAsync(() => {
// expect(component.customFieldQueriesModel.getExcludedItems()).toHaveLength(0) expect(component.customFieldQueriesModel.isEmpty()).toBeTruthy()
// component.filterRules = [ component.filterRules = [
// { {
// rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS, rule_type: FILTER_CUSTOM_FIELDS_LOOKUP,
// value: '42', value: '["AND", [[42, "exists", "true"],[43, "exists", "true"]]]',
// }, },
// { ]
// rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS, expect(component.customFieldQueriesModel.queries[0].operator).toEqual(
// value: '43', CustomFieldQueryLogicalOperator.And
// }, )
// ] expect(component.customFieldQueriesModel.queries[0].value.length).toEqual(2)
// expect(component.customFieldQueriesModel.logicalOperator).toEqual( expect(
// LogicalOperator.And (
// ) component.customFieldQueriesModel.queries[0]
// expect(component.customFieldQueriesModel.getExcludedItems()).toEqual( .value[0] as CustomFieldQueryAtom
// custom_fields ).serialize()
// ) ).toEqual([42, CustomFieldQueryOperator.Exists, 'true'])
// // coverage
// component.filterRules = [ // atom
// { component.filterRules = [
// rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS, {
// value: null, 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(() => { it('should ingest filter rules for owner', fakeAsync(() => {

View File

@ -8,6 +8,7 @@ import {
CustomFieldQueryLogicalOperator, CustomFieldQueryLogicalOperator,
CustomFieldQueryOperator, CustomFieldQueryOperator,
} from '../data/custom-field-query' } from '../data/custom-field-query'
import { fakeAsync, tick } from '@angular/core/testing'
describe('CustomFieldQueryElement', () => { describe('CustomFieldQueryElement', () => {
it('should initialize with correct type and id', () => { it('should initialize with correct type and id', () => {
@ -86,23 +87,95 @@ describe('CustomFieldQueryAtom', () => {
expect(atom.value).toEqual([]) 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', () => { it('should serialize correctly', () => {
const atom = new CustomFieldQueryAtom([1, 'operator', 'value']) const atom = new CustomFieldQueryAtom([1, 'operator', 'value'])
expect(atom.serialize()).toEqual([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', () => { describe('CustomFieldQueryExpression', () => {
it('should initialize with correct operator and value', () => { it('should initialize with default operator and empty value', () => {
const expression = new CustomFieldQueryExpression([ const expression = new CustomFieldQueryExpression()
CustomFieldQueryLogicalOperator.And, expect(expression.operator).toBe(CustomFieldQueryLogicalOperator.Or)
[],
])
expect(expression.operator).toBe(CustomFieldQueryLogicalOperator.And)
expect(expression.value).toEqual([]) 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 expression = new CustomFieldQueryExpression()
const atom = new CustomFieldQueryAtom([ const atom = new CustomFieldQueryAtom([
1, 1,
@ -111,9 +184,14 @@ describe('CustomFieldQueryExpression', () => {
]) ])
expression.addAtom(atom) expression.addAtom(atom)
expect(expression.value).toContain(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 expression = new CustomFieldQueryExpression()
const subExpression = new CustomFieldQueryExpression([ const subExpression = new CustomFieldQueryExpression([
CustomFieldQueryLogicalOperator.Or, CustomFieldQueryLogicalOperator.Or,
@ -121,19 +199,42 @@ describe('CustomFieldQueryExpression', () => {
]) ])
expression.addExpression(subExpression) expression.addExpression(subExpression)
expect(expression.value).toContain(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', () => { it('should serialize correctly', () => {
const expression = new CustomFieldQueryExpression([ const expression = new CustomFieldQueryExpression([
CustomFieldQueryLogicalOperator.And, CustomFieldQueryLogicalOperator.And,
[[1, 'operator', 'value']], [[1, 'exists', 'true']],
]) ])
expect(expression.serialize()).toEqual([ expect(expression.serialize()).toEqual([
CustomFieldQueryLogicalOperator.And, 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', () => { it('should be negatable if it has one child which is an expression', () => {
const expression = new CustomFieldQueryExpression([ const expression = new CustomFieldQueryExpression([
CustomFieldQueryLogicalOperator.Not, CustomFieldQueryLogicalOperator.Not,

View File

@ -73,31 +73,20 @@ export class CustomFieldQueryAtom extends CustomFieldQueryElement {
this.value = null this.value = null
} else { } else {
if (!newTypes.includes(typeof this.value)) { if (!newTypes.includes(typeof this.value)) {
if (newTypes.length === 1) { switch (newTypes[0]) {
switch (newTypes[0]) { case 'string':
case 'string': this.value = ''
this.value = '' break
break case 'boolean':
case 'boolean': this.value = 'true'
this.value = 'true' break
break case 'array':
case 'array': this.value = []
this.value = [] break
break case 'number':
default: const num = parseFloat(this.value as string)
this.value = null this.value = isNaN(num) ? null : num.toString()
break break
}
} else {
if (newTypes.includes('number')) {
try {
this.value = parseFloat(this.value as string).toString()
} catch (e) {
this.value = null
}
} else {
this.value = null
}
} }
} else if ( } else if (
['true', 'false'].includes(this.value as string) && ['true', 'false'].includes(this.value as string) &&
@ -133,6 +122,8 @@ export class CustomFieldQueryAtom extends CustomFieldQueryElement {
} }
export class CustomFieldQueryExpression extends CustomFieldQueryElement { export class CustomFieldQueryExpression extends CustomFieldQueryElement {
protected _value: string[] | number[] | CustomFieldQueryElement[]
constructor( constructor(
expressionArray: [CustomFieldQueryLogicalOperator, any[]] = [ expressionArray: [CustomFieldQueryLogicalOperator, any[]] = [
CustomFieldQueryLogicalOperator.Or, CustomFieldQueryLogicalOperator.Or,
@ -174,17 +165,13 @@ export class CustomFieldQueryExpression extends CustomFieldQueryElement {
public override serialize() { public override serialize() {
let value let value
if (this._value instanceof Array) { value = this._value.map((element) => element.serialize())
value = this._value.map((atom) => atom.serialize()) // If the expression is negated it should have only one child which is an expression
// If the expression is negated it should have only one child which is an expression if (
if ( this._operator === CustomFieldQueryLogicalOperator.Not &&
this._operator === CustomFieldQueryLogicalOperator.Not && value.length === 1
value.length === 1 ) {
) { value = value[0]
value = value[0]
}
} else {
value = value.serialize()
} }
return [this._operator, value] return [this._operator, value]
} }