diff --git a/cypress/integration/find-cypress-widget.ts b/cypress/integration/find-cypress-widget.ts index 01f10962..1c8e63dd 100644 --- a/cypress/integration/find-cypress-widget.ts +++ b/cypress/integration/find-cypress-widget.ts @@ -9,7 +9,7 @@ import { DataExporterWidgetObject, FindableWidget, } from '@vcd/ui-components'; -import { FindCypressWidgetOptions } from '@vcd/ui-components'; +import { FindElementOptions } from '@vcd/ui-components'; import Cypress from 'cypress'; type Chainable = Cypress.Chainable; @@ -27,11 +27,11 @@ type Chainable = Cypress.Chainable; */ export function findCypressWidget>( widgetConstructor: FindableWidget, - findOptions?: FindCypressWidgetOptions + findOptions?: FindElementOptions ): W { return new CypressWidgetObjectFinder().find(widgetConstructor, findOptions) as W; } -export function findDataExporter(options?: FindCypressWidgetOptions): DataExporterWidgetObject { +export function findDataExporter(options?: FindElementOptions): DataExporterWidgetObject { return findCypressWidget>(DataExporterWidgetObject, options); } diff --git a/projects/components/CHANGELOG.MD b/projects/components/CHANGELOG.MD index cb019876..8beaca91 100644 --- a/projects/components/CHANGELOG.MD +++ b/projects/components/CHANGELOG.MD @@ -7,11 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [2.0.0-dev.8] - ### Added - Find widget by data ui attribute. +### Change +- Widget find/action options are now type safe + +## [2.0.0-dev.8] + ### Added - `clear` is now a method in a widget object. - Added functionality to disable an option in `FormSelectComponent` diff --git a/projects/components/src/utils/test/widget-object/angular/angular-widget-finder.ts b/projects/components/src/utils/test/widget-object/angular/angular-widget-finder.ts index 4b13b9c5..7d0d60b4 100644 --- a/projects/components/src/utils/test/widget-object/angular/angular-widget-finder.ts +++ b/projects/components/src/utils/test/widget-object/angular/angular-widget-finder.ts @@ -9,13 +9,6 @@ import { By } from '@angular/platform-browser'; import { BaseWidgetObject, FindableWidget, FindElementOptions } from '../widget-object'; import { AngularWidgetObjectElement, TestElement } from './angular-widget-object-element'; -/** - * Adds Angular specific options for finding widgets - */ -export interface FindAngularWidgetOptions extends FindElementOptions { - ancestor?: DebugElement; -} - /** * Knows how to find and instantiate Angular Widgets objects. */ @@ -49,7 +42,7 @@ export class AngularWidgetObjectFinder { */ public find>( widgetConstructor: FindableWidget, - findOptions: FindAngularWidgetOptions = {} + findOptions: FindElementOptions = {} ): W { const { ancestor } = findOptions; @@ -60,7 +53,7 @@ export class AngularWidgetObjectFinder { if (findOptions?.cssSelector) { query = query + findOptions.cssSelector; } - const parentQuery: FindAngularWidgetOptions = { + const parentQuery: FindElementOptions = { cssSelector: query, dataUiSelector: findOptions?.dataUiSelector, text: findOptions?.text, diff --git a/projects/components/src/utils/test/widget-object/angular/angular-widget-object-element.ts b/projects/components/src/utils/test/widget-object/angular/angular-widget-object-element.ts index 931442ac..cdaceb43 100644 --- a/projects/components/src/utils/test/widget-object/angular/angular-widget-object-element.ts +++ b/projects/components/src/utils/test/widget-object/angular/angular-widget-object-element.ts @@ -7,8 +7,14 @@ import { DebugElement, Injector, Type } from '@angular/core'; import { ComponentFixture } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { SelectorUtil } from '../selector-util'; -import { BaseWidgetObject, FindableWidget, FindElementOptions, WidgetObjectElement } from '../widget-object'; -import { AngularWidgetObjectFinder, FindAngularWidgetOptions } from './angular-widget-finder'; +import { + AngularWidgetActionOptions, + BaseWidgetObject, + FindableWidget, + FindElementOptions, + WidgetObjectElement, +} from '../widget-object'; +import { AngularWidgetObjectFinder } from './angular-widget-finder'; /** * Angular implementation of the Widget Object's internal HTML Element wrapper @@ -21,8 +27,8 @@ export class AngularWidgetObjectElement implements WidgetObjectElement): AngularWidgetObjectElement { + const cssSelector = SelectorUtil.extractSelector(selector); const elements = this.testElement.elements; let matches = [].concat(...elements.map((element) => element.queryAll(By.css(cssSelector)))); if (typeof selector !== 'string') { @@ -34,23 +40,20 @@ export class AngularWidgetObjectElement implements WidgetObjectElement element.queryAll(By.css(cssSelector)))); - nextElements = nextElements.filter((el) => el.nativeElement.textContent.includes(value)); - return new AngularWidgetObjectElement(new TestElement(nextElements, this.testElement.fixture)); - } /** * @inheritdoc */ - parents(cssSelector: string): AngularWidgetObjectElement { + parents(cssSelector: string | FindElementOptions): AngularWidgetObjectElement { + let selector: string; + if (typeof cssSelector === 'string') { + selector = cssSelector; + } else { + selector = SelectorUtil.extractSelector(cssSelector); + } return new AngularWidgetObjectElement( new TestElement( - this.testElement.elements.map((el) => this.findParent(cssSelector, el.parent)), + this.testElement.elements.map((el) => this.findParent(selector, el.parent)), this.testElement.fixture ) ); @@ -59,33 +62,33 @@ export class AngularWidgetObjectElement implements WidgetObjectElement>( widgetCtor: FindableWidget, - findOptions: FindAngularWidgetOptions + findOptions: FindElementOptions ): W { return new AngularWidgetObjectFinder(this.testElement.fixture).find(widgetCtor, { ...findOptions, diff --git a/projects/components/src/utils/test/widget-object/cypress/cypress-widget-finder.ts b/projects/components/src/utils/test/widget-object/cypress/cypress-widget-finder.ts index aec11687..aacf1b5d 100644 --- a/projects/components/src/utils/test/widget-object/cypress/cypress-widget-finder.ts +++ b/projects/components/src/utils/test/widget-object/cypress/cypress-widget-finder.ts @@ -4,17 +4,12 @@ */ import { IdGenerator } from '../../../id-generator/id-generator'; -import { BaseWidgetObject, FindableWidget, FindElementOptions } from '../widget-object'; +import { BaseWidgetObject, CypressWidgetActionOptions, FindableWidget, FindElementOptions } from '../widget-object'; import { CypressWidgetObjectElement } from './cypress-widget-object-element'; declare const cy; const idGenerator = new IdGenerator('cy-id'); -export interface FindCypressWidgetOptions extends FindElementOptions { - ancestor?: string; - options?: { timeout: number }; -} - /** * Knows how to find and construct Cypress widget objects within the DOM. * @@ -34,19 +29,20 @@ export class CypressWidgetObjectFinder { */ public find>( widgetConstructor: FindableWidget, - findOptions?: FindCypressWidgetOptions + findOptions?: FindElementOptions ): W { const id = idGenerator.generate(); + const options: CypressWidgetActionOptions = findOptions?.options; const ancestor = findOptions?.ancestor - ? cy.get(findOptions?.ancestor, { timeout: findOptions?.options?.timeout }) - : cy.get('body', { timeout: findOptions?.options?.timeout }); + ? cy.get(findOptions?.ancestor, { timeout: options?.timeout }) + : cy.get('body', { timeout: options?.timeout }); const parentWidget = new CypressWidgetObjectElement(ancestor, false, undefined); let query = widgetConstructor.tagName; if (findOptions?.cssSelector) { query = query + findOptions.cssSelector; } - const parentQuery: FindCypressWidgetOptions = { + const parentQuery: FindElementOptions = { cssSelector: query, dataUiSelector: findOptions?.dataUiSelector, text: findOptions?.text, diff --git a/projects/components/src/utils/test/widget-object/cypress/cypress-widget-object-element.spec.ts b/projects/components/src/utils/test/widget-object/cypress/cypress-widget-object-element.spec.ts index 0a950e02..540c9a31 100644 --- a/projects/components/src/utils/test/widget-object/cypress/cypress-widget-object-element.spec.ts +++ b/projects/components/src/utils/test/widget-object/cypress/cypress-widget-object-element.spec.ts @@ -118,8 +118,8 @@ describe('CypressWidgetObjectElement', () => { it('clears the given input', () => { const clearSpy = spyOn(cy, 'clear').and.callThrough(); const widget = new CypressWidgetObjectElement(cy, true, '1'); - widget.clear({ a: 'test' }); - expect(clearSpy).toHaveBeenCalledWith({ a: 'test' }); + widget.clear({ timeout: 1 }); + expect(clearSpy).toHaveBeenCalledWith({ timeout: 1 }); }); }); }); diff --git a/projects/components/src/utils/test/widget-object/cypress/cypress-widget-object-element.ts b/projects/components/src/utils/test/widget-object/cypress/cypress-widget-object-element.ts index 3143e17e..7e3d44a8 100644 --- a/projects/components/src/utils/test/widget-object/cypress/cypress-widget-object-element.ts +++ b/projects/components/src/utils/test/widget-object/cypress/cypress-widget-object-element.ts @@ -5,12 +5,14 @@ import { SelectorUtil } from '../selector-util'; import { BaseWidgetObject, + CypressWidgetActionOptions, ElementActions, FindableWidget, FindElementOptions, + WidgetActionOptions, WidgetObjectElement, } from '../widget-object'; -import { CypressWidgetObjectFinder, FindCypressWidgetOptions } from './cypress-widget-finder'; +import { CypressWidgetObjectFinder } from './cypress-widget-finder'; declare const cy; @@ -24,15 +26,15 @@ declare const cy; * [this PR](https://github.com/vmware/vmware-cloud-director-ui-components/pull/248) * we could not load the Cypress types in our library. */ -export class CypressWidgetObjectElement implements WidgetObjectElement { +export class CypressWidgetObjectElement> implements WidgetObjectElement { constructor(private chainable: T, private isRoot: boolean, private alias: string) {} /** * @inheritdoc */ - get(selector: string | FindElementOptions): CypressWidgetObjectElement { + get(selector: string | FindElementOptions): CypressWidgetObjectElement { const root = this.getBase(); - const cssSelector = SelectorUtil.extractSelector(selector); + const cssSelector = SelectorUtil.extractSelector(selector); let chainable: any; if (typeof selector === 'string') { chainable = root.find(selector); @@ -50,13 +52,13 @@ export class CypressWidgetObjectElement implements Wid /** * @inheritdoc */ - parents(selector: string | FindElementOptions): CypressWidgetObjectElement { + parents(selector: string | FindElementOptions): CypressWidgetObjectElement { const root = this.getBase(); if (typeof selector === 'string') { return new CypressWidgetObjectElement(root.parents(selector), false, this.alias); } return new CypressWidgetObjectElement( - root.parents(SelectorUtil.extractSelector(selector), selector.options), + root.parents(SelectorUtil.extractSelector(selector), selector.options), false, this.alias ); @@ -72,50 +74,53 @@ export class CypressWidgetObjectElement implements Wid /** * @inheritdoc */ - click(options?: unknown): void { + click(options?: WidgetActionOptions): void { this.chainable.click(options); } /** * @inheritdoc */ - type(value: string, options: unknown): void { + type(value: string, options: WidgetActionOptions): void { this.chainable.type(value, options); } /** * @inheritdoc */ - select(text: string, options: unknown): void { + select(text: string, options: WidgetActionOptions): void { this.chainable.select(text, options); } /** * @inheritdoc */ - check(options?: unknown): void { + check(options?: WidgetActionOptions): void { this.chainable.check(options); } /** * @inheritdoc */ - uncheck(options?: unknown): void { + uncheck(options?: WidgetActionOptions): void { this.chainable.uncheck(options); } /** * @inheritdoc */ - clear(options?: unknown): void { + clear(options?: WidgetActionOptions): void { this.chainable.clear(options); } /** * @inheritdoc */ - findWidget>(widget: FindableWidget, findOptions: FindCypressWidgetOptions): W { - return new CypressWidgetObjectFinder().find(widget, { ancestor: '@' + this.alias, ...findOptions }); + findWidget>(widget: FindableWidget, findOptions: FindElementOptions): W { + return new CypressWidgetObjectFinder().find(widget, { + ancestor: '@' + this.alias, + ...findOptions, + } as FindElementOptions); } /** diff --git a/projects/components/src/utils/test/widget-object/index.ts b/projects/components/src/utils/test/widget-object/index.ts index 3b1f3a25..cb9bef30 100644 --- a/projects/components/src/utils/test/widget-object/index.ts +++ b/projects/components/src/utils/test/widget-object/index.ts @@ -4,7 +4,7 @@ */ export { BaseWidgetObject, FindableWidget } from './widget-object'; -export { CypressWidgetObjectFinder, FindCypressWidgetOptions } from './cypress/cypress-widget-finder'; +export { CypressWidgetObjectFinder } from './cypress/cypress-widget-finder'; export { AngularWidgetObjectFinder } from './angular/angular-widget-finder'; export { TestElement } from './angular/angular-widget-object-element'; -export { FindElementOptions } from './widget-object'; +export { FindElementOptions, WidgetActionOptions } from './widget-object'; diff --git a/projects/components/src/utils/test/widget-object/selector-util.ts b/projects/components/src/utils/test/widget-object/selector-util.ts index f1c33fe6..0ba02fce 100644 --- a/projects/components/src/utils/test/widget-object/selector-util.ts +++ b/projects/components/src/utils/test/widget-object/selector-util.ts @@ -8,7 +8,7 @@ export class SelectorUtil { /** * Extracts the selector from the parameter passed */ - static extractSelector(selector: string | FindElementOptions): string { + static extractSelector(selector: string | FindElementOptions): string { if (typeof selector === 'string') { return selector; } else { diff --git a/projects/components/src/utils/test/widget-object/widget-object.ts b/projects/components/src/utils/test/widget-object/widget-object.ts index 17cdd4db..1dfb526e 100644 --- a/projects/components/src/utils/test/widget-object/widget-object.ts +++ b/projects/components/src/utils/test/widget-object/widget-object.ts @@ -2,22 +2,39 @@ * Copyright 2020 VMware, Inc. * SPDX-License-Identifier: BSD-2-Clause */ + +import { DebugElement } from '@angular/core'; +import { TestElement } from './angular/angular-widget-object-element'; + /** * A function tht finds an HTML Element wrapper of some type `T` */ -type ElementLocator = (options?: FindElementOptions) => T; +type ElementLocator = (options?: FindElementOptions) => T; + +/** + * The options that a call to a Cypress widget will accept. + */ +export interface CypressWidgetActionOptions { + timeout?: number; +} + +/** + * The options that a call to a Angular widget will accept. + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface AngularWidgetActionOptions {} /** - * Like unknown but can be an object but also needs to be cast before it can be used (since an empty object doesn't - * allow properties) - * * These options will be passed down to the implementation when querying or running actions. For example, when * specifying a timeout for a cypress command. An implementation specific type should be used in their implementation of * the WidgetObjectElement */ -type UnknownOptions = {}; +export type WidgetActionOptions = T extends TestElement ? AngularWidgetActionOptions : CypressWidgetActionOptions; -export type FindElementOptions = { +/** + * These are the options a user has when searching for an element. + */ +export type FindElementOptions = { /** CSS selector used to query. Ignored if {@link #dataUiSelector} is passed */ cssSelector?: string; @@ -31,10 +48,10 @@ export type FindElementOptions = { text?: string; /** An implementation specific parent can be used to start the search */ - ancestor?: unknown; + ancestor?: T extends TestElement ? DebugElement : string; /** Implementation specific options. For example, timeouts in cypress */ - options?: UnknownOptions; + options?: WidgetActionOptions; }; /** @@ -45,61 +62,61 @@ interface Locator { * Finds all descendants by CSS selector * @param selector - Can be a CSS query string or a FindElementOptions for more refined querying */ - get(selector: string | FindElementOptions): WidgetObjectElement; + get(selector: string | FindElementOptions): WidgetObjectElement; /** * Finds the closest parent that matches the given cssSelector. Ignores the index attribute */ - parents(selector: string | FindElementOptions): WidgetObjectElement; + parents(selector: string | FindElementOptions): WidgetObjectElement; /** * Returns an instance of the given widget within this widget object. */ - findWidget>(widget: FindableWidget, findOptions?: FindElementOptions): W; + findWidget>(widget: FindableWidget, findOptions?: FindElementOptions): W; } /** * Actions that can be taken on elements from within widget objects. This * is a subset of the functionality from Cypress */ -export interface ElementActions { +export interface ElementActions { /** * Clicks an element, it must typically be visible * @param options Options to be passed down to implementations */ - click(options?: UnknownOptions): void; + click(options?: WidgetActionOptions): void; /** * Types into a text field * @param value What to type into the field * @param options Options to be passed down to implementations */ - type(value: string, options?: UnknownOptions): void; + type(value: string, options?: WidgetActionOptions): void; /** * For checkboxes, makes sure a box is checked * @param options Options to be passed down to implementations */ - check(options?: UnknownOptions): void; + check(options?: WidgetActionOptions): void; /** * For checkboxes, makes sure a box is unchecked * @param options Options to be passed down to implementations */ - uncheck(options?: UnknownOptions): void; + uncheck(options?: WidgetActionOptions): void; /** * For select elements * @param value The text of the dropdown to select * @param options Options to be passed down to implementations */ - select(value: string, options: UnknownOptions): void; + select(value: string, options: WidgetActionOptions): void; /** * For inputs or text areas, clears the current value. * @param options Options to be passed down to implementations */ - clear(options?: UnknownOptions): void; + clear(options?: WidgetActionOptions): void; } /** @@ -113,7 +130,7 @@ export interface ElementActions { * `T` is the type of the external interface for an HTMLELement. For example, DebugElement in Angular or Cy * */ -export interface WidgetObjectElement extends Locator, ElementActions { +export interface WidgetObjectElement extends Locator, ElementActions { /** * Unwraps the value from this WidgetObjectElement and turns it into the resulting object type (T) which is what * should be exposed to subclasses @@ -151,12 +168,6 @@ export interface WidgetObjectElement extends Locator, ElementActions { * * // Don't need an internal version, use the factory methods to create public methods * public errorMessage = this.factory.dataUi('error-message'); - * - * // Sometimes there's no factory for more complex finding logic, write a custom query. - * // Be sure to call unwrap on the result if exposing it publicly - * getOkButtonContainer(): T { - * return this.el.getByText('button', 'Ok').parent('.button-container').unwrap(); - * } * ... * * login(user, password) { @@ -171,7 +182,7 @@ export class BaseWidgetObject { * This is like {@link #factory} but it returns WidgetObjectElements, which can be used internally */ protected internalFactory = { - css: (cssSelector: string) => (options?: FindElementOptions) => this.el.get({ cssSelector, ...options }), + css: (cssSelector: string) => (options?: FindElementOptions) => this.el.get({ cssSelector, ...options }), dataUi: (name: string) => this.internalFactory.css(`[data-ui="${name}"]`), }; @@ -212,8 +223,8 @@ export class BaseWidgetObject { /** * Utility to create versions of factories that return the unwrapped T */ - unwrap: (fun: ElementLocator>) => { - return (options?: FindElementOptions) => fun(options).unwrap(); + unwrap: (fun: ElementLocator, T>) => { + return (options?: FindElementOptions) => fun(options).unwrap(); }, css: (cssSelector: string) => this.factory.unwrap(this.internalFactory.css(cssSelector)),