From 190e92a448bf49390b6a7bc3d84d64e72dd0366e Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Mon, 17 Mar 2025 22:54:08 -0700
Subject: [PATCH] should be full coverage

---
 .../filterable-dropdown.component.spec.ts     | 38 +++++++++
 .../filter-editor.component.spec.ts           | 79 +++++++++++++++++++
 src-ui/src/app/utils/query-params.spec.ts     | 11 +++
 3 files changed, 128 insertions(+)

diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts
index d13b21f81..cd279b1b5 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts
+++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts
@@ -7,6 +7,7 @@ import {
   tick,
 } from '@angular/core/testing'
 import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
+import { NEGATIVE_NULL_FILTER_VALUE } from 'src/app/data/filter-rule-type'
 import {
   DEFAULT_MATCHING_ALGORITHM,
   MATCH_ALL,
@@ -44,6 +45,11 @@ const nullItem = {
   name: 'Not assigned',
 }
 
+const negativeNullItem = {
+  id: NEGATIVE_NULL_FILTER_VALUE,
+  name: 'Not assigned',
+}
+
 let selectionModel: FilterableDropdownSelectionModel
 
 describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () => {
@@ -482,6 +488,24 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
     expect(changedResult.getExcludedItems()).toEqual(items)
   }))
 
+  it('should update null item selection on toggleIntersection', () => {
+    component.selectionModel.items = items
+    component.selectionModel = selectionModel
+    component.selectionModel.intersection = Intersection.Include
+    console.log(component.selectionModel.items[0])
+    component.selectionModel.set(null, ToggleableItemState.Selected)
+    component.selectionModel.intersection = Intersection.Exclude
+    component.selectionModel.toggleIntersection()
+    console.log(component.selectionModel)
+    expect(component.selectionModel.getExcludedItems()).toEqual([
+      negativeNullItem,
+    ])
+
+    component.selectionModel.intersection = Intersection.Include
+    component.selectionModel.toggleIntersection()
+    expect(component.selectionModel.getSelectedItems()).toEqual([nullItem])
+  })
+
   it('selection model should sort items by state', () => {
     component.selectionModel = selectionModel
     component.selectionModel.items = items.concat([{ id: 3, name: 'Item3' }])
@@ -494,6 +518,20 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
       { id: 3, name: 'Item3' },
       items[0],
     ])
+
+    selectionModel.intersection = Intersection.Exclude
+    selectionModel.toggleIntersection()
+    selectionModel.apply()
+    expect(selectionModel.items).toEqual([
+      negativeNullItem,
+      items[1],
+      { id: 3, name: 'Item3' },
+      items[0],
+    ])
+
+    // coverage
+    selectionModel.items = selectionModel.items.reverse()
+    selectionModel.apply()
   })
 
   it('selection model should sort items by state and document counts = 0, if set', () => {
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 1364a8022..4e8a797cc 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
@@ -69,6 +69,7 @@ import {
   FILTER_STORAGE_PATH,
   FILTER_TITLE,
   FILTER_TITLE_CONTENT,
+  NEGATIVE_NULL_FILTER_VALUE,
 } from 'src/app/data/filter-rule-type'
 import { StoragePath } from 'src/app/data/storage-path'
 import { Tag } from 'src/app/data/tag'
@@ -678,6 +679,19 @@ describe('FilterEditorComponent', () => {
       correspondents[0],
     ])
     component.toggleCorrespondent(12) // coverage
+
+    component.filterRules = [
+      {
+        rule_type: FILTER_CORRESPONDENT,
+        value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+      },
+    ]
+    expect(component.correspondentSelectionModel.intersection).toEqual(
+      Intersection.Exclude
+    )
+    expect(component.correspondentSelectionModel.getExcludedItems()).toEqual([
+      { id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
+    ])
   }))
 
   it('should ingest filter rules for has any of correspondents', fakeAsync(() => {
@@ -758,6 +772,19 @@ describe('FilterEditorComponent', () => {
       document_types[0],
     ])
     component.toggleDocumentType(22) // coverage
+
+    component.filterRules = [
+      {
+        rule_type: FILTER_DOCUMENT_TYPE,
+        value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+      },
+    ]
+    expect(component.documentTypeSelectionModel.intersection).toEqual(
+      Intersection.Exclude
+    )
+    expect(component.documentTypeSelectionModel.getExcludedItems()).toEqual([
+      { id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
+    ])
   }))
 
   it('should ingest filter rules for has any of document types', fakeAsync(() => {
@@ -835,6 +862,19 @@ describe('FilterEditorComponent', () => {
       storage_paths[0],
     ])
     component.toggleStoragePath(32) // coverage
+
+    component.filterRules = [
+      {
+        rule_type: FILTER_STORAGE_PATH,
+        value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+      },
+    ]
+    expect(component.storagePathSelectionModel.intersection).toEqual(
+      Intersection.Exclude
+    )
+    expect(component.storagePathSelectionModel.getExcludedItems()).toEqual([
+      { id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
+    ])
   }))
 
   it('should ingest filter rules for has any of storage paths', fakeAsync(() => {
@@ -1386,6 +1426,19 @@ describe('FilterEditorComponent', () => {
         value: null,
       },
     ])
+
+    const excludeButton = correspondentsFilterableDropdown.queryAll(
+      By.css('input[value=exclude]')
+    )[0]
+    excludeButton.nativeElement.checked = true
+    excludeButton.triggerEventHandler('change')
+    fixture.detectChanges()
+    expect(component.filterRules).toEqual([
+      {
+        rule_type: FILTER_CORRESPONDENT,
+        value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+      },
+    ])
   }))
 
   it('should convert user input to correct filter rules on document type selections', fakeAsync(() => {
@@ -1443,6 +1496,19 @@ describe('FilterEditorComponent', () => {
         value: null,
       },
     ])
+
+    const excludeButton = docTypesFilterableDropdown.queryAll(
+      By.css('input[value=exclude]')
+    )[0]
+    excludeButton.nativeElement.checked = true
+    excludeButton.triggerEventHandler('change')
+    fixture.detectChanges()
+    expect(component.filterRules).toEqual([
+      {
+        rule_type: FILTER_DOCUMENT_TYPE,
+        value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+      },
+    ])
   }))
 
   it('should convert user input to correct filter rules on storage path selections', fakeAsync(() => {
@@ -1500,6 +1566,19 @@ describe('FilterEditorComponent', () => {
         value: null,
       },
     ])
+
+    const excludeButton = storagePathsFilterableDropdown.queryAll(
+      By.css('input[value=exclude]')
+    )[0]
+    excludeButton.nativeElement.checked = true
+    excludeButton.triggerEventHandler('change')
+    fixture.detectChanges()
+    expect(component.filterRules).toEqual([
+      {
+        rule_type: FILTER_STORAGE_PATH,
+        value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+      },
+    ])
   }))
 
   it('should convert user input to correct filter rules on custom field selections', fakeAsync(() => {
diff --git a/src-ui/src/app/utils/query-params.spec.ts b/src-ui/src/app/utils/query-params.spec.ts
index cc91f3f6c..c22c90d11 100644
--- a/src-ui/src/app/utils/query-params.spec.ts
+++ b/src-ui/src/app/utils/query-params.spec.ts
@@ -8,6 +8,7 @@ import {
   FILTER_HAS_CUSTOM_FIELDS_ALL,
   FILTER_HAS_CUSTOM_FIELDS_ANY,
   FILTER_HAS_TAGS_ALL,
+  NEGATIVE_NULL_FILTER_VALUE,
 } from '../data/filter-rule-type'
 import {
   filterRulesFromQueryParams,
@@ -97,6 +98,16 @@ describe('QueryParams Utils', () => {
       correspondent__isnull: 1,
     })
 
+    params = queryParamsFromFilterRules([
+      {
+        rule_type: FILTER_CORRESPONDENT,
+        value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+      },
+    ])
+    expect(params).toEqual({
+      correspondent__isnull: 0,
+    })
+
     params = queryParamsFromFilterRules([
       {
         rule_type: FILTER_HAS_ANY_TAG,