Skip to content

[🐛 Bug]: TypeError: invalid 'instanceof' operand window.ShadowRoot in Firefox 55 + IE11 #11705

@colinrotherham

Description

@colinrotherham

Have you read the Contributing Guidelines on issues?

WebdriverIO Version

8.23.2

Node.js Version

20.9.0

Mode

Standalone Mode

Which capabilities are you using?

[
  {
    browserName: 'firefox',
    browserVersion: '55',
    platformName: 'Windows 10'
  },
  {
    browserName: 'internet explorer',
    browserVersion: '11.285',
    platformName: 'Windows 10'
  }
]

What happened?

I'm upgrading alphagov/accessible-autocomplete from webdriverio@7.33.0 to webdriverio@8.23.2

But I'm now getting errors in Firefox 55 and Internet Explorer 11 using .waitForExist()

TypeError: invalid 'instanceof' operand window.ShadowRoot

It appears to come from the check on line 94 in scripts/isElementDisplayed.ts

// if document-fragment, skip it and use element.host instead. This happens
// when the element is inside a shadow root.
// window.getComputedStyle errors on document-fragment.
if (element instanceof window.ShadowRoot) {
element = element.host
}

Where perhaps the fix in 410ea50 didn't go far enough to guard undefined globals in IE11?

-       if (element instanceof window.ShadowRoot) {
+       if ('ShadowRoot' in window && element instanceof window.ShadowRoot) {

I'd be happy to open a PR

Log output is public below:

SauceLabs via webdriverio@7.33.0

https://github.com/alphagov/accessible-autocomplete/actions/runs/6935370100/job/18865380147

Firefox 55 on Windows 10
https://app.saucelabs.com/tests/2d2579eb2a09406ca29126241d1cef4a

Internet Explorer 11 on Windows 10
https://app.saucelabs.com/tests/5d02b7b34ee749709fc6c9e1f23315d3

SauceLabs via webdriverio@8.23.2

https://github.com/alphagov/accessible-autocomplete/actions/runs/6935148161/job/18864740081?pr=612

Firefox 55 on Windows 10
https://app.saucelabs.com/tests/2d2579eb2a09406ca29126241d1cef4a

Internet Explorer 11 on Windows 10
https://app.saucelabs.com/tests/5d02b7b34ee749709fc6c9e1f23315d3

What is your expected behavior?

Calls to .waitForExist() to succeed in Firefox 55 and Internet Explorer 11

How to reproduce the bug.

This bug report was recreated from alphagov/accessible-autocomplete#612

I've cloned the branch to package-updates-bug-report with the following commits:

  1. Test in webdriverio@7.33.0 via alphagov/accessible-autocomplete@927b0fa
  2. Test in webdriverio@8.23.2 via alphagov/accessible-autocomplete@9637469
npm install
npm run wdio:test

With the following minimal example extracted from test/integration/index.js

describe('basic example', () => {
  let $input

  beforeEach(async () => {
    $input = await $('input#autocomplete-default')
  })

  it('should show the input', async () => {
    await $input.waitForExist()
  })
})

Relevant log output

[
  {
    "screenshot": null,
    "suggestion_values": [],
    "start_time": 1700510392.586,
    "request": {
      "args": [
        {
          "element-6066-11e4-a52e-4f735466cecf": "d8395e7b-5fd5-456e-ab64-532fb440fbe1",
          "ELEMENT": "d8395e7b-5fd5-456e-ab64-532fb440fbe1"
        }
      ],
      "script": "return (function isElementDisplayed(element) {\n    function nodeIsElement(node) {\n        if (!node) {\n            return false;\n        }\n        switch (node.nodeType) {\n            case Node.ELEMENT_NODE:\n            case Node.DOCUMENT_NODE:\n            case Node.DOCUMENT_FRAGMENT_NODE:\n                return true;\n            default:\n                return false;\n        }\n    }\n    function parentElementForElement(element) {\n        if (!element) {\n            return null;\n        }\n        return enclosingNodeOrSelfMatchingPredicate(element.parentNode, nodeIsElement);\n    }\n    function enclosingNodeOrSelfMatchingPredicate(targetNode, predicate) {\n        for (let node = targetNode; node && node !== targetNode.ownerDocument; node = node.parentNode) {\n            if (predicate(node)) {\n                return node;\n            }\n        }\n        return null;\n    }\n    function enclosingElementOrSelfMatchingPredicate(targetElement, predicate) {\n        for (let element = targetElement; element && element !== targetElement.ownerDocument; element = parentElementForElement(element)) {\n            if (predicate(element)) {\n                return element;\n            }\n        }\n        return null;\n    }\n    function cascadedStylePropertyForElement(element, property) {\n        if (!element || !property) {\n            return null;\n        }\n        // if document-fragment, skip it and use element.host instead. This happens\n        // when the element is inside a shadow root.\n        // window.getComputedStyle errors on document-fragment.\n        if (element instanceof window.ShadowRoot) {\n            element = element.host;\n        }\n        const computedStyle = window.getComputedStyle(element);\n        const computedStyleProperty = computedStyle.getPropertyValue(property);\n        if (computedStyleProperty && computedStyleProperty !== 'inherit') {\n            return computedStyleProperty;\n        }\n        // Ideally getPropertyValue would return the 'used' or 'actual' value, but\n        // it doesn't for legacy reasons. So we need to do our own poor man's cascade.\n        // Fall back to the first non-'inherit' value found in an ancestor.\n        // In any case, getPropertyValue will not return 'initial'.\n        // FIXME: will this incorrectly inherit non-inheritable CSS properties?\n        // I think all important non-inheritable properties (width, height, etc.)\n        // for our purposes here are specially resolved, so this may not be an issue.\n        // Specification is here: https://drafts.csswg.org/cssom/#resolved-values\n        const parentElement = parentElementForElement(element);\n        return cascadedStylePropertyForElement(parentElement, property);\n    }\n    function elementSubtreeHasNonZeroDimensions(element) {\n        const boundingBox = element.getBoundingClientRect();\n        if (boundingBox.width > 0 && boundingBox.height > 0) {\n            return true;\n        }\n        // Paths can have a zero width or height. Treat them as shown if the stroke width is positive.\n        if (element.tagName.toUpperCase() === 'PATH' && boundingBox.width + boundingBox.height > 0) {\n            const strokeWidth = cascadedStylePropertyForElement(element, 'stroke-width');\n            return !!strokeWidth && (parseInt(strokeWidth, 10) > 0);\n        }\n        const cascadedOverflow = cascadedStylePropertyForElement(element, 'overflow');\n        if (cascadedOverflow === 'hidden') {\n            return false;\n        }\n        // If the container's overflow is not hidden and it has zero size, consider the\n        // container to have non-zero dimensions if a child node has non-zero dimensions.\n        return Array.from(element.childNodes).some((childNode) => {\n            if (childNode.nodeType === Node.TEXT_NODE) {\n                return true;\n            }\n            if (nodeIsElement(childNode)) {\n                return elementSubtreeHasNonZeroDimensions(childNode);\n            }\n            return false;\n        });\n    }\n    function elementOverflowsContainer(element) {\n        const cascadedOverflow = cascadedStylePropertyForElement(element, 'overflow');\n        if (cascadedOverflow !== 'hidden') {\n            return false;\n        }\n        // FIXME: this needs to take into account the scroll position of the element,\n        // the display modes of it and its ancestors, and the container it overflows.\n        // See Selenium's bot.dom.getOverflowState atom for an exhaustive list of edge cases.\n        return true;\n    }\n    function isElementSubtreeHiddenByOverflow(element) {\n        if (!element) {\n            return false;\n        }\n        if (!elementOverflowsContainer(element)) {\n            return false;\n        }\n        if (!element.childNodes.length) {\n            return false;\n        }\n        // This element's subtree is hidden by overflow if all child subtrees are as well.\n        return Array.from(element.childNodes).every((childNode) => {\n            // Returns true if the child node is overflowed or otherwise hidden.\n            // Base case: not an element, has zero size, scrolled out, or doesn't overflow container.\n            // Visibility of text nodes is controlled by parent\n            if (childNode.nodeType === Node.TEXT_NODE) {\n                return false;\n            }\n            if (!nodeIsElement(childNode)) {\n                return true;\n            }\n            if (!elementSubtreeHasNonZeroDimensions(childNode)) {\n                return true;\n            }\n            // Recurse.\n            return isElementSubtreeHiddenByOverflow(childNode);\n        });\n    }\n    // walk up the tree testing for a shadow root\n    function isElementInsideShadowRoot(element) {\n        if (!element) {\n            return false;\n        }\n        if (element.parentNode && element.parentNode.host) {\n            return true;\n        }\n        return isElementInsideShadowRoot(element.parentNode);\n    }\n    // This is a partial reimplementation of Selenium's \"element is displayed\" algorithm.\n    // When the W3C specification's algorithm stabilizes, we should implement that.\n    // If this command is misdirected to the wrong document (and is NOT inside a shadow root), treat it as not shown.\n    if (!isElementInsideShadowRoot(element) && !document.contains(element)) {\n        return false;\n    }\n    // Special cases for specific tag names.\n    switch (element.tagName.toUpperCase()) {\n        case 'BODY':\n            return true;\n        case 'SCRIPT':\n        case 'NOSCRIPT':\n            return false;\n        case 'OPTGROUP':\n        case 'OPTION': {\n            // Option/optgroup are considered shown if the containing <select> is shown.\n            const enclosingSelectElement = enclosingNodeOrSelfMatchingPredicate(element, (e) => e.tagName.toUpperCase() === 'SELECT');\n            return isElementDisplayed(enclosingSelectElement);\n        }\n        case 'INPUT':\n            // <input type=\"hidden\"> is considered not shown.\n            if (element.type === 'hidden') {\n                return false;\n            }\n            break;\n        // case 'MAP':\n        // FIXME: Selenium has special handling for <map> elements. We don't do anything now.\n        default:\n            break;\n    }\n    if (cascadedStylePropertyForElement(element, 'visibility') !== 'visible') {\n        return false;\n    }\n    const hasAncestorWithZeroOpacity = !!enclosingElementOrSelfMatchingPredicate(element, (e) => {\n        return Number(cascadedStylePropertyForElement(e, 'opacity')) === 0;\n    });\n    const hasAncestorWithDisplayNone = !!enclosingElementOrSelfMatchingPredicate(element, (e) => {\n        return cascadedStylePropertyForElement(e, 'display') === 'none';\n    });\n    if (hasAncestorWithZeroOpacity || hasAncestorWithDisplayNone) {\n        return false;\n    }\n    if (!elementSubtreeHasNonZeroDimensions(element)) {\n        return false;\n    }\n    if (isElementSubtreeHiddenByOverflow(element)) {\n        return false;\n    }\n    return true;\n}).apply(null, arguments)"
    },
    "result": {
      "message": "TypeError: invalid 'instanceof' operand window.ShadowRoot",
      "error": "javascript error"
    },
    "duration": 0.031000137329101562,
    "path": "execute/sync",
    "hide_from_ui": false,
    "between_commands": 0.21899986267089844,
    "visual_command": false,
    "HTTPStatus": 500,
    "suggestion": null,
    "request_id": "f9b0d865-bc67-45ee-a86a-5324b0dd5b01",
    "in_video_timeline": 9.75,
    "method": "POST",
    "statusCode": 1
  }
]

Code of Conduct

  • I agree to follow this project's Code of Conduct

Is there an existing issue for this?

  • I have searched the existing issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions