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

wrapper.find('#name') returning two elements but only one element is shown in wrapper.html() #836

Closed
RyanAtViceSoftware opened this issue Mar 3, 2017 · 47 comments

Comments

@RyanAtViceSoftware
Copy link

wrapper.find('#name') returning two elements but only one element is shown in wrapper.html().

expected outcome

only one element would be returned by wrapper.find('#name') and my eventual goal is to get the val of that textarea.

wrapper.find('#name')

image

wrapper.html()

Note that there is only the one element with id=name below <textarea id="name">Activate or redeem credit card</textarea>

<div class="App">
   <div id="siteModal" class="hide">
      <div class="modalInnerContainer">
         <div class="modalTitle">
            <!-- react-text: 5 --><!-- /react-text --><span class="modalExit">X</span>
         </div>
         <div class="modalContent"></div>
         <div class="modalActions"><button class="action">CANCEL</button><button class="action">CONTINUE</button></div>
      </div>
   </div>
   <div>
      <div class="flyoutPanel left ">
         <div class="flyoutPanelContent">
            <div class="panelHeader">
               <svg class="dsi dsiClose closePanel">
                  <use xlink:href="symbol-defs.svg#dsiClose"></use>
               </svg>
               <!-- react-text: 17 --><!-- /react-text -->
            </div>
            <div class="panelContent">
               <div>
                  <div class="sidePanelInnerContainer">
                     <div class="inputContainer">
                        <label for="name">Name</label>
                        <textarea id="name">Activate or redeem credit card</textarea>
                        <hr>
                     </div>
                     <div class="inputContainer">
                        <label for="description">Description</label>
                        <textarea id="description" name="description"></textarea>
                        <hr>
                     </div>
                  </div>
                  <button id="closeDrawer" style="visibility: hidden;">Close Flyout</button>
               </div>
            </div>
            <div class="panelFooter"></div>
         </div>
      </div>
      <a class="closeDrawer" style="z-index: 1;"></a>
      <div class="flyoutPanel right  show">
         <div class="flyoutPanelContent">
            <div class="panelHeader">
               <svg class="dsi dsiClose closePanel">
                  <use xlink:href="symbol-defs.svg#dsiClose"></use>
               </svg>
               <h3 class="title">Process Step Details</h3>
            </div>
            <div class="panelContent">
               <div>
                  <div class="sidePanelInnerContainer">
                     <div class="inputContainer">
                        <label for="name">Name</label>
                        <textarea id="name">Activate or redeem credit card</textarea>
                        <hr>
                     </div>
                     <div class="inputContainer">
                        <label for="description">Description</label>
                        <textarea id="description" name="description"></textarea>
                        <hr>
                     </div>
                  </div>
                  <button id="closeDrawer" style="visibility: hidden;">Close Flyout</button>
               </div>
            </div>
            <div class="panelFooter"></div>
         </div>
      </div>
      <a class="closeDrawer active" style="z-index: 1;"></a>
   </div>
   <div class="App-header">
      <span class="app-header-icon"><span class="letter">S</span><span class="dot">.</span></span><span class="app-header-segment">Entity Name Here</span>
      <span class="app-header-segment">
         <!-- react-text: 170 -->FY16 Year End Audit<!-- /react-text --><!-- react-text: 42 -->&nbsp;<!-- /react-text -->
      </span>
      <span class="app-header-segment green small-text">STAGE</span><span class="app-header-actions"><span class="temp-action">&nbsp;</span></span>
   </div>
   <div class="currentProcess">
      <div class="currentProcessNameContainer"><label id="processNameLabel" class="currentProcessLabel">Financial Statement Review</label><input type="hidden" id="processNameInput" class="currentProcessName" value="Financial Statement Review" style="width: 55%;"><span class="last-saved">Last saved: 8:23 PM</span><span class="currentProcessActions">&nbsp;</span></div>
      <div>
         <div class="mainContent">
            <div class="canvasHeader">
               <div class="canvasHeaderLeft">
                  <div style="display: inline-flex; width: 400px; height: 40px; background-color: rgba(0, 0, 0, 0.01); position: relative; z-index: 100;"></div>
               </div>
               <div class="canvasHeaderRight">
                  <svg class="dsi dsiUndo">
                     <use xlink:href="symbol-defs.svg#dsiUndo"></use>
                  </svg>
                  <svg class="dsi dsiRedo">
                     <use xlink:href="symbol-defs.svg#dsiRedo"></use>
                  </svg>
                  <svg class="dsi dsiEye">
                     <use xlink:href="symbol-defs.svg#dsiEye"></use>
                  </svg>
                  <svg class="dsi dsiZoom">
                     <use xlink:href="symbol-defs.svg#dsiZoom"></use>
                  </svg>
                  <svg class="dsi dsiMore">
                     <use xlink:href="symbol-defs.svg#dsiMore"></use>
                  </svg>
               </div>
            </div>
            <div class="cardsContainer" style="width: 100%; position: relative;">
               <div class="cardContainer">
                  <div class="toggleEngagementLibrary"></div>
                  <div class="cardInner mainCanvas" style="display: block; width: 100%; height: 865px; background-color: rgb(255, 255, 255);"></div>
               </div>
            </div>
            <div><button class="button primaryButton" type="submit" style="margin-left: 24px; margin-right: 10px; padding: 10px 16px;">Save</button><textarea id="customTextEditor" class="customTextEditorBlock"></textarea><textarea id="customOffSetTextEditor" class="customTextEditorBlock"></textarea></div>
         </div>
      </div>
   </div>
