diff --git a/packages/protractor/src/screenplay/models/ProtractorPageElement.ts b/packages/protractor/src/screenplay/models/ProtractorPageElement.ts index 360ba281118..b3b3976c5f5 100644 --- a/packages/protractor/src/screenplay/models/ProtractorPageElement.ts +++ b/packages/protractor/src/screenplay/models/ProtractorPageElement.ts @@ -20,6 +20,10 @@ export class ProtractorPageElement extends PageElement { return new ProtractorPageElement(this.locator.of(parent.locator)); } + closestTo(child: ProtractorPageElement): ProtractorPageElement { + return new ProtractorPageElement(this.locator.closestTo(child.locator)); + } + async clearValue(): Promise { // eslint-disable-next-line unicorn/consistent-function-scoping function times(length: number, key: string) { diff --git a/packages/protractor/src/screenplay/models/locators/ProtractorLocator.ts b/packages/protractor/src/screenplay/models/locators/ProtractorLocator.ts index 93b668238dc..5bbdc1c05bd 100644 --- a/packages/protractor/src/screenplay/models/locators/ProtractorLocator.ts +++ b/packages/protractor/src/screenplay/models/locators/ProtractorLocator.ts @@ -1,12 +1,15 @@ +import { LogicError } from '@serenity-js/core'; import type { PageElement, RootLocator, Selector } from '@serenity-js/web'; -import { Locator } from '@serenity-js/web'; -import type * as protractor from 'protractor'; +import { ByCss, Locator } from '@serenity-js/web'; +import * as protractor from 'protractor'; import { unpromisedWebElement } from '../../unpromisedWebElement'; import type { ProtractorErrorHandler } from '../ProtractorErrorHandler'; import { ProtractorPageElement } from '../ProtractorPageElement'; import type { ProtractorRootLocator } from './ProtractorRootLocator'; import { ProtractorSelectors } from './ProtractorSelectors'; +import { WebElement } from 'selenium-webdriver'; +import { promised } from '../../promised'; /** * Protractor-specific implementation of {@apilink Locator}. @@ -46,7 +49,7 @@ export class ProtractorLocator extends Locator { + protected async resolveNativeElement(): Promise { const parent = await this.parent.nativeElement(); const result = await unpromisedWebElement(parent.element(this.nativeSelector())); @@ -65,7 +68,11 @@ export class ProtractorLocator extends Locator { + closestTo(child: ProtractorLocator): Locator { + return new ProtractorParentElementLocator(this.parent, this.selector, child, this.errorHandler); + } + + locate(child: ProtractorLocator): Locator { return new ProtractorLocator(this, child.selector, this.errorHandler); } @@ -102,11 +109,39 @@ export class ProtractorExistingElementLocator extends ProtractorLocator { super(parent, selector, errorHandler); } - async nativeElement(): Promise { + override async nativeElement(): Promise { return this.existingNativeElement; } - async allNativeElements(): Promise> { + override async allNativeElements(): Promise> { return [ this.existingNativeElement ]; } } + +class ProtractorParentElementLocator extends ProtractorLocator { + constructor( + parent: RootLocator, + selector: Selector, + private readonly child: ProtractorLocator, + errorHandler: ProtractorErrorHandler + ) { + super(parent, selector, errorHandler); + } + + protected async resolveNativeElement(): Promise { + const cssSelector = this.asCssSelector(this.selector); + const child = await this.child.nativeElement(); + + const webElement: WebElement = await child.getWebElement(); + + return await promised(webElement.getDriver().executeScript( + `return arguments[0].closest(arguments[1])`, + webElement, + cssSelector.value, + )); + } + + override async allNativeElements(): Promise> { + return [ await this.nativeElement() ]; + } +}