| /* |
| * Copyright (C) 2025 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| import {Type} from '@angular/core'; |
| import {ComponentFixture, flush} from '@angular/core/testing'; |
| import {By} from '@angular/platform-browser'; |
| import {assertDefined} from 'common/assert_utils'; |
| import {KeyboardEventKey, KeyboardEventKeyCode} from 'common/dom_utils'; |
| |
| export class DOMTestHelper<T> { |
| constructor( |
| private fixture: ComponentFixture<T>, |
| private root: HTMLElement, |
| ) {} |
| |
| detectChanges() { |
| this.fixture.detectChanges(); |
| } |
| |
| async whenStable() { |
| await this.fixture.whenStable(); |
| } |
| |
| async whenRenderingDone() { |
| await this.fixture.whenRenderingDone(); |
| } |
| |
| async detectChangesAndWaitStable() { |
| this.detectChanges(); |
| await this.whenStable(); |
| } |
| |
| async detectChangesAndRenderingDone() { |
| this.detectChanges(); |
| await this.fixture.whenRenderingDone(); |
| } |
| |
| find(selector: string): DOMTestHelper<T> | undefined { |
| const element = this.root.querySelector<HTMLElement>(selector); |
| return element ? new DOMTestHelper(this.fixture, element) : undefined; |
| } |
| |
| findAll(selector: string): Array<DOMTestHelper<T>> { |
| const helpers: Array<DOMTestHelper<T>> = []; |
| this.root.querySelectorAll<HTMLElement>(selector).forEach((el) => { |
| helpers.push(new DOMTestHelper(this.fixture, el)); |
| }); |
| return helpers; |
| } |
| |
| findByDirective<T>(component: Type<T>): T | undefined { |
| return ( |
| this.fixture.debugElement.query(By.directive(component)) |
| ?.componentInstance ?? undefined |
| ); |
| } |
| |
| findInDocument(selector: string): DOMTestHelper<T> | undefined { |
| const element = document.querySelector<HTMLElement>(selector); |
| return element ? new DOMTestHelper(this.fixture, element) : undefined; |
| } |
| |
| get(selector: string): DOMTestHelper<T> { |
| return assertDefined(this.find(selector)); |
| } |
| |
| getInDocument(selector: string): DOMTestHelper<T> { |
| return assertDefined(this.findInDocument(selector)); |
| } |
| |
| click() { |
| this.root.click(); |
| this.fixture.detectChanges(); |
| } |
| |
| findAndClick(selector: string): DOMTestHelper<T> { |
| const element = this.get(selector); |
| element.click(); |
| return element; |
| } |
| |
| findAndClickByIndex(selector: string, index: number): DOMTestHelper<T> { |
| const element = this.findAll(selector)[index]; |
| element.click(); |
| return element; |
| } |
| |
| findAndClickInDocument(selector: string): DOMTestHelper<T> { |
| const element = this.getInDocument(selector); |
| element.click(); |
| return element; |
| } |
| |
| async clickAndWaitStable(selector: string) { |
| const element = this.get(selector); |
| element.click(); |
| await this.whenStable(); |
| } |
| |
| async clickByIndexAndWaitStable(selector: string, index: number) { |
| const element = this.findAll(selector)[index]; |
| element.click(); |
| await this.whenStable(); |
| } |
| |
| async clickLastAndWaitStable(selector: string) { |
| const elements = this.findAll(selector); |
| const element = elements[elements.length - 1]; |
| element.click(); |
| await this.whenStable(); |
| } |
| |
| findAndDispatchInput(field: string, value: string): DOMTestHelper<T> { |
| const input = this.get(field + ' input'); |
| input.dispatchInput(value); |
| return input; |
| } |
| |
| dispatchInput(value: string) { |
| if ( |
| !( |
| this.root instanceof HTMLInputElement || |
| this.root instanceof HTMLTextAreaElement |
| ) |
| ) { |
| throw new Error('cannot dispatch input on node ' + this.root.nodeName); |
| } |
| this.root.value = value; |
| this.root.dispatchEvent(new Event('input')); |
| this.fixture.detectChanges(); |
| } |
| |
| isMatSelectOpen(): boolean { |
| return this.findInDocument('.mat-select-panel') !== undefined; |
| } |
| |
| async openMatSelect(index = 0) { |
| const trigger = '.mat-select-trigger'; |
| await this.clickByIndexAndWaitStable(trigger, index); |
| } |
| |
| clickMatOption() { |
| const panel = this.getMatSelectPanel(); |
| panel.findAndClick('mat-option'); |
| } |
| |
| getMatSelectPanel(): DOMTestHelper<T> { |
| return this.getInDocument('.mat-select-panel'); |
| } |
| |
| findMatTooltipPanel(): DOMTestHelper<T> | undefined { |
| return this.findInDocument('.mat-tooltip-panel'); |
| } |
| |
| getSnackBar(): DOMTestHelper<T> { |
| return this.getInDocument('snack-bar'); |
| } |
| |
| addEventListener(event: string, listener: (event: Event) => void) { |
| this.root.addEventListener(event, listener); |
| } |
| |
| keydownEnter(shiftKey = false) { |
| const event = new KeyboardEvent('keydown', { |
| key: KeyboardEventKey.ENTER, |
| shiftKey, |
| }); |
| this.dispatchEvent(event); |
| } |
| |
| keydownEsc() { |
| const event = new KeyboardEvent('keydown', {key: KeyboardEventKey.ESCAPE}); |
| this.dispatchEvent(event); |
| } |
| |
| keydownSpace() { |
| const event = new KeyboardEvent('keydown', { |
| keyCode: KeyboardEventKeyCode.SPACE, |
| }); |
| this.dispatchEvent(event); |
| } |
| |
| keydownArrowLeft(toDocument = false) { |
| const event = new KeyboardEvent('keydown', { |
| key: KeyboardEventKey.ARROW_LEFT, |
| }); |
| toDocument |
| ? this.dispatchEventInDocument(event) |
| : this.dispatchEvent(event); |
| } |
| |
| keydownArrowRight(toDocument = false) { |
| const event = new KeyboardEvent('keydown', { |
| key: KeyboardEventKey.ARROW_RIGHT, |
| }); |
| toDocument |
| ? this.dispatchEventInDocument(event) |
| : this.dispatchEvent(event); |
| } |
| |
| focusOut() { |
| this.dispatchEvent(new FocusEvent('focusout')); |
| } |
| |
| dragElement(x: number, y: number) { |
| const {left, top} = this.root.getBoundingClientRect(); |
| this.dispatchMouseEvent(this.root, 'mousedown', left, top, 0, 0); |
| this.dispatchMouseEvent(document, 'mousemove', left + 1, top + 0, 1, y); |
| this.dispatchMouseEvent(document, 'mousemove', left + x, top + y, x, y); |
| this.dispatchMouseEvent(document, 'mouseup', left + x, top + y, x, y); |
| } |
| |
| dispatchEvent(event: Event) { |
| this.root.dispatchEvent(event); |
| this.detectChanges(); |
| } |
| |
| dispatchEventInDocument(event: Event) { |
| document.dispatchEvent(event); |
| this.detectChanges(); |
| } |
| |
| getHTMLElement<T extends HTMLElement>() { |
| return this.root as T; |
| } |
| |
| getText(): string | undefined { |
| return this.root.textContent?.trim() ?? undefined; |
| } |
| |
| checkText(value: string) { |
| expect(this.root.textContent?.trim()).toContain(value); |
| } |
| |
| checkTextExact(value: string) { |
| expect(this.root.textContent?.trim()).toEqual(value); |
| } |
| |
| checkInnerHTML(value: string, isPresent = true) { |
| isPresent |
| ? expect(this.root.innerHTML).toContain(value) |
| : expect(this.root.innerHTML).not.toContain(value); |
| } |
| |
| checkClassName(value: string, isPresent = true) { |
| isPresent |
| ? expect(this.root.className).toContain(value) |
| : expect(this.root.className).not.toContain(value); |
| } |
| |
| checkClassNameExact(value: string, isPresent = true) { |
| isPresent |
| ? expect(this.root.className).toEqual(value) |
| : expect(this.root.className).not.toEqual(value); |
| } |
| |
| checkDisabled(value: boolean) { |
| if ('disabled' in this.root) { |
| return (this.root as any).disabled === value; |
| } |
| throw new Error('disabled not present on node ' + this.root.nodeName); |
| } |
| |
| checkValue(value: string) { |
| if ('value' in this.root) { |
| return (this.root as any).value === value; |
| } |
| throw new Error('value not present on node ' + this.root.nodeName); |
| } |
| |
| updateValue(value: string) { |
| (this.root as any).value = value; |
| } |
| |
| checkSectionCollapseAndExpand(selector: string, sectionTitle: string) { |
| const section = this.get(selector); |
| section |
| .get('collapsible-section-title .mat-title') |
| .checkTextExact(sectionTitle); |
| section.findAndClick('collapsible-section-title button'); |
| section.checkClassName('collapsed'); |
| const collapsedSection = this.get('collapsed-sections .collapsed-section'); |
| collapsedSection.checkTextExact(sectionTitle + ' arrow_right'); |
| collapsedSection.click(); |
| this.checkNoCollapsedSectionButtons(); |
| } |
| |
| checkNoCollapsedSectionButtons() { |
| const collapsedSections = this.get('collapsed-sections'); |
| expect(collapsedSections.find('.collapsed-section')).toBeUndefined(); |
| } |
| |
| async checkTooltip(text: string | undefined) { |
| this.dispatchEvent(new Event('mouseenter')); |
| const panel = this.findMatTooltipPanel(); |
| if (text !== undefined) { |
| assertDefined(panel).checkText(text); |
| } else { |
| expect(panel).toBeUndefined(); |
| } |
| this.dispatchEvent(new Event('mouseleave')); |
| await this.whenStable(); |
| } |
| |
| private dispatchMouseEvent( |
| source: Node, |
| type: string, |
| screenX: number, |
| screenY: number, |
| clientX: number, |
| clientY: number, |
| ) { |
| const event = document.createEvent('MouseEvent'); |
| event.initMouseEvent( |
| type, |
| true /* canBubble */, |
| false /* cancelable */, |
| window /* view */, |
| 0 /* detail */, |
| screenX /* screenX */, |
| screenY /* screenY */, |
| clientX /* clientX */, |
| clientY /* clientY */, |
| false /* ctrlKey */, |
| false /* altKey */, |
| false /* shiftKey */, |
| false /* metaKey */, |
| 0 /* button */, |
| null /* relatedTarget */, |
| ); |
| Object.defineProperty(event, 'buttons', {get: () => 1}); |
| source.dispatchEvent(event); |
| this.detectChanges(); |
| flush(); |
| } |
| } |
| |
| export async function checkTooltips<T>( |
| elements: Array<DOMTestHelper<T>>, |
| expTooltips: Array<string | undefined>, |
| ) { |
| for (const [index, el] of elements.entries()) { |
| await el.checkTooltip(expTooltips[index]); |
| } |
| } |