</div>
@ljharb
Copy link
Member

ljharb commented Mar 3, 2017

Is the wrapper using mount or shallow? What is the .html() of the list of 2 text areas?

@ljharb
Copy link
Member

ljharb commented Mar 3, 2017

Also what's the component code look like? It's possible cheerio is filtering out a duplicate ID.

@RyanAtViceSoftware
Copy link
Author

Wow, thanks for the quick response. I'm using mount. I get an error when I try and do .html()

image

@RyanAtViceSoftware
Copy link
Author

This is really weird to me also. This creates an error

image

but this works

image

so i don't know how i do an html on the second element.

@ljharb
Copy link
Member

ljharb commented Mar 3, 2017

wrapper.find('#name').eq(1).html()

@RyanAtViceSoftware
Copy link
Author

no luck wrapper.find('#name').eq(1).html()

image

@ljharb
Copy link
Member

ljharb commented Mar 3, 2017

oops, sorry :-) .at(1) should do it

@RyanAtViceSoftware
Copy link
Author

I guess I could always just RTFM... thanks man! That seemed to work. But I'm getting two elements which is unexpected and odd to me.

image

@ljharb
Copy link
Member

ljharb commented Mar 4, 2017

@RyanAtViceSoftware the next step is, could you provide the actual React component code that generates this, as well as the code you're using in your tests to create the wrapper?

@RyanAtViceSoftware
Copy link
Author

@ljharb Here's the component and test

Component

import React, { Component } from 'react';
import '../../../../node_modules/deloitte-symphony-core-fe/dist/scss/components/form.scss';
import '../../../assets/sass/tabstyles.scss';
import '../../../assets/sass/sidePanelContainer.scss';
import * as icActions from '../../../state/actions/internalControl.action';
import { updateRightDrawerContent } from '../../../state/actions/drawer.action';
import { CANVAS_OBJECT_TYPES } from '../../../constants/canvasObjectTypes';

class OperationDetails extends Component {
  constructor(props) {
    super(props);
    this.closePanel = this.closePanel.bind(this);
    this.handleInputChange = this.handleInputChange.bind(this);
  }

  closePanel() {
    // TODO: Ryan - We should be firing an action here and using redux state
    var closeButton = document.getElementById("closeDrawer");
    closeButton.click();
  }

  handleInputChange(event, field) {
    let target = event.target;
    let content = this.props.content;
    if (target !== undefined) {
      const value = target.type === 'checkbox' ? target.checked : target.value;
      content.dto[field] = value;
      this.props.content.dispatch(icActions.updateProcessDtos(content.dto, CANVAS_OBJECT_TYPES.Operation));
      this.props.content.dispatch(updateRightDrawerContent(content));
    }
  }

