Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
3 tasks done
colinrotherham opened this issue Nov 20, 2023 · 0 comments · Fixed by #11706
Closed
3 tasks done
Labels
Bug 🐛 Needs Triaging ⏳ No one has looked into the issue yet

Comments

@colinrotherham
Copy link
Contributor

colinrotherham commented Nov 20, 2023

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
@colinrotherham colinrotherham added Bug 🐛 Needs Triaging ⏳ No one has looked into the issue yet labels Nov 20, 2023
colinrotherham added a commit to colinrotherham/webdriverio that referenced this issue Nov 20, 2023
…iverio#11705)

Fixes an issue in older browsers where the right-hand operand in `element instanceof window.ShadowRoot` isn’t callable and has no `prototype` so it throws the error:

```
TypeError: invalid 'instanceof' operand window.ShadowRoot
```

Confirmed to affect Firefox 55 and Internet Explorer 11 in webdriverio#11705

See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/invalid_right_hand_side_instanceof_operand
christian-bromann pushed a commit that referenced this issue Nov 20, 2023
… (#11706)

Fixes an issue in older browsers where the right-hand operand in `element instanceof window.ShadowRoot` isn’t callable and has no `prototype` so it throws the error:

```
TypeError: invalid 'instanceof' operand window.ShadowRoot
```

Confirmed to affect Firefox 55 and Internet Explorer 11 in #11705

See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/invalid_right_hand_side_instanceof_operand
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug 🐛 Needs Triaging ⏳ No one has looked into the issue yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant