diff --git a/cypress/integration/find-cypress-widget.ts b/cypress/integration/find-cypress-widget.ts index 01f109620..1c8e63dd5 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 cb0198762..8beaca91c 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 4b13b9c51..7d0d60b43 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 931442acf..4952a0321 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 @@ -8,7 +8,7 @@ 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 { AngularWidgetObjectFinder } from './angular-widget-finder'; /** * Angular implementation of the Widget Object's internal HTML Element wrapper @@ -21,7 +21,7 @@ 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)))); @@ -118,7 +118,7 @@ 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 aec11687a..aacf1b5dd 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 0a950e023..540c9a317 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 3143e17e3..cb26dfe56 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,13 +26,13 @@ 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); let chainable: any; @@ -50,7 +52,7 @@ 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); @@ -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 3b1f3a252..cb9bef30c 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/widget-object.ts b/projects/components/src/utils/test/widget-object/widget-object.ts index 17cdd4db7..fdf0e73b0 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 @@ -171,7 +188,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 +229,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)),