dperini / nwsapi

Fast CSS Selectors API Engine
MIT License
103 stars 35 forks source link

TypeError: Cannot read properties of undefined (reading 'toLowerCase') #73

Closed ewlsh closed 1 year ago

ewlsh commented 1 year ago

Hi šŸ‘‹ I'm working on upgrading our codebase from Jest 26 to Jest 29 and alongside that we're upping our jsdom version and thus nwsapi. I've been struggling with this very odd error in some components and eventually tracked it down to the code nwsapi is generating. What seems to be happening is whenever a component contains duet date picker *ByRole selectors from React Testing Library start to fail. It seems to occur when jsdom evaluates a component within the duet date picker.

It seems that with this code generated by nwsapi, e.element&&e.type.toLowerCase() introduced here e.type is undefined for these duet components and thus the generated code throws an error. I changed this to be e.element&&e.type&&e.type.toLowerCase() locally and my tests are now passing again.

Duet does use custom web elements, but I'm not sure if that is relevant. Additionally it uses more complex selectors than most of our components, so could also be a factor.

The error:


    TypeError: Cannot read properties of undefined (reading 'toLowerCase')

      107 |
      108 |      const name = component.getTranslation('translation:common.submit');
    > 109 |     const submitButton = component.getByRole('button', {
          |                                    ^
      110 |         name: name,
      111 |     });
      112 |     userEvent.click(submitButton);

      at Array.Resolver (eval at compile (../node_modules/nwsapi/src/nwsapi.js:773:17), <anonymous>:3:83)
      at match_assert (../node_modules/nwsapi/src/nwsapi.js:1356:13)
      at Object._matches [as match] (../node_modules/nwsapi/src/nwsapi.js:1374:16)
      at exports.matchesDontThrow (../node_modules/jsdom/lib/jsdom/living/helpers/selectors.js:29:36)
      at matches (../node_modules/jsdom/lib/jsdom/living/helpers/style-rules.js:50:10)
      at ../node_modules/jsdom/lib/jsdom/living/helpers/style-rules.js:35:18
          at Array.forEach (<anonymous>)
      at handleSheet (../node_modules/jsdom/lib/jsdom/living/helpers/style-rules.js:26:13)
          at Array.forEach (<anonymous>)
      at Object.exports.forEachMatchingSheetRuleOfElement (../node_modules/jsdom/lib/jsdom/living/helpers/style-rules.js:46:11)
      at getCascadedPropertyValue (../node_modules/jsdom/lib/jsdom/living/helpers/style-rules.js:62:11)
      at getSpecifiedValue (../node_modules/jsdom/lib/jsdom/living/helpers/style-rules.js:80:19)
      at getComputedValue (../node_modules/jsdom/lib/jsdom/living/helpers/style-rules.js:100:12)
      at getSpecifiedValue (../node_modules/jsdom/lib/jsdom/living/helpers/style-rules.js:89:12)
      at getComputedValue (../node_modules/jsdom/lib/jsdom/living/helpers/style-rules.js:100:12)
      at getSpecifiedValue (../node_modules/jsdom/lib/jsdom/living/helpers/style-rules.js:89:12)
      at getComputedValue (../node_modules/jsdom/lib/jsdom/living/helpers/style-rules.js:100:12)
      at getSpecifiedValue (../node_modules/jsdom/lib/jsdom/living/helpers/style-rules.js:89:12)
      at getComputedValue (../node_modules/jsdom/lib/jsdom/living/helpers/style-rules.js:100:12)
      at exports.getResolvedValue (../node_modules/jsdom/lib/jsdom/living/helpers/style-rules.js:111:10)
      at ../node_modules/jsdom/lib/jsdom/browser/Window.js:819:41
          at Array.forEach (<anonymous>)
      at getComputedStyleImplementation (../node_modules/jsdom/lib/jsdom/browser/Window.js:818:13)
      at isHidden (../node_modules/dom-accessibility-api/sources/accessible-name-and-description.ts:84:16)
      at computeTextAlternative (../node_modules/dom-accessibility-api/sources/accessible-name-and-description.ts:559:4)
      at computeTextAlternative (../node_modules/dom-accessibility-api/sources/accessible-name-and-description.ts:696:3)
      at computeAccessibleName (../node_modules/dom-accessibility-api/sources/accessible-name.ts:40:9)
      at ../node_modules/@testing-library/react/node_modules/@testing-library/dom/dist/queries/role.js:132:82
          at Array.filter (<anonymous>)
      at queryAllByRole (../node_modules/@testing-library/react/node_modules/@testing-library/dom/dist/queries/role.js:127:6)
      at ../node_modules/@testing-library/react/node_modules/@testing-library/dom/dist/query-helpers.js:74:17
      at ../node_modules/@testing-library/react/node_modules/@testing-library/dom/dist/query-helpers.js:52:17
      at getByRole (../node_modules/@testing-library/react/node_modules/@testing-library/dom/dist/query-helpers.js:95:19)

Code generated before change:

if(e.element&&e.type.toLowerCase()=="::before"){e=e.element;n=e;while((e=e.parentElement)){if((/(^|\s)duet-date(\s|$)/.test(e.getAttribute("class")))){r=true;}}e=n;}
if(e.element&&e.type.toLowerCase()=="::after"){e=e.element;n=e;while((e=e.parentElement)){if((/(^|\s)duet-date(\s|$)/.test(e.getAttribute("class")))){r=true;}}e=n;}
if(e.element&&e.type.toLowerCase()=="::-webkit-input-placeholder"){e=e.element;if((/(^|\s)duet-date__input(\s|$)/.test(e.getAttribute("class")))){r=true;}}
if(e.element&&e.type.toLowerCase()=="::before"){e=e.element;if(!true){if((/(^|\s)duet-date__day(\s|$)/.test(e.getAttribute("class")))){r=true;}}}
if(e.element&&e.type.toLowerCase()=="::before"){e=e.element;if((/(^|\s)is-today(\s|$)/.test(e.getAttribute("class")))){if((/(^|\s)duet-date__day(\s|$)/.test(e.getAttribute("class")))){r=true;}}}
if(e.element&&e.type.toLowerCase()=="::before"){e=e.element;if(!s.match(".is-today",e)){if(((/^true$/).test(e.getAttribute&&e.getAttribute("aria-disabled"))==true)){if((/(^|\s)duet-date__day(\s|$)/.test(e.getAttribute("class")))){r=true;}}}}
if(e.element&&e.type.toLowerCase()=="::before"){e=e.element;if((/(^|\s)is-outside(\s|$)/.test(e.getAttribute("class")))){if((/(^|\s)duet-date__day(\s|$)/.test(e.getAttribute("class")))){r=true;}}}

Code generated after change:

if(e.element&&e.type&&e.type.toLowerCase()=="::before"){e=e.element;n=e;while((e=e.parentElement)){if((/(^|\s)duet-date(\s|$)/.test(e.getAttribute("class")))){r=true;}}e=n;}
if(e.element&&e.type&&e.type.toLowerCase()=="::after"){e=e.element;n=e;while((e=e.parentElement)){if((/(^|\s)duet-date(\s|$)/.test(e.getAttribute("class")))){r=true;}}e=n;}
if(e.element&&e.type&&e.type.toLowerCase()=="::-webkit-input-placeholder"){e=e.element;if((/(^|\s)duet-date__input(\s|$)/.test(e.getAttribute("class")))){r=true;}}
if(e.element&&e.type&&e.type.toLowerCase()=="::before"){e=e.element;if(!true){if((/(^|\s)duet-date__day(\s|$)/.test(e.getAttribute("class")))){r=true;}}}
if(e.element&&e.type&&e.type.toLowerCase()=="::before"){e=e.element;if((/(^|\s)is-today(\s|$)/.test(e.getAttribute("class")))){if((/(^|\s)duet-date__day(\s|$)/.test(e.getAttribute("class")))){r=true;}}}
if(e.element&&e.type&&e.type.toLowerCase()=="::before"){e=e.element;if(!s.match(".is-today",e)){if(((/^true$/).test(e.getAttribute&&e.getAttribute("aria-disabled"))==true)){if((/(^|\s)duet-date__day(\s|$)/.test(e.getAttribute("class")))){r=true;}}}}
if(e.element&&e.type&&e.type.toLowerCase()=="::before"){e=e.element;if((/(^|\s)is-outside(\s|$)/.test(e.getAttribute("class")))){if((/(^|\s)duet-date__day(\s|$)/.test(e.getAttribute("class")))){r=true;}}}
dperini commented 1 year ago

@ewlsh great finding ... these kind of bugs might be affecting other selectors too. I will have this fixed asap together with others I am currently working on. Thank you for your help and for reporting the error.

dperini commented 1 year ago

@ewlsh the issue should be resolved by the above commit soo I am closing this. Feel free to reopen in case you still have problems, remember to send a reproducible test case.

jas-chen commented 1 year ago

Hi @dperini I still encounter this issue after upgrading to v2.2.3. Here is the reproducible test case, the issue is caused by the element property on the custom element. https://github.com/jas-chen/nwsapi-issue-73

dperini commented 1 year ago

@jas-chen Thank you for reporting this issue. I will have a deeper look to this. While at it, can you also repeat the same test using the latest 2.2.4 release ? Release 2.2.3 was a failure and I will probably remove it from npm in a few days.

dperini commented 1 year ago

@jas-chen & @ewlsh here is a simplified test case as an attempt to triage this bug in the right repository, it is the same test case as posted above, I just got rid of React and Jest dependencies and left the simplest native JavaScript code to reproduce this bug. Here is the HTML/JavaScript code with which I performed my test:

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>customElements and nwsapi</title>
<script src="src/nwsapi.js"></script>
<script>
window.onload = () => {

  class MyElement extends HTMLElement {
    static properties = {
      element: {},
    };

    render() {
      return this.element;
    }
  }

  customElements.define("my-element", MyElement);

  const style = document.createElement("style");
  style.innerHTML = `
      body::before {
        content: '';
      }`;

  document.head.appendChild(style);

  document.body.innerHTML = '<my-element element="hello"></my-element>';

  const myElement = document.querySelector("my-element");
  // this replaces the "expect" line in the original test
  console.log(myElement.attributes['element'].value);
  console.log(window.getComputedStyle(myElement));

  // these are additional queries to ensure nwsapi works
  console.log(document.querySelector("my-element"));
  console.log(document.querySelector("my-element[element]").attributes);
  console.log(document.querySelector("my-element[element]").getAttribute('element'));
};
</script>
</head>
<body>

</body>
</html>

Please copy the above code and paste it in the console and check if the results are correct. If that is the case, I guess we should start looking for the issue in another place.

Thank you for the help with this.

jas-chen commented 1 year ago

@dperini

can you also repeat the same test using the latest 2.2.4 release ?

Yes, the issue is still there after upgrading to 2.2.4.

https://github.com/jas-chen/nwsapi-issue-73/blob/d47f2c03cd1f00ccfefeac2b1289dfda8c8b92ff/package-lock.json#L856

Please copy the above code and paste it in the console and check if the results are correct.

The result is not correct, myElement.element should have value 'hello', also the error TypeError: Cannot read properties of undefined (reading 'toLowerCase') is not thrown.

Here is the code that I consider to be correct (note: the API of my-element is changed, the attribute element is replaced with the element property).

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>customElements and nwsapi</title>
<script src="src/nwsapi.js"></script>
<script>
window.onload = () => {
  class MyElement extends HTMLElement {
    get element() {
      return "hello";
    }

    render() {
      return this.element;
    }
  }

  customElements.define("my-element", MyElement);

  const style = document.createElement("style");
  style.innerHTML = `
      body::before {
        content: '';
      }`;

  document.head.appendChild(style);

  document.body.innerHTML = '<my-element></my-element>';

  const myElement = document.querySelector("my-element");

  if (myElement.element !== "hello") {
    throw new Error(`myElement.element should have value 'hello'`);
  }

  // should throw TypeError: Cannot read properties of undefined (reading 'toLowerCase')
  console.log(window.getComputedStyle(myElement));
};
</script>
</head>
<body>

</body>
</html>
dperini commented 1 year ago

@jas-chen & @ewlsh
the results of the tests executed with "nwsapi" and native qSA are the same, that tells me "nwsapi" is ok but I don't know the code you are using nor am I familiar with your environment so there might be quirks I did not consider.

If you can setup a minimal native JavaScript/DOM code showing different behavior compared to that of "nwsapi", then I can dig further in the issue and try to fix my code else I should give up and only rely on your input.

The scope of "nwsapi" is mimic and replicate the functionality of the native browsers qSA, in browsers and headless environments like "jsdom".

Looking forward for a reproducible test case, without dependencies, showing the issue in "nwsapi". Thank you for your help and availability.

dperini commented 1 year ago

@jas-chen & @ewlsh I forgot to tell that in case we do not discover the reason of the issue or if we are unable to fix it, I am open to consider adding that small extra check that @ewlsh mentioned in his first report, thus changing:

e.element&&e.type.toLowerCase()

with an extra check as suggested:

e.element&&e.type&&e.type.toLowerCase()

ensuring first that this change doesn't affect other functionalities or produces other issues.

jas-chen commented 1 year ago

@dperini Sorry I didn't test the HTML code in the browser, it seems that only breaks in jsdom.

I made another code that should produce the issue in the browser (tested with Chrome).

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <script src="https://unpkg.com/nwsapi@2.2.4/src/nwsapi.js"></script>
</head>
<body>
  <script>
    class MyElement extends HTMLElement {
      get element() {
        return "hello";
      }

      render() {
        return this.element;
      }
    }

    customElements.define("my-element", MyElement);

    document.body.innerHTML = '<my-element></my-element>';

    function match() {
      return document.querySelector("my-element").matches('my-element::after');
    }

    // this works
    console.log(match());

    NW.Dom.install();

    // throws `TypeError: Cannot read properties of undefined (reading 'toLowerCase')`
    console.log(match());
  </script>
</body>
</html>
jas-chen commented 1 year ago

@dperini please correct me if I am wrong, but I don't see this issue fixed?