  // TODO: Ryan - We should use container\presentational pattern here
  render() {
    const { content } = this.props;
    if (content) {
      return (
          <div className="sidePanelInnerContainer">
            <div className="inputContainer">
              <label htmlFor="name">Name</label>
              <textarea id="name"
                     value={(content.dto) ? content.dto.text : ''}
                     onChange={(e) => {
                       this.handleInputChange(e, "text");
                     }}
              ></textarea>
              <hr />
            </div>
            <div className="inputContainer">
              <label htmlFor="description">Description</label>
              <textarea id="description" name="description" defaultValue={content.dto && content.dto.description} onChange={(e) => {
                this.handleInputChange(e, "description")
              }}></textarea>
              <hr />
            </div>
          </div>
      );
    }
    else {
      return <div/>;
    }
  }
}

export default OperationDetails;

Test

import React from 'react';
import chai, { expect } from 'chai';
import { Provider } from 'react-redux';
import { mount, shallow } from 'enzyme';
import createRootReducer from '../../../src/state/reducers/createRootReducer';
import createStore from '../../../src/state/stores/main.store';
import '../../setupTests';
import App from '../../../src/containers/App/App';
import behaviors from '../behaviors';
import expected from './rightDrawerExpected';
import chaiProperties from 'chai-properties';
const go = (process.env.NODE_ENV === 'production') ? require('../../../GoJs/1.6.23-go-src') :
  require('../../../GoJs/1.6.23-go-src-debug');
import * as chartUtils from '../../../src/common/util/chart/canvas.utils';
import ProcessCanvasComponent from '../../../src/components/canvas/processCanvas';
import {stubComponent} from '../../testUtils';
chai.use(chaiProperties);

chai.use(function(_chai, _) {
  _chai.Assertion.addMethod('withMessage', function(msg) {
    _.flag(this, 'message', msg);
  });
});

function getTestingStore(initialState) {
  const store = createStore(createRootReducer(initialState));

  const dispatch = action => {
    store.dispatch(action);
    return {
      andExpect: function(expected) {
        expect(store.getState(),
          "Store state was not what was expected after dispatching the following action: "
          + JSON.stringify(action))
          .to.have.properties(expected);
      }
    }
  };

  return {
    dispatch,
    getState: store.getState,
    subscribe: store.subscribe,
    replacementReducer: store.replaceReducer
  };
}

describe('Given I open a process and expand the details on an operation', () => {
  it(
    'when I update the title in the details pane then the operation on the canvas also shows the title',
    () => {

      const symphonyContext = {
        engagementId: '897c6642-2244-4b7c-8311-198ed54602ce'
      };

      const store = getTestingStore();

      const diagramMock = {
        toolManager: {
          linkingTool: { linkValidation: function() {} },
          relinkingTool: { linkValidation: function() {} }
        },
        startTransaction: function() {}
      };

      chartUtils.AddNodeShapes = function() {
      };
      chartUtils.AddPalette = function() {
      };

      go.GraphObject = {
        make: function() {
          return diagramMock;
        }
      };

      stubComponent(ProcessCanvasComponent);

      const wrapper = mount(<Provider store={store}><App
        symphonyContext={symphonyContext}/></Provider>);

      store.dispatch(behaviors.setSymponyContext);

      store.dispatch(behaviors.setConnectionRules);

      store.dispatch(behaviors.loadProcesses);

      store.dispatch(behaviors.setSymphonyEngagement);

      store.dispatch(behaviors.setCurrentProcess);

      store.dispatch(behaviors.navigateToProcessFlowDiagram);

      store.dispatch(behaviors.openOperationInRightDrawer);

      const nameTextAreaWrapper = wrapper.find('#name').first();
      const nameTextAreaHtml = nameTextAreaWrapper.html();
      const expectedNameTextAreaHtml = '<textarea id="name">Activate or redeem credit card</textarea>';

      expect(nameTextAreaHtml).to.equal(expectedNameTextAreaHtml);

      nameTextAreaWrapper.simulate('keydown', { which: 'c' })

      const nameTextAreaHtmlAfter = wrapper.find('#name').first().html();
      const expectedNameTextAreaHtmlAfter = '<textarea id="name">Activate or redeem credit card</textarea>';
      expect(nameTextAreaHtmlAfter).to.equal(expectedNameTextAreaHtmlAfter);
    });
});

@frankmariette
Copy link

@RyanAtViceSoftware Have you found a solution for this issue? Currently having a similar experience trying to locate text in a component.

@RyanAtViceSoftware
Copy link
Author

@frankmariette unfortunately no...

@ljharb
Copy link
Member

ljharb commented Sep 26, 2017

Is this still an issue in v3?

@CoderK
Copy link

CoderK commented Oct 16, 2017

I have a same issue in v3.

@ljharb
Copy link
Member

ljharb commented Oct 16, 2017

@CoderK can you file a new issue, just in case?

@CoderK
Copy link

CoderK commented Oct 17, 2017

#1174

@ljharb
I got a hint from another issue card. This seems to be a compatibility issue with upgrading Enzyme v3. Tests are broken a lot ㅜㅜ. Other users seem to have some similar misunderstanding.

@skrece22
Copy link

@RyanAtViceSoftware
You created a wrapper for Whole App instead of the component you are testing. It looks like you have one more component that uses name as id . Check other components.

@RyanAtBarefoot
Copy link

RyanAtBarefoot commented Jan 25, 2018

@skrece22 there was not more than one component with name as id, that was the whole point of the issue and not I'm using mount, not shallow, and mount is intended for testing more than one component.

@skrece22
Copy link

@RyanAtBarefoot OperationDetails must rendered twice. Please share github repo for this hope it willbe more helpful to fix the bug.

@badaljain
Copy link

@RyanAtViceSoftware Whats the IDE you are using?

@RyanAtBarefoot
Copy link

@badaljain i was using web storm at that time

@mateuszsokola
Copy link

I'm experiencing the same problem. Using Visual Studio Code and running code in console (npm test).

@LukeSideris
Copy link

LukeSideris commented Mar 21, 2018

Chiming in - I am having the same issue here and was able to get more information using wrapper.find('#target_id').debug()

Here's a simplified example:

<Component id="target_id" text="Foo">
  <span id="target_id">Foo</span>
</Component>

You can see that enzyme included the react pseudo-element in its markup, so that if id is the name of a prop it will match twice!

This was done with mount()

@ljharb
Copy link
Member

ljharb commented Mar 21, 2018

This is indeed correct behavior.

To filter out custom components, use .hostNodes() - ie, wrapper.find('#someID').hostNodes() and you'll only get HTML elements.

@ljharb ljharb closed this as completed Mar 21, 2018
@jstray
Copy link

jstray commented May 15, 2018

hostNodes() solves it -- but why is this necessary? Could at least be documented

@jsardev
Copy link

jsardev commented Jun 7, 2018

I've also encountered this issue and don't really understand the reason why it works like this now? Why do I need to run some hostNodes() function on my wrapper?

Mounting a single div with a className and querying this className with find returns me 2 elements now. IMO it's something completely opposite to being developer-intuitive.

For example this:

const Component = () => <div className="class-1" />;
const wrapper = mount(<Component className="class-1" />);
console.log(wrapper.find('.class-1').length); // returns 2

Why would I expect to have 2 elements here?

Could someone explain this behaviour to me? @ljharb? 😃

@ljharb
Copy link
Member

ljharb commented Jun 29, 2018

@sarneeh because in enzyme 3, the nodes you get are both React component instances, and DOM nodes. If you want to just have DOM nodes, you use .hostNodes(). You're getting 2 because one of them is Component, and the other is the div that Component renders.

@jsardev
Copy link

jsardev commented Jun 29, 2018

@ljharb Alright, I understand that. But why it has been decided that this behaviour should be the default one? Couldn't it be reversed, so by default I'll get the behaviour from Enzyme v2, and in v3 have an additional method to get all of them?

To be clear: I don't want to hate anyone nor anything, I just want to understand the decision behind it, because maybe I'm missing something, and maybe I could learn something too 😄

@ljharb
Copy link
Member

ljharb commented Jun 29, 2018

@sarneeh i believe this was chosen to match the way the react dev tools chrome extension works.

@cancerberoSgx

This comment has been minimized.

@ljharb

This comment has been minimized.

@cancerberoSgx

This comment has been minimized.

@asumaran
Copy link

> mountedComponent.find("#foo")
ReactWrapper {length: 2, Symbol(enzyme.__unrendered__): null, Symbol(enzyme.__renderer__): {…}, Symbol(enzyme.__root__): ReactWrapper, Symbol(enzyme.__node__): {…}, …}

> mountedComponent.find("input#foo")
ReactWrapper {length: 1, Symbol(enzyme.__unrendered__): null, Symbol(enzyme.__renderer__): {…}, Symbol(enzyme.__root__): ReactWrapper, Symbol(enzyme.__node__): {…}, …}

Got the same issue. For me worked adjusting a little bit the selector. Not sure why though. Also, .hostNodes() worked as well.

@ljharb
Copy link
Member

ljharb commented Apr 23, 2019

@asumaran because of #836 (comment)

@kumar303
Copy link

kumar303 commented Sep 16, 2019

In the following example, getting two results from getDOMNode() is surprising since only one DOM node exists:

function SomeWrapper({ as: AsComponent, id }) {
  // Imagine that this does useful things :) There is a case for it.
  return <AsComponent id={id} />;
}

mount(<SomeWrapper as="div" id="example-id" />)
  .find('#example-id')
  // This returns two items.
  .getDOMNode();

@ljharb
Copy link
Member

ljharb commented Sep 16, 2019

@kumar303 .find returns two items; getDOMNode returns the result for each found item. If you want to restrict to host nodes, use .hostNodes().getDOMNode() instead.

@kumar303
Copy link

Thanks, I did discover that from this thread (hooray). I was just providing a concrete example of how this is very surprising behavior. Maybe it could be aliased as getNode() or something.

@ljharb
Copy link
Member

ljharb commented Sep 17, 2019

Is "node" a DOM node, or a React tree node? Would a React Native user expect "node" to mean "DOM node", when a Host Node for RN is something like Text or Image?

@kumar303
Copy link

Is "node" a DOM node, or a React tree node?

In the case of getDOMNode(), it could be either. This is why I found it surprising. I expected it only to return DOM nodes.

@ljharb
Copy link
Member

ljharb commented Sep 18, 2019

Your comment is that getting two dom nodes is surprising - are you getting a non-dom node from that method?

@kumar303
Copy link

kumar303 commented Sep 18, 2019

Yes, the example in #836 (comment) gets a non-DOM node from getDOMNode(). The first node it gets is <SomeWrapper>, the second is <div />. I would only expect it to return <div /> since that's the only DOM node. This was all discussed by others earlier up in the thread but since the example component I showed is really only a wrapper, it's a good illustration of how this behavior is surprising.

@ljharb
Copy link
Member

ljharb commented Sep 23, 2019

Interesting. That does seem like a problem; getDOMNode should probably only return host nodes. A new issue might be warranted for that.

@kumar303
Copy link

A new issue might be warranted for that.

Sure: #2244

Thanks for looking into it.

@jasonsrogers
Copy link

Hi All
Ran across the issue recently with

import Button from "react-bootstrap/Button";
...
<Button className="foo-class"></Button>

and my test was failing

expect(converterWrapper.find(".foo-class").toHaveLength(1); // ==> returns 2

When debugging converterWrapper.find(".foo-class").debug()

<Button className="conversion-form__switch-currency conversion-form__switch-currency-GBP" onClick={[Function: onClick]} variant="primary" active={false} disabled={false} type="button">
      <button onClick={[Function: onClick]} disabled={false} type="button" className="foo-class btn btn-primary">
        To: 
        GBP
      </button>
    </Button>
    
    
    <button onClick={[Function: onClick]} disabled={false} type="button" className="foo-class btn btn-primary">
      To: 
      GBP
    </button>

So Enzyme generates Button and button which is why it returns 2 nodes

I haven't found a better solution than specifying Button
expect(converterWrapper.find("Button.foo-class").toHaveLength(1)

@ljharb
Copy link
Member

ljharb commented Nov 18, 2019

@jasonsrogers you can add .hostNodes() to filter it down to the HTML elements; separately, don't find by string when you can .find(Button).filter('.foo-class')

@lstickelhube
Copy link

what is that "evaluate expression" debugger that you are using? it seems cool :D

@TeresaP
Copy link

TeresaP commented May 17, 2021

@lstickelhube It looks like WebStorm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests