Dedupe search component tests.

Bug: 380878140
Test: npm run test:unit:ci
Change-Id: I19eb35b309b02c152cc045eb200a8546455249b6
diff --git a/tools/winscope/src/test/unit/dom_test_utils.ts b/tools/winscope/src/test/unit/dom_test_utils.ts
index ef967c8..94275de 100644
--- a/tools/winscope/src/test/unit/dom_test_utils.ts
+++ b/tools/winscope/src/test/unit/dom_test_utils.ts
@@ -130,7 +130,12 @@
   }
 
   dispatchInput(value: string) {
-    if (!(this.root instanceof HTMLInputElement)) {
+    if (
+      !(
+        this.root instanceof HTMLInputElement ||
+        this.root instanceof HTMLTextAreaElement
+      )
+    ) {
       throw new Error('cannot dispatch input on node ' + this.root.nodeName);
     }
     this.root.value = value;
@@ -168,8 +173,11 @@
     this.root.addEventListener(event, listener);
   }
 
-  keydownEnter() {
-    const event = new KeyboardEvent('keydown', {key: KeyboardEventKey.ENTER});
+  keydownEnter(shiftKey = false) {
+    const event = new KeyboardEvent('keydown', {
+      key: KeyboardEventKey.ENTER,
+      shiftKey,
+    });
     this.dispatchEvent(event);
   }
 
diff --git a/tools/winscope/src/test/unit/utils.ts b/tools/winscope/src/test/unit/utils.ts
index ba4c1eb..255bed2 100644
--- a/tools/winscope/src/test/unit/utils.ts
+++ b/tools/winscope/src/test/unit/utils.ts
@@ -299,49 +299,6 @@
     return parser.getEntry(index);
   }
 
-  static checkSectionCollapseAndExpand<T>(
-    htmlElement: HTMLElement,
-    fixture: ComponentFixture<T>,
-    selector: string,
-    sectionTitle: string,
-  ) {
-    const section = assertDefined(htmlElement.querySelector(selector));
-    expect(
-      assertDefined(
-        section.querySelector<HTMLElement>(
-          'collapsible-section-title .mat-title',
-        ),
-      ).textContent,
-    ).toEqual(sectionTitle);
-    const collapseButton = assertDefined(
-      section.querySelector<HTMLElement>('collapsible-section-title button'),
-    );
-    collapseButton.click();
-    fixture.detectChanges();
-    expect(section.classList).toContain('collapsed');
-    const collapsedSections = assertDefined(
-      htmlElement.querySelector('collapsed-sections'),
-    );
-    const collapsedSection = assertDefined(
-      collapsedSections.querySelector('.collapsed-section'),
-    ) as HTMLElement;
-    expect(collapsedSection.textContent?.trim()).toEqual(
-      sectionTitle + '  arrow_right',
-    );
-    collapsedSection.click();
-    fixture.detectChanges();
-    UnitTestUtils.checkNoCollapsedSectionButtons(htmlElement);
-  }
-
-  static checkNoCollapsedSectionButtons(htmlElement: HTMLElement) {
-    const collapsedSections = assertDefined(
-      htmlElement.querySelector('collapsed-sections'),
-    );
-    expect(
-      collapsedSections.querySelectorAll('.collapsed-section').length,
-    ).toEqual(0);
-  }
-
   static makeEmptyTrace<T extends TraceType>(
     traceType: T,
     descriptors: string[] = [],
diff --git a/tools/winscope/src/viewers/components/hierarchy_tree_node_data_view_component_test.ts b/tools/winscope/src/viewers/components/hierarchy_tree_node_data_view_component_test.ts
index d2b8517..cdff03b 100644
--- a/tools/winscope/src/viewers/components/hierarchy_tree_node_data_view_component_test.ts
+++ b/tools/winscope/src/viewers/components/hierarchy_tree_node_data_view_component_test.ts
@@ -54,19 +54,19 @@
     dom.checkTextExact('test node');
   });
 
-  it('shows display name if set, with full name on hover', () => {
+  it('shows display name if set, with full name on hover', async () => {
     testNode.setDisplayName('display name');
     component.node = testNode;
     dom.detectChanges();
     dom.checkTextExact('1 - display name');
-    dom.get('.display-name').checkTooltip('test node');
+    await dom.get('.display-name').checkTooltip('test node');
   });
 
-  it('shows chips with tooltip on hover', () => {
+  it('shows chips with tooltip on hover', async () => {
     testNode.addChip(VISIBLE_CHIP);
     component.node = testNode;
     dom.detectChanges();
     dom.checkTextExact(`1 - test node ${VISIBLE_CHIP.short}`);
-    dom.get('.tree-view-chip').checkTooltip(VISIBLE_CHIP.long);
+    await dom.get('.tree-view-chip').checkTooltip(VISIBLE_CHIP.long);
   });
 });
diff --git a/tools/winscope/src/viewers/viewer_search/active_search_component_test.ts b/tools/winscope/src/viewers/viewer_search/active_search_component_test.ts
index 1f21e27..c740726 100644
--- a/tools/winscope/src/viewers/viewer_search/active_search_component_test.ts
+++ b/tools/winscope/src/viewers/viewer_search/active_search_component_test.ts
@@ -16,7 +16,7 @@
 
 import {CommonModule, NgTemplateOutlet} from '@angular/common';
 import {Component} from '@angular/core';
-import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {TestBed} from '@angular/core/testing';
 import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
 import {MatButtonModule} from '@angular/material/button';
 import {MatFormFieldModule} from '@angular/material/form-field';
@@ -26,6 +26,7 @@
 import {MatTooltipModule} from '@angular/material/tooltip';
 import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
 import {assertDefined} from 'common/assert_utils';
+import {DOMTestHelper} from 'test/unit/dom_test_utils';
 import {
   SearchQueryClickDetail,
   ViewerEvents,
@@ -34,9 +35,8 @@
 
 describe('ActiveSearchComponent', () => {
   const testQuery = 'select * from table';
-  let fixture: ComponentFixture<ActiveSearchComponent>;
   let component: ActiveSearchComponent;
-  let htmlElement: HTMLElement;
+  let dom: DOMTestHelper<ActiveSearchComponent>;
 
   beforeEach(async () => {
     await TestBed.configureTestingModule({
@@ -55,14 +55,13 @@
         NgTemplateOutlet,
       ],
     }).compileComponents();
-    fixture = TestBed.createComponent(ActiveSearchComponent);
+    const fixture = TestBed.createComponent(ActiveSearchComponent);
     component = fixture.componentInstance;
-    htmlElement = fixture.nativeElement;
-
+    dom = new DOMTestHelper(fixture, fixture.nativeElement);
     component.isSearchInitialized = true;
     component.lastTraceFailed = false;
     component.saveQueryNameControl = new FormControl();
-    fixture.detectChanges();
+    dom.detectChanges();
   });
 
   it('can be created', () => {
@@ -76,23 +75,21 @@
   it('handles search via enter key', () => {
     const runSearch = () => {
       const textInput = getTextInput();
-      changeInput(textInput, testQuery);
-      pressEnter(textInput);
+      textInput.dispatchInput(testQuery);
+      textInput.keydownEnter();
     };
     runSearchAndCheckHandled(runSearch);
   });
 
   it('does not handle search on enter key + shift key', () => {
     let query: string | undefined;
-    htmlElement
-      .querySelector('viewer-search')
-      ?.addEventListener(ViewerEvents.SearchQueryClick, (event) => {
-        const detail: SearchQueryClickDetail = (event as CustomEvent).detail;
-        query = detail.query;
-      });
+    dom.addEventListener(ViewerEvents.SearchQueryClick, (event) => {
+      const detail: SearchQueryClickDetail = (event as CustomEvent).detail;
+      query = detail.query;
+    });
     const textInput = getTextInput();
-    changeInput(textInput, testQuery);
-    pressEnter(textInput, true);
+    textInput.dispatchInput(testQuery);
+    textInput.keydownEnter(true);
     expect(query).toBeUndefined();
   });
 
@@ -100,83 +97,68 @@
     runSearchByQueryButton();
     component.canAdd = true;
     component.executedQuery = testQuery;
-    fixture.detectChanges();
-    expect(htmlElement.querySelector('.running-query-message')).toBeNull();
-    expect(
-      htmlElement.querySelector<HTMLButtonElement>('.add-button')?.disabled,
-    ).toBeFalse();
+    dom.detectChanges();
+    expect(dom.find('.running-query-message')).toBeUndefined();
+    dom.get('.add-button').checkDisabled(false);
   });
 
   it('handles running query failure', () => {
     runSearchByQueryButton();
     component.canAdd = true;
     component.lastTraceFailed = true;
-    fixture.detectChanges();
-    expect(htmlElement.querySelector('.running-query-message')).toBeNull();
-    expect(
-      htmlElement.querySelector<HTMLButtonElement>('.add-button')?.disabled,
-    ).toBeTrue();
+    dom.detectChanges();
+    expect(dom.find('.running-query-message')).toBeUndefined();
+    dom.get('.add-button').checkDisabled(true);
   });
 
   it('disables search query until initialized', () => {
     component.isSearchInitialized = false;
-    fixture.detectChanges();
-    changeInput(getTextInput(), testQuery);
-    expect(getSearchQueryButton().disabled).toBeTrue();
+    dom.detectChanges();
+    getTextInput().dispatchInput(testQuery);
+    getSearchQueryButton().checkDisabled(true);
 
     component.isSearchInitialized = true;
-    fixture.detectChanges();
-    expect(getSearchQueryButton().disabled).toBeFalse();
+    dom.detectChanges();
+    getSearchQueryButton().checkDisabled(false);
   });
 
   it('clears query', () => {
-    expect(htmlElement.querySelector('.clear-button')).toBeNull();
+    expect(dom.find('.clear-button')).toBeUndefined();
     component.canClear = true;
-    fixture.detectChanges();
-    const clearButton = assertDefined(
-      htmlElement.querySelector<HTMLElement>('.clear-button'),
-    );
+    dom.detectChanges();
+    const clearButton = dom.get('.clear-button');
     spyOn(component.clearQueryClick, 'emit');
-    expect(clearButton.textContent?.trim()).toContain('Clear');
+    clearButton.checkText('Clear');
     clearButton.click();
-    fixture.detectChanges();
     expect(component.clearQueryClick.emit).toHaveBeenCalledTimes(1);
   });
 
   it('adds query', () => {
-    expect(htmlElement.querySelector('.add-button')).toBeNull();
+    expect(dom.find('.add-button')).toBeUndefined();
     component.canAdd = true;
-    fixture.detectChanges();
-    const addButton = assertDefined(
-      htmlElement.querySelector<HTMLButtonElement>('.add-button'),
-    );
-    expect(addButton.textContent?.trim()).toContain('+ Add Query');
-    expect(addButton.disabled).toBeTrue();
+    dom.detectChanges();
+    const addButton = dom.get('.add-button');
+    addButton.checkText('+ Add Query');
+    addButton.checkDisabled(true);
 
     spyOn(component.addQueryClick, 'emit');
     component.executedQuery = testQuery;
-    fixture.detectChanges();
+    dom.detectChanges();
     addButton.click();
-    fixture.detectChanges();
     expect(component.addQueryClick.emit).toHaveBeenCalledTimes(1);
   });
 
   it('labels section', () => {
     component.label = 'test label';
-    fixture.detectChanges();
-    expect(htmlElement.querySelector('.header')?.textContent?.trim()).toEqual(
-      'test label',
-    );
+    dom.detectChanges();
+    dom.get('.header').checkText('test label');
   });
 
   it('shows last query execution time', () => {
-    expect(htmlElement.querySelector('.query-execution-time')).toBeNull();
-
+    expect(dom.find('.query-execution-time')).toBeUndefined();
     component.lastQueryExecutionTime = '10 ms';
-    fixture.detectChanges();
-    expect(
-      htmlElement.querySelector('.query-execution-time')?.textContent?.trim(),
-    ).toEqual('Executed in 10 ms');
+    dom.detectChanges();
+    dom.get('.query-execution-time').checkText('Executed in 10 ms');
   });
 
   it('shows current search information and save query field', () => {
@@ -206,57 +188,29 @@
     ).toEqual('test name');
   });
 
-  function getTextInput(): HTMLTextAreaElement {
-    return assertDefined(
-      htmlElement.querySelector<HTMLTextAreaElement>('.query-field textarea'),
-    );
+  function getTextInput(): DOMTestHelper<ActiveSearchComponent> {
+    return dom.get('.query-field textarea');
   }
 
-  function changeInput(
-    input: HTMLInputElement | HTMLTextAreaElement,
-    query: string,
-  ) {
-    input.value = query;
-    input.dispatchEvent(new Event('input'));
-    fixture.detectChanges();
-  }
-
-  function getSearchQueryButton(): HTMLButtonElement {
-    return assertDefined(
-      htmlElement.querySelector<HTMLButtonElement>(
-        '.query-actions .search-button',
-      ),
-    );
+  function getSearchQueryButton(): DOMTestHelper<ActiveSearchComponent> {
+    return dom.get('.query-actions .search-button');
   }
 
   function runSearchByQueryButton() {
-    changeInput(getTextInput(), testQuery);
+    getTextInput().dispatchInput(testQuery);
     getSearchQueryButton().click();
-    fixture.detectChanges();
   }
 
   function runSearchAndCheckHandled(runSearch: () => void) {
     spyOn(component.searchQueryClick, 'emit');
     runSearch();
     component.runningQuery = true;
-    fixture.detectChanges();
+    dom.detectChanges();
     expect(component.searchQueryClick.emit).toHaveBeenCalledOnceWith(testQuery);
-    expect(getSearchQueryButton().disabled).toBeTrue();
-    const runningQueryMessage = assertDefined(
-      htmlElement.querySelector('.running-query-message'),
-    );
-    expect(runningQueryMessage.textContent?.trim()).toEqual(
-      'timer Calculating results',
-    );
-    expect(runningQueryMessage.querySelector('mat-spinner')).toBeTruthy();
-  }
-
-  function pressEnter(
-    input: HTMLInputElement | HTMLTextAreaElement,
-    shiftKey = false,
-  ) {
-    input.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', shiftKey}));
-    fixture.detectChanges();
+    getSearchQueryButton().checkDisabled(true);
+    const runningQueryMessage = dom.get('.running-query-message');
+    runningQueryMessage.checkTextExact('timer Calculating results');
+    expect(runningQueryMessage.find('mat-spinner')).toBeDefined();
   }
 
   @Component({
diff --git a/tools/winscope/src/viewers/viewer_search/search_list_component_test.ts b/tools/winscope/src/viewers/viewer_search/search_list_component_test.ts
index 513a3b0..3b9b97e 100644
--- a/tools/winscope/src/viewers/viewer_search/search_list_component_test.ts
+++ b/tools/winscope/src/viewers/viewer_search/search_list_component_test.ts
@@ -17,20 +17,18 @@
 import {CdkMenuModule} from '@angular/cdk/menu';
 import {NgTemplateOutlet} from '@angular/common';
 import {Component, ViewChild} from '@angular/core';
-import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {TestBed} from '@angular/core/testing';
 import {MatButtonModule} from '@angular/material/button';
 import {MatIconModule} from '@angular/material/icon';
 import {MatTooltipModule} from '@angular/material/tooltip';
 import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
-import {assertDefined} from 'common/assert_utils';
-import {UnitTestUtils} from 'test/unit/utils';
+import {DOMTestHelper} from 'test/unit/dom_test_utils';
 import {ListItemOption, SearchListComponent} from './search_list_component';
 import {ListedSearch} from './ui_data';
 
 describe('SearchListComponent', () => {
-  let fixture: ComponentFixture<TestHostComponent>;
   let component: TestHostComponent;
-  let htmlElement: HTMLElement;
+  let dom: DOMTestHelper<TestHostComponent>;
 
   beforeEach(async () => {
     await TestBed.configureTestingModule({
@@ -43,10 +41,10 @@
         MatButtonModule,
       ],
     }).compileComponents();
-    fixture = TestBed.createComponent(TestHostComponent);
+    const fixture = TestBed.createComponent(TestHostComponent);
     component = fixture.componentInstance;
-    htmlElement = fixture.nativeElement;
-    fixture.detectChanges();
+    dom = new DOMTestHelper(fixture, fixture.nativeElement);
+    dom.detectChanges();
   });
 
   it('can be created', () => {
@@ -54,11 +52,11 @@
   });
 
   it('shows placeholder text if no searches', () => {
-    expect(htmlElement.textContent?.trim()).toEqual('');
+    dom.checkTextExact('');
     const placeholderText = 'placeholder text';
     component.placeholderText = placeholderText;
-    fixture.detectChanges();
-    expect(htmlElement.textContent?.trim()).toEqual(placeholderText);
+    dom.detectChanges();
+    dom.checkTextExact(placeholderText);
   });
 
   it('shows search names with tooltips', async () => {
@@ -66,85 +64,73 @@
       new ListedSearch('query1', 'name1'),
       new ListedSearch('query2', 'query2'),
     ];
-    fixture.detectChanges();
+    dom.detectChanges();
 
-    const listedSearches =
-      htmlElement.querySelectorAll<HTMLElement>('.listed-search');
+    const listedSearches = dom.findAll('.listed-search');
     expect(listedSearches.length).toEqual(2);
 
-    const queryName1 = assertDefined(
-      listedSearches[0].querySelector<HTMLElement>('.listed-search-name'),
-    );
-    const queryName2 = assertDefined(
-      listedSearches[1].querySelector<HTMLElement>('.listed-search-name'),
-    );
-    expect(queryName1.textContent?.trim()).toEqual('name1');
-    expect(queryName2.textContent?.trim()).toEqual('query2');
+    const queryName1 = listedSearches[0].get('.listed-search-name');
+    const queryName2 = listedSearches[1].get('.listed-search-name');
+    queryName1.checkTextExact('name1');
+    queryName2.checkTextExact('query2');
 
     // shows tooltip when name and query are different
-    await UnitTestUtils.checkTooltips([queryName1], ['name1: query1'], fixture);
+    await queryName1.checkTooltip('name1: query1');
 
     // does not show tooltip when name and query are the same
-    await UnitTestUtils.checkTooltips([queryName2], [undefined], fixture);
+    await queryName2.checkTooltip(undefined);
 
     // shows tooltip when element is overflowing
-    queryName2.style.maxWidth = queryName2.offsetWidth / 2 + 'px';
-    fixture.detectChanges();
-    await UnitTestUtils.checkTooltips([queryName2], ['query2'], fixture);
+    const query2El = queryName2.getHTMLElement();
+    query2El.style.maxWidth = query2El.offsetWidth / 2 + 'px';
+    dom.detectChanges();
+    await queryName2.checkTooltip('query2');
   });
 
   it('formats search dates', () => {
     spyOn(Date, 'now').and.returnValue(1000);
     component.searches = [new ListedSearch('query1', 'name1')];
-    fixture.detectChanges();
+    dom.detectChanges();
     const expectedDate = new Date(1000);
-    expect(
-      htmlElement
-        .querySelector('.listed-search-date-options')
-        ?.textContent?.trim(),
-    ).toEqual(
-      `${expectedDate
-        .toTimeString()
-        .slice(0, 5)}\n${expectedDate.toLocaleDateString()}`,
-    );
+    dom
+      .get('.listed-search-date-options')
+      .checkTextExact(
+        `${expectedDate
+          .toTimeString()
+          .slice(0, 5)}\n${expectedDate.toLocaleDateString()}`,
+      );
   });
 
-  it('shows options and triggers callback on interaction', () => {
+  it('shows options and triggers callback on interaction', async () => {
     let optionClicked: ListedSearch | undefined;
     component.searches = [new ListedSearch('query1', 'name1')];
-    fixture.detectChanges();
+    dom.detectChanges();
     // does not show menu button if no options
-    expect(htmlElement.querySelector('.listed-search-options')).toBeNull();
+    expect(dom.find('.listed-search-options')).toBeUndefined();
 
     const onClickCallback = (search: ListedSearch) => (optionClicked = search);
     component.listItemOptions = [
       {name: 'option1', icon: 'test', onClickCallback},
     ];
-    fixture.detectChanges();
+    dom.detectChanges();
 
-    const option = assertDefined(
-      htmlElement.querySelector<HTMLElement>('.listed-search-option'),
-    );
-    UnitTestUtils.checkTooltips([option], ['option1'], fixture);
+    const option = dom.get('.listed-search-option');
+    await option.checkTooltip('option1');
     option.click();
     expect(optionClicked).toEqual(component.searches[0]);
   });
 
-  it('shows menu', () => {
+  it('shows menu', async () => {
     component.listItemOptions = [
       {name: 'option1', icon: 'test', menu: component.testTemplate},
     ];
     component.searches = [new ListedSearch('query1', 'name1')];
-    fixture.detectChanges();
-    const option = assertDefined(
-      htmlElement.querySelector<HTMLElement>('.listed-search-option'),
-    );
-    UnitTestUtils.checkTooltips([option], ['option1'], fixture);
+    dom.detectChanges();
+    const option = dom.get('.listed-search-option');
+    await option.checkTooltip('option1');
     option.click();
-    const menu = assertDefined(
-      document.querySelector<HTMLElement>('.context-menu'),
-    );
-    expect(menu.querySelector('.test-menu-item')).toBeTruthy();
+    const menu = dom.getInDocument('.context-menu');
+    expect(menu.find('.test-menu-item')).toBeDefined();
   });
 
   @Component({
diff --git a/tools/winscope/src/viewers/viewer_search/viewer_search_component_test.ts b/tools/winscope/src/viewers/viewer_search/viewer_search_component_test.ts
index a9517c5..d51e851 100644
--- a/tools/winscope/src/viewers/viewer_search/viewer_search_component_test.ts
+++ b/tools/winscope/src/viewers/viewer_search/viewer_search_component_test.ts
@@ -18,7 +18,7 @@
 import {CdkMenuModule} from '@angular/cdk/menu';
 import {ScrollingModule} from '@angular/cdk/scrolling';
 import {Component, ViewChild} from '@angular/core';
-import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {TestBed} from '@angular/core/testing';
 import {FormsModule, ReactiveFormsModule} from '@angular/forms';
 import {MatButtonModule} from '@angular/material/button';
 import {MatDividerModule} from '@angular/material/divider';
@@ -30,7 +30,7 @@
 import {MatTooltipModule} from '@angular/material/tooltip';
 import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
 import {assertDefined} from 'common/assert_utils';
-import {UnitTestUtils} from 'test/unit/utils';
+import {DOMTestHelper} from 'test/unit/dom_test_utils';
 import {
   AddQueryClickDetail,
   ClearQueryClickDetail,
@@ -49,9 +49,11 @@
 
 describe('ViewerSearchComponent', () => {
   const testQuery = 'select * from table';
-  let fixture: ComponentFixture<TestHostComponent>;
+  const accordionItemSelector = '.accordion-item-header';
+  const searchQuerySelector = '.query-actions .search-button';
+  const listedSearchSelector = '.listed-search-option';
   let component: TestHostComponent;
-  let htmlElement: HTMLElement;
+  let dom: DOMTestHelper<TestHostComponent>;
 
   beforeEach(async () => {
     await TestBed.configureTestingModule({
@@ -81,12 +83,12 @@
         MatDividerModule,
       ],
     }).compileComponents();
-    fixture = TestBed.createComponent(TestHostComponent);
+    const fixture = TestBed.createComponent(TestHostComponent);
     component = fixture.componentInstance;
-    htmlElement = fixture.nativeElement;
+    dom = new DOMTestHelper(fixture, fixture.nativeElement);
     component.inputData.initialized = true;
     component.inputData.currentSearches = [new CurrentSearch(1)];
-    fixture.detectChanges();
+    dom.detectChanges();
   });
 
   it('can be created', () => {
@@ -94,46 +96,28 @@
   });
 
   it('creates global search section with tabs', () => {
-    const globalSearch = assertDefined(
-      htmlElement.querySelector('.global-search'),
-    );
-    const searchTabs =
-      globalSearch.querySelectorAll<HTMLElement>('.mat-tab-label');
-    const [searchTab, savedTab, recentTab] = Array.from(searchTabs);
-    expect(searchTab.textContent).toEqual('Search');
-    expect(savedTab.textContent).toEqual('Saved');
-    expect(recentTab.textContent).toEqual('Recent');
+    const globalSearch = dom.get('.global-search');
+    const [searchTab, savedTab, recentTab] =
+      globalSearch.findAll('.mat-tab-label');
+    searchTab.checkTextExact('Search');
+    savedTab.checkTextExact('Saved');
+    recentTab.checkTextExact('Recent');
   });
 
   it('creates collapsed sections with no buttons', () => {
-    UnitTestUtils.checkNoCollapsedSectionButtons(htmlElement);
+    dom.checkNoCollapsedSectionButtons();
   });
 
   it('handles search box section collapse/expand', () => {
-    UnitTestUtils.checkSectionCollapseAndExpand(
-      htmlElement,
-      fixture,
-      '.global-search',
-      'GLOBAL SEARCH',
-    );
+    dom.checkSectionCollapseAndExpand('.global-search', 'GLOBAL SEARCH');
   });
 
   it('handles tabulated results section collapse/expand', () => {
-    UnitTestUtils.checkSectionCollapseAndExpand(
-      htmlElement,
-      fixture,
-      '.search-results',
-      'SEARCH RESULTS',
-    );
+    dom.checkSectionCollapseAndExpand('.search-results', 'SEARCH RESULTS');
   });
 
   it('handles documentation groups section collapse/expand', () => {
-    UnitTestUtils.checkSectionCollapseAndExpand(
-      htmlElement,
-      fixture,
-      '.how-to-search',
-      'HOW TO SEARCH',
-    );
+    dom.checkSectionCollapseAndExpand('.how-to-search', 'HOW TO SEARCH');
   });
 
   it('handles search via search query click', () => {
@@ -142,16 +126,16 @@
 
   it('handles search via run query from saved without creating new active search', async () => {
     component.inputData.savedSearches = [new ListedSearch(testQuery, 'saved1')];
-    fixture.detectChanges();
+    dom.detectChanges();
     await changeTab(1);
-    runSearchAndCheckHandled(runSearchFromListedSearchOption);
+    runSearchAndCheckHandled(() => dom.findAndClick(listedSearchSelector));
   });
 
   it('handles search via run query from recents without creating new active search', async () => {
     component.inputData.recentSearches = [new ListedSearch(testQuery)];
-    fixture.detectChanges();
+    dom.detectChanges();
     await changeTab(2);
-    runSearchAndCheckHandled(runSearchFromListedSearchOption);
+    runSearchAndCheckHandled(() => dom.findAndClick(listedSearchSelector));
   });
 
   it('handles search via run query from saved creating new active search', async () => {
@@ -186,63 +170,57 @@
 
   it('handles running query complete', () => {
     const placeholderCss = '.results-placeholder.placeholder-text';
-    expect(htmlElement.querySelector(placeholderCss)).toBeTruthy();
+    expect(dom.find(placeholderCss)).toBeDefined();
 
-    clickSearchQueryButton();
+    dom.get(searchQuerySelector).click();
     runSearchByQueryButton();
-    expect(htmlElement.querySelector(placeholderCss)).toBeNull();
+    expect(dom.find(placeholderCss)).toBeUndefined();
 
     addCurrentSearchWithResult();
-    expect(htmlElement.querySelector('.query-execution-time')).toBeTruthy();
-    expect(htmlElement.querySelector('log-view')).toBeTruthy();
-    expect(htmlElement.querySelector(placeholderCss)).toBeNull();
+    expect(dom.find('.query-execution-time')).toBeDefined();
+    expect(dom.find('log-view')).toBeDefined();
+    expect(dom.find(placeholderCss)).toBeUndefined();
   });
 
   it('adds search sections', () => {
     const spy = jasmine.createSpy();
-    htmlElement
-      .querySelector('viewer-search')
-      ?.addEventListener(ViewerEvents.AddQueryClick, (event) => {
+    dom
+      .get('viewer-search')
+      .addEventListener(ViewerEvents.AddQueryClick, (event) => {
         const detail: AddQueryClickDetail = (event as CustomEvent).detail;
         expect(detail).toBeFalsy();
         spy();
       });
 
-    let addButton = assertDefined(
-      htmlElement.querySelector<HTMLButtonElement>('.add-button'),
-    );
-    expect(htmlElement.querySelector('.clear-button')).toBeNull();
-    expect(addButton.disabled).toBeTrue();
+    const addButton = dom.get('.add-button');
+    expect(dom.find('.clear-button')).toBeUndefined();
+    addButton.checkDisabled(true);
 
     const data = structuredClone(component.inputData);
     data.currentSearches[0].query = testQuery;
     updateInputDataAndDetectChanges(data);
 
     addButton.click();
-    fixture.detectChanges();
     expect(spy).toHaveBeenCalledTimes(1);
 
     const newData = structuredClone(component.inputData);
     newData.currentSearches.push(new CurrentSearch(2));
     updateInputDataAndDetectChanges(newData);
 
-    const activeSections = htmlElement.querySelectorAll('active-search');
+    const activeSections = dom.findAll('active-search');
     expect(activeSections.length).toEqual(2);
-    expect(activeSections.item(0).querySelector('.clear-button')).toBeTruthy();
-    expect(activeSections.item(1).querySelector('.clear-button')).toBeTruthy();
+    expect(activeSections[0].find('.clear-button')).toBeDefined();
+    expect(activeSections[1].find('.clear-button')).toBeDefined();
 
-    expect(activeSections.item(0).querySelector('.add-button')).toBeNull();
-    addButton = assertDefined(
-      activeSections.item(1).querySelector<HTMLButtonElement>('.add-button'),
-    );
-    expect(addButton.disabled).toBeTrue();
+    expect(activeSections[0].find('.add-button')).toBeUndefined();
+    activeSections[1].get('.add-button').checkDisabled(true);
   });
 
   it('handles multiple results', async () => {
     let uid: number | undefined;
-    htmlElement
-      .querySelector('viewer-search')
-      ?.addEventListener(ViewerEvents.ClearQueryClick, (event) => {
+    dom
+      .get('viewer-search')
+      .addEventListener(ViewerEvents.ClearQueryClick, (event) => {
         const detail: ClearQueryClickDetail = (event as CustomEvent).detail;
         uid = detail.uid;
       });
@@ -251,35 +229,27 @@
     data.currentSearches[0].result = new SearchResult([], []);
     updateInputDataAndDetectChanges(data);
     addCurrentSearchWithResult(testQuery, 2);
-    let resultTabs = htmlElement.querySelectorAll(
-      '.result-tabs .mat-tab-label',
-    );
-    let activeSections =
-      htmlElement.querySelectorAll<HTMLElement>('active-search');
+    let resultTabs = dom.findAll('.result-tabs .mat-tab-label');
+    let activeSections = dom.findAll('active-search');
     expect(activeSections.length).toEqual(2);
     expect(resultTabs.length).toEqual(2);
-    expect(resultTabs.item(0).textContent).toEqual('Query 1');
-    expect(resultTabs.item(1).textContent).toEqual('Query 2');
+    resultTabs[0].checkTextExact('Query 1');
+    resultTabs[1].checkTextExact('Query 2');
 
-    const clearButton = assertDefined(
-      htmlElement.querySelector<HTMLElement>('.clear-button'),
-    );
-    clearButton.click();
-    fixture.detectChanges();
+    dom.findAndClick('.clear-button');
     expect(uid).toEqual(1);
 
-    const finalActiveSection = activeSections.item(1);
-    const spy = spyOn(finalActiveSection, 'scrollIntoView');
+    const spy = spyOn(activeSections[1].getHTMLElement(), 'scrollIntoView');
 
     const newData = structuredClone(component.inputData);
     newData.currentSearches.shift();
     updateInputDataAndDetectChanges(newData);
-    await fixture.whenStable();
+    await dom.whenStable();
 
-    resultTabs = htmlElement.querySelectorAll('.result-tabs .mat-tab-label');
-    activeSections = htmlElement.querySelectorAll('active-search');
+    resultTabs = dom.findAll('.result-tabs .mat-tab-label');
+    activeSections = dom.findAll('active-search');
     expect(resultTabs.length).toEqual(1);
-    expect(resultTabs.item(0).textContent).toEqual('Query 2');
+    resultTabs[0].checkTextExact('Query 2');
     expect(activeSections.length).toEqual(1);
     expect(spy).toHaveBeenCalled();
   });
@@ -289,162 +259,124 @@
     const data = structuredClone(component.inputData);
     data.lastTraceFailed = true;
     updateInputDataAndDetectChanges(data);
-    expect(htmlElement.querySelector('.query-execution-time')).toBeTruthy();
-    expect(htmlElement.querySelector('.running-query-message')).toBeNull();
-    expect(htmlElement.querySelector('log-view')).toBeNull();
-    expect(getSearchQueryButton().disabled).toBeFalse();
+    expect(dom.find('.query-execution-time')).toBeDefined();
+    expect(dom.find('.running-query-message')).toBeUndefined();
+    expect(dom.find('log-view')).toBeUndefined();
+    dom.get(searchQuerySelector).checkDisabled(false);
   });
 
   it('emits event on save query click', () => {
     let detail: SaveQueryClickDetail | undefined;
-    htmlElement
-      .querySelector('viewer-search')
-      ?.addEventListener(ViewerEvents.SaveQueryClick, (event) => {
+    dom
+      .get('viewer-search')
+      .addEventListener(ViewerEvents.SaveQueryClick, (event) => {
         detail = (event as CustomEvent).detail;
       });
     const testName = 'Query 1';
     component.inputData.savedSearches.push(
       new ListedSearch(testQuery, testName),
     );
-    fixture.detectChanges();
+    dom.detectChanges();
     addCurrentSearchWithResult();
-    const saveField = assertDefined(
-      htmlElement.querySelector('.current-search .save-field'),
-    );
-    const saveQueryButton = assertDefined(
-      saveField.querySelector<HTMLElement>('.query-button'),
-    );
-    const input = assertDefined(
-      saveField.querySelector<HTMLInputElement>('input'),
-    );
-    changeInput(input, testName);
-    pressEnter(input);
+    const saveField = dom.get('.current-search .save-field');
+    const saveQueryButton = saveField.get('.query-button');
+    const input = saveField.get('input');
+    input.dispatchInput(testName);
     saveQueryButton.click();
-    fixture.detectChanges();
     expect(detail).toBeUndefined(); // name already exists
 
     const testName2 = 'Query 2';
-    changeInput(input, testName2);
-    pressEnter(input); // save by enter key
+    input.dispatchInput(testName2);
+    input.keydownEnter(); // save by enter key
     expect(detail).toEqual(new SaveQueryClickDetail(testQuery, testName2));
 
     const testName3 = 'Query 3';
-    changeInput(input, testName3);
-    saveQueryButton.click();
-    fixture.detectChanges(); // save by click
+    input.dispatchInput(testName3);
+    saveQueryButton.click(); // save by click
     expect(detail).toEqual(new SaveQueryClickDetail(testQuery, testName3));
   });
 
   it('emits event on delete saved query click', async () => {
     let detail: DeleteSavedQueryClickDetail | undefined;
-    htmlElement
-      .querySelector('viewer-search')
-      ?.addEventListener(ViewerEvents.DeleteSavedQueryClick, (event) => {
+    dom
+      .get('viewer-search')
+      .addEventListener(ViewerEvents.DeleteSavedQueryClick, (event) => {
         detail = (event as CustomEvent).detail;
       });
     const search = new ListedSearch(testQuery);
     component.inputData.savedSearches = [search];
-    fixture.detectChanges();
+    dom.detectChanges();
 
     await changeTab(1);
-    const listedSearchButton = assertDefined(
-      htmlElement.querySelectorAll<HTMLElement>('.listed-search-option'),
-    );
-    listedSearchButton.item(2).click();
+    dom.findAndClickByIndex(listedSearchSelector, 2);
     expect(detail).toEqual(new DeleteSavedQueryClickDetail(search));
   });
 
   it('handles trace search initialization', () => {
     component.inputData.initialized = false;
-    fixture.detectChanges();
+    dom.detectChanges();
     const spy = jasmine.createSpy();
-    htmlElement
-      .querySelector('viewer-search')
-      ?.addEventListener(ViewerEvents.GlobalSearchSectionClick, (event) =>
+    dom
+      .get('viewer-search')
+      .addEventListener(ViewerEvents.GlobalSearchSectionClick, (event) =>
         spy(),
       );
-    const globalSearch = assertDefined(
-      htmlElement.querySelector<HTMLElement>('.global-search'),
-    );
-    expect(globalSearch.querySelector('.message-with-spinner')).toBeNull();
+    const globalSearch = dom.get('.global-search');
+    expect(globalSearch.find('.message-with-spinner')).toBeUndefined();
 
     clickGlobalSearchAndCheckMessage(globalSearch);
     clickGlobalSearchAndCheckMessage(globalSearch);
     expect(spy).toHaveBeenCalledTimes(1);
 
-    changeInput(getTextInput(), testQuery);
-    expect(getSearchQueryButton().disabled).toBeTrue();
+    getTextInput().dispatchInput(testQuery);
+    dom.get(searchQuerySelector).checkDisabled(true);
 
     const data = structuredClone(component.inputData);
     data.initialized = true;
     updateInputDataAndDetectChanges(data);
-    expect(globalSearch.querySelector('.message-with-spinner')).toBeNull();
-    expect(getSearchQueryButton().disabled).toBeFalse();
+    expect(globalSearch.find('.message-with-spinner')).toBeUndefined();
+    dom.get(searchQuerySelector).checkDisabled(false);
   });
 
   it('can open SQL view descriptors in how to section', () => {
-    const accordionItems = htmlElement.querySelectorAll<HTMLElement>(
-      '.how-to-search .accordion-item',
-    );
+    const accordionItems = dom.findAll('.how-to-search .accordion-item');
     expect(accordionItems.length).toEqual(6);
     accordionItems.forEach((item) => checkAccordionItemCollapsed(item));
 
-    clickAccordionItemHeader(accordionItems.item(0));
-    checkAccordionItemExpanded(accordionItems.item(0));
-    checkAccordionItemCollapsed(accordionItems.item(1));
+    accordionItems[0].get(accordionItemSelector).click();
+    checkAccordionItemExpanded(accordionItems[0]);
+    checkAccordionItemCollapsed(accordionItems[1]);
 
-    clickAccordionItemHeader(accordionItems.item(1));
-    checkAccordionItemExpanded(accordionItems.item(0));
-    checkAccordionItemExpanded(accordionItems.item(1));
+    accordionItems[1].get(accordionItemSelector).click();
+    checkAccordionItemExpanded(accordionItems[0]);
+    checkAccordionItemExpanded(accordionItems[1]);
 
-    clickAccordionItemHeader(accordionItems.item(0));
-    checkAccordionItemCollapsed(accordionItems.item(0));
-    checkAccordionItemExpanded(accordionItems.item(1));
+    accordionItems[0].get(accordionItemSelector).click();
+    checkAccordionItemCollapsed(accordionItems[0]);
+    checkAccordionItemExpanded(accordionItems[1]);
   });
 
-  function clickGlobalSearchAndCheckMessage(globalSearch: HTMLElement) {
-    globalSearch.click();
-    fixture.detectChanges();
-    expect(globalSearch.querySelector('.message-with-spinner')).toBeTruthy();
-    expect(getSearchQueryButton().disabled).toBeTrue();
-  }
-
-  function getTextInput(i = 0): HTMLTextAreaElement {
-    return htmlElement
-      .querySelectorAll<HTMLTextAreaElement>('.query-field textarea')
-      .item(i);
-  }
-
-  function changeInput(
-    input: HTMLInputElement | HTMLTextAreaElement,
-    query: string,
+  function clickGlobalSearchAndCheckMessage(
+    globalSearch: DOMTestHelper<TestHostComponent>,
   ) {
-    input.value = query;
-    input.dispatchEvent(new Event('input'));
-    fixture.detectChanges();
+    globalSearch.click();
+    expect(dom.find('.message-with-spinner')).toBeDefined();
+    dom.get(searchQuerySelector).checkDisabled(true);
   }
 
-  function getSearchQueryButton(i = 0): HTMLButtonElement {
-    return htmlElement
-      .querySelectorAll<HTMLButtonElement>('.query-actions .search-button')
-      .item(i);
-  }
-
-  function clickSearchQueryButton(i = 0) {
-    getSearchQueryButton(i).click();
-    fixture.detectChanges();
+  function getTextInput(i = 0): DOMTestHelper<TestHostComponent> {
+    return dom.findAll('.query-field textarea')[i];
   }
 
   function runSearchByQueryButton(i = 0) {
-    changeInput(getTextInput(i), testQuery);
-    clickSearchQueryButton(i);
+    getTextInput(i).dispatchInput(testQuery);
+    dom.findAndClickByIndex(searchQuerySelector, i);
   }
 
   async function changeTab(index: number) {
     const matTabGroups = assertDefined(component.searchComponent?.matTabGroups);
     matTabGroups.first.selectedIndex = index;
-    fixture.detectChanges();
-    await fixture.whenStable();
+    await dom.detectChangesAndWaitStable();
   }
 
   async function checkRunQueryFromOptionsWhenResultPresent(tabIndex: number) {
@@ -452,63 +384,46 @@
     data.currentSearches[0].query = testQuery;
     data.currentSearches[0].result = new SearchResult([], []);
     let query: string | undefined;
-    htmlElement
-      .querySelector('viewer-search')
-      ?.addEventListener(ViewerEvents.AddQueryClick, (event) => {
+    dom
+      .get('viewer-search')
+      .addEventListener(ViewerEvents.AddQueryClick, (event) => {
         const detail: AddQueryClickDetail = (event as CustomEvent).detail;
         query = detail.query;
       });
     updateInputDataAndDetectChanges(data);
 
     await changeTab(tabIndex);
-    runSearchFromListedSearchOption();
+    dom.findAndClick(listedSearchSelector);
     expect(query).toEqual(testQuery);
     await changeTab(0);
     runSearchAndCheckHandled(addCurrentSearchWithResult);
-    const activeSections = htmlElement.querySelectorAll('active-search');
-    expect(activeSections.length).toEqual(2);
-  }
-
-  function runSearchFromListedSearchOption() {
-    assertDefined(
-      htmlElement.querySelector<HTMLElement>('.listed-search-option'),
-    ).click();
-    fixture.detectChanges();
+    expect(dom.findAll('active-search').length).toEqual(2);
   }
 
   function runSearchAndCheckHandled(runSearch: () => void) {
     let query: string | undefined;
-    htmlElement
-      .querySelector('viewer-search')
-      ?.addEventListener(ViewerEvents.SearchQueryClick, (event) => {
+    dom
+      .get('viewer-search')
+      .addEventListener(ViewerEvents.SearchQueryClick, (event) => {
         const detail: SearchQueryClickDetail = (event as CustomEvent).detail;
         query = detail.query;
       });
     runSearch();
     expect(query).toEqual(testQuery);
-    expect(getSearchQueryButton().disabled).toBeTrue();
-    const runningQueryMessage = assertDefined(
-      htmlElement.querySelector('.running-query-message'),
-    );
-    expect(runningQueryMessage.textContent?.trim()).toEqual(
-      'timer Calculating results',
-    );
-    expect(runningQueryMessage.querySelector('mat-spinner')).toBeTruthy();
-  }
-
-  function pressEnter(input: HTMLInputElement, shiftKey = false) {
-    input.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', shiftKey}));
-    fixture.detectChanges();
+    dom.get(searchQuerySelector).checkDisabled(true);
+    const runningQueryMessage = dom.get('.running-query-message');
+    runningQueryMessage.checkTextExact('timer Calculating results');
+    expect(runningQueryMessage.find('mat-spinner')).toBeDefined();
   }
 
   async function checkEditQueryFromOptionsWhenResultPresent(tabIndex: number) {
     component.inputData.currentSearches[0].result = new SearchResult([], []);
-    fixture.detectChanges();
+    dom.detectChanges();
 
     let query: string | undefined;
-    htmlElement
-      .querySelector('viewer-search')
-      ?.addEventListener(ViewerEvents.AddQueryClick, (event) => {
+    dom
+      .get('viewer-search')
+      .addEventListener(ViewerEvents.AddQueryClick, (event) => {
         const detail: AddQueryClickDetail = (event as CustomEvent).detail;
         query = detail.query;
       });
@@ -522,34 +437,30 @@
     const data = structuredClone(component.inputData);
     data.currentSearches.push(new CurrentSearch(2, testQuery));
     updateInputDataAndDetectChanges(data);
-    fixture.detectChanges();
-    await fixture.whenStable();
+    await dom.detectChangesAndWaitStable();
     expect(
       component.searchComponent?.matTabGroups?.first.selectedIndex,
     ).toEqual(0);
-    expect(getTextInput(0).value).toEqual('');
-    expect(getTextInput(1).value).toEqual(testQuery);
+    getTextInput(0).checkValue('');
+    getTextInput(1).checkValue(testQuery);
   }
 
   async function checkEditQueryFromOptions(tabIndex: number) {
-    fixture.detectChanges();
+    dom.detectChanges();
     const input = getTextInput();
-    expect(input.value).toEqual('');
+    expect(input.checkValue(''));
     await changeTabAndClickEdit(tabIndex);
     expect(
       component.searchComponent?.matTabGroups?.first.selectedIndex,
     ).toEqual(0);
-    expect(input.value).toEqual(testQuery);
+    expect(input.checkValue(testQuery));
   }
 
   async function changeTabAndClickEdit(tabIndex: number) {
     await changeTab(tabIndex);
-    const listedSearchButton = assertDefined(
-      htmlElement.querySelectorAll<HTMLElement>('.listed-search-option'),
-    );
-    listedSearchButton.item(1).click();
-    fixture.detectChanges();
-    await fixture.whenStable();
+    const listedSearchButton = dom.findAll('.listed-search-option');
+    listedSearchButton[1].click();
+    await dom.whenStable();
   }
 
   function addCurrentSearchWithResult(q = testQuery, uid = 2) {
@@ -559,33 +470,19 @@
     updateInputDataAndDetectChanges(data);
   }
 
-  function getAccordionItemHeader(item: HTMLElement) {
-    return assertDefined(
-      item.querySelector<HTMLElement>('.accordion-item-header'),
-    );
+  function checkAccordionItemCollapsed(item: DOMTestHelper<TestHostComponent>) {
+    item.get(accordionItemSelector).checkText('chevron_right');
+    expect(item.find('.accordion-item-body')).toBeUndefined();
   }
 
-  function clickAccordionItemHeader(item: HTMLElement) {
-    const header = getAccordionItemHeader(item);
-    header.click();
-    fixture.detectChanges();
-  }
-
-  function checkAccordionItemCollapsed(item: HTMLElement) {
-    const header = getAccordionItemHeader(item);
-    expect(header.textContent).toContain('chevron_right');
-    expect(item.querySelector('.accordion-item-body')).toBeNull();
-  }
-
-  function checkAccordionItemExpanded(item: HTMLElement) {
-    const header = getAccordionItemHeader(item);
-    expect(header.textContent).toContain('arrow_drop_down');
-    expect(item.querySelector('.accordion-item-body')).toBeTruthy();
+  function checkAccordionItemExpanded(item: DOMTestHelper<TestHostComponent>) {
+    item.get(accordionItemSelector).checkText('arrow_drop_down');
+    expect(item.find('.accordion-item-body')).toBeDefined();
   }
 
   function updateInputDataAndDetectChanges(data: UiData) {
     component.inputData = data;
-    fixture.detectChanges();
+    dom.detectChanges();
   }
 
   @Component({