Skip to content

Commit

Permalink
(webdriverio): polish isElementClickable (#12492)
Browse files Browse the repository at this point in the history
  • Loading branch information
erwinheitzman committed Mar 14, 2024
1 parent 6f9c046 commit 0130787
Show file tree
Hide file tree
Showing 4 changed files with 36 additions and 33 deletions.
15 changes: 8 additions & 7 deletions packages/webdriverio/src/commands/element/isClickable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import isElementClickableScript from '../../scripts/isElementClickable.js'

/**
*
* Return true if the selected DOM-element:
*
* - exists
* - is visible
* - is within viewport (if not try scroll to it)
* - its center is not overlapped with another element
* - is not disabled
* An element is considered to be clickable when the following conditions are met:
*
* - the element exists
* - the element is displayed
* - the element is not disabled
* - the element is within the viewport
* - the element can be scrolled into the viewport
* - the element's center is not overlapped with another element
*
* otherwise return false.
*
Expand Down
45 changes: 25 additions & 20 deletions packages/webdriverio/src/scripts/isElementClickable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,13 @@ export default function isElementClickable (elem: HTMLElement) {
// applicable if element's text is multiline.
function getOverlappingRects (elem: HTMLElement, context?: Document) {
context = context || document
const elems = []

const rects = elem.getClientRects()
// webdriver clicks on center of the first element's rect (line of text), it might change in future
const rect = rects[0]
const x = rect.left + (rect.width / 2)
const y = rect.top + (rect.height / 2)
elems.push(context.elementFromPoint(x, y))

return elems
return [context.elementFromPoint(x, y)]
}

// get overlapping elements
Expand Down Expand Up @@ -120,27 +117,35 @@ export default function isElementClickable (elem: HTMLElement) {
return (vertInView && horInView)
}

function isClickable (elem: any) {
return (
isElementInViewport(elem) && elem.disabled !== true &&
isOverlappingElementMatch(getOverlappingElements(elem) as any as HTMLElement[], elem)
)
function isEnabled(elem: HTMLFormElement) {
return elem.disabled !== true
}

function hasOverlaps(elem: HTMLElement) {
return !isOverlappingElementMatch(getOverlappingElements(elem) as any as HTMLElement[], elem)
}

// scroll to the element if it's not clickable
if (!isClickable(elem)) {
// works well in dialogs, but the element may be still overlapped by some sticky header/footer
elem.scrollIntoView(scrollIntoViewFullSupport ? { block: 'nearest', inline: 'nearest' } : false)
function isFullyDisplayedInViewport(elem: HTMLElement) {
return isElementInViewport(elem) && !hasOverlaps(elem)
}

// scroll the element to the center of the viewport when
// it is not fully displayed in the viewport or is overlapped by another element
// to check if it still overlapped/not in the viewport
// afterwards we scroll back to the original position
let _isFullyDisplayedInViewport = isFullyDisplayedInViewport(elem)
if (!_isFullyDisplayedInViewport) {
const { x: originalX, y: originalY } = elem.getBoundingClientRect()

elem.scrollIntoView(scrollIntoViewFullSupport ? { block: 'center', inline: 'center' } : false)

// if element is still not clickable take another scroll attempt
if (!isClickable(elem)) {
// scroll to element, try put it in the screen center.
// Should definitely work even if element was covered with sticky header/footer
elem.scrollIntoView(scrollIntoViewFullSupport ? { block: 'center', inline: 'center' } : true)
_isFullyDisplayedInViewport = isFullyDisplayedInViewport(elem)

return isClickable(elem)
const { x, y } = elem.getBoundingClientRect()
if (x !== originalX || y !== originalY) {
elem.scroll(scrollX, scrollY)
}
}

return true
return _isFullyDisplayedInViewport && isEnabled(elem as HTMLFormElement)
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe('isClickable test', () => {
vi.mocked(got).mockClear()
})

it('should allow to check if element is displayed', async () => {
it('should allow to check if element is clickable', async () => {
await elem.isClickable()
expect(vi.mocked(got).mock.calls[0][0]!.pathname)
.toBe('/session/foobar-123/execute/sync')
Expand Down
7 changes: 2 additions & 5 deletions packages/webdriverio/tests/scripts/isElementClickable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,11 @@ describe('isElementClickable script', () => {
let attempts = 0
global.document = { elementFromPoint: () => {
attempts += 1
return { parentNode: attempts > 4 ? elemMock : {} }
return { parentNode: attempts > 3 ? elemMock : {} }
} } as any

expect(isElementClickable(elemMock)).toBe(true)
expect(elemMock.scrollIntoView).toBeCalledWith(false)
expect(elemMock.scrollIntoView).toBeCalledWith(true)
})

it('should be clickable if in viewport and elementFromPoint if element is Document Fragment [Edge]', () => {
Expand All @@ -98,12 +97,11 @@ describe('isElementClickable script', () => {
let attempts = 0
global.document = { elementFromPoint: () => {
attempts += 1
return { parentNode: attempts > 4 ? elemMock : {} }
return { parentNode: attempts > 3 ? elemMock : {} }
} } as any

expect(isElementClickable(elemMock)).toBe(true)
expect(elemMock.scrollIntoView).toBeCalledWith(false)
expect(elemMock.scrollIntoView).toBeCalledWith(true)
})

it('should be clickable if in viewport and elementFromPoint of the rect matches', () => {
Expand Down Expand Up @@ -199,7 +197,6 @@ describe('isElementClickable script', () => {
global.document = { elementFromPoint: () => null } as any

expect(isElementClickable(elemMock)).toBe(false)
expect(elemMock.scrollIntoView).toBeCalledWith({ block: 'nearest', inline: 'nearest' })
expect(elemMock.scrollIntoView).toBeCalledWith({ block: 'center', inline: 'center' })
})

Expand Down

0 comments on commit 0130787

Please sign in to comment.