enzyme icon indicating copy to clipboard operation
enzyme copied to clipboard

can't find() element but wrapper.contains() it

Open pixelass opened this issue 6 years ago • 20 comments

Current behavior

I cannot find(Component|".selector") but I can see it via html() and via .contains()

Here are the most important parts of code. (I am using ava, t.log is the test logger)

I am using a styled component here but it is the same when I add a custom className, id or data-attribute to attempt finding the element/Component.

code

const RetryButtonId = RetryButton.styledComponentId;
const retrySelector = `.${RetryButtonId}`;

t.log(retrySelector);
const retry = wrapper.find(RetryButton);
const _retry = wrapper.find(retrySelector);
const html = wrapper.html();
const match = html.match(new RegExp(`<button class="${RetryButtonId}.*?">Retry</button>`));
t.log(`match: ${match && match[0]}`);
t.log(`length "${retrySelector}": ${_retry.length}`);
t.log(`length "RetryButton": ${retry.length}`);
t.log(`exists "${retrySelector}": ${wrapper.exists(retrySelector)}`);
t.log(`contains "RetryButton": ${wrapper.contains(RetryButton)}`);

The logs show inconsistencies. While contains() returns true, exists() returns false.

The html() matches the element but find() and exists() ignore it.

Logs

ℹ .sc-jTzLTM
ℹ match: <button class="sc-jTzLTM dlKhUw sc-bdVaJa kgpquR">Retry</button>
ℹ length ".sc-jTzLTM": 0
ℹ length "RetryButton": 0
ℹ exists ".sc-jTzLTM": false
ℹ contains "RetryButton": true

Expected behavior

contains(), find(), exists(), html() should return matching results.

Your environment

OS X 10.13.6

node: v10.10.0 npm: 6.4.1 yarn 1.12.3

API

  • [ ] shallow
  • [x] mount
  • [ ] render

Version

library version
enzyme 3.8.0
react 16.7.0
react-dom 16.7.0
react-test-renderer
adapter (below)

Adapter

  • [x] enzyme-adapter-react-16
  • [ ] enzyme-adapter-react-16.3
  • [ ] enzyme-adapter-react-16.2
  • [ ] enzyme-adapter-react-16.1
  • [ ] enzyme-adapter-react-15
  • [ ] enzyme-adapter-react-15.4
  • [ ] enzyme-adapter-react-14
  • [ ] enzyme-adapter-react-13
  • [ ] enzyme-adapter-react-helper
  • [ ] others ( )

pixelass avatar Dec 22 '18 12:12 pixelass

I get the expected results if I call wrapper.update() beforehand but this shouldn't be needed, right?

pixelass avatar Dec 22 '18 12:12 pixelass

What does .debug() print out prior to the update? (.html() should be ignored)

ljharb avatar Dec 22 '18 18:12 ljharb

I’ll note that styled components tend to have some quirks which make working with them difficult. Could you provide the full component and test code?

ljharb avatar Dec 22 '18 18:12 ljharb

Sorry I can't share the code since it's private. I'd have to set up a MVP for reproduction (probably won't have time).

Inspecting the debug tree is pretty hard. I tried it and wasn't very successful since the output is too cluttered (nested & extended components.). I gave up after a while and don't think I will try this method again in the near future.

I think I did notice that the "conditional section" wasn't in the tree. I'm not fully sure (cluttered output) and I moved on to try other methods.

My main concern was, that contains and html return absolutely different results than exists and find. This should simply not be an issue. If you suggest not to use html then this should be noted or be removed. What use is a method if it returns the wrong output? especially if several methods aside have conflicting output.

pixelass avatar Dec 23 '18 04:12 pixelass

I’ll note that styled components tend to have some quirks which make working with them difficult. Could you provide the full component and test code?

As mentioned I' might not have time to create an MVP. This is the best I can do to explain those componennts.

const Button = styled.button`
  background: red;
`

const MyButton = styled(Button)`
  background: red;
`

class App extends Component (
  state = {
  	there: false
  }
  toggle() {
  	this.setState(prevState => ({foo: !prevState.there})); 
  }
  <ThemeProvider theme={myTheme}>
	<React.Fragment>
      <Button onClick={this.toggle}>Toggle</Button>
      {this.state.there && <MyButton onClick={this.toggle}>Sometimes I'm here</Button>}
    </React.Fragment>
  </ThemeProvider>
)
const Wrapper = mount <App/>
const button = wrapper.find(Button).at(0);

// >> FAIL
// The next operation fails ("something about not present length... expected 1 but received 0")
// button.simulate(click, {target: {value: "Mock event"}});
// const myButton = wrapper.find(Button).at(1); // ReactWrapper[] 
// myButton.simulate(click, {target: {value: "I'm conditional"}});

// >> FAIL
// The next operation fails ("something about not present length... expected 1 but received 0")
// wrapper.setState({there: true}, () => {
//   EVEN INSIDE THE CALLBACK
//   const myButton = wrapper.find(Button).at(1); // ReactWrapper[] 
//   myButton.simulate(click, {target: {value: "I'm conditional"}});
//});

// >> WINNER
wrapper.update()
// The next operation works as expected.
myButton.simulate(click, {target: {value: "I'm conditional"}});

I'm sorry it's late at night and This is the best I can do right now. (probably ever)

pixelass avatar Dec 23 '18 04:12 pixelass

The purpose of .html is that it uses the render API, and produces HTML output. I agree that it should probably be removed.

contains, exists, and find should definitely agree - although it's worth noting as well that simulate doesn't actually simulate anything, it's just sugar for invoking a prop function - so you might indeed need a wrapper.update() before things are consistent.

ljharb avatar Dec 23 '18 04:12 ljharb

Thank you for your time. I understand how simulate works. rendering is handled async from setting a state in react. I think this is why the issue occurs. I didn't see this in any documentation (which IMHO is too vague anyways). I tried several methods and was in some cases able to get the correct result after a await Promise.resolve() or (await new Promise(r => setTimeout(r, n)) (which greatly helped me understand the issue).

I think better examples considering this topic would help. It seems to be a very common use case.

pixelass avatar Dec 23 '18 04:12 pixelass

I’d be happy to accept a PR that made the docs more clear.

ljharb avatar Dec 23 '18 05:12 ljharb

If I ever find the time... I am currently writing docs for another big library. (enquirer) so right now I'm rather busy.

I enjoy writing docs (mostly because I value them so much) so I might get back to this, ... no promises.

I usually write easy to follow guides alongside documentation. I might suggest a format (in January). I've had good responses to that format.

pixelass avatar Dec 23 '18 05:12 pixelass

I suppose I hit a similar issue, but perhaps due to different reason as wrapper.update() makes no difference:

it.each(['foo', 'bar'])(
 'lorem ipsum',
 (value) => {
    // Logs 1
    console.log(wrapper.find({ value }).length);
    // Shows the nodes with matching `value`
    // <mockConstructor>
    // <WithStyles(MyElem) checked={true} value="foo" />
    // <WithStyles(MyElem) checked={true} value="bar" />
    // </mockConstructor>
    console.log(wrapper.debug());
    // Fails with: Method “props” is meant to be run on 1 node. 0 found instead.
    expect(wrapper.find({ value }).props()).toMatchObject({ checked: true }); 
  }
);

Any suggestions?

maciej-gurban avatar Jul 09 '19 09:07 maciej-gurban

@maciej-gurban hm, that's strange. Could you file a new issue, ideally with a repro repo?

ljharb avatar Jul 09 '19 20:07 ljharb

I am on a similar issue here. the component which is to render conditionally isn't showing up when using .find() but shows up on output of .contains()

// passes
loginWrapper.contains('input[placeholder="NewPassword');
// .props() meant to be run on 1 node , 0 found instead
loginWrapper.find('input[placeholder="New Password"]').props(); 

shaleenmundra avatar Apr 22 '20 07:04 shaleenmundra

@shaleenmundra what's loginWrapper.debug() say?

ljharb avatar Apr 22 '20 07:04 ljharb

it doesn't return the "New Password" input component which .html() does .debug() is in sync with .find()

shaleenmundra avatar Apr 22 '20 09:04 shaleenmundra

Any response on this thread?

toro705 avatar Sep 23 '21 14:09 toro705

@shaleenmundra it would still help if you could provide the literal output of .debug().

@toro705 if you have a similar issue, it'd be great if you could provide component code, test code, and wrapper.debug() output.

ljharb avatar Sep 23 '21 17:09 ljharb

Got the same issue ("react": "^17.0.2", "jest": "^27.0.6","enzyme": "^3.11.0")

it('should not accept file that is too small', async () => {
    const file = new File([''], { type: 'image/png' });
    Object.defineProperty(file, 'name', { value: 'file_too_small.jpg' });
    Object.defineProperty(file, 'size', { value: 1571 });

    await act(async () => {
      await wrapper
        .find('input')
        .props()
        .onChange({ target: { files: [file] } });
      jest.advanceTimersByTime(10);
    });

    wrapper.update();

    expect(wrapper.find('.file-name').text()).toBe('Please upload your image');
    expect(wrapper.find('.file-clear-button').hasClass('hidden')).toBe(true);
    expect(wrapper.find('.file-upload-icon').exists()).toBe(true);
    expect(wrapper.find('.file-wrapper').hasClass('invalid')).toBe(true);
  });

Last expect does not work without jest.advanceTimersByTime(10) and wrapper.update() - element cannot be found even when it's html contains that class.

Inveth avatar Sep 29 '21 10:09 Inveth

@Inveth what's wrapper.debug() look like after the update call?

ljharb avatar Sep 29 '21 22:09 ljharb

@ljharb It has invalid class.

I did something else - I removed wrapper.update() (leavingadvanceTimersByTime) and wrapper.html() does have that invalid class while wrapper.debug() does not.

  it('should not accept file that is too small', async () => {
    const file = new File([''], { type: 'image/png' });
    Object.defineProperty(file, 'name', { value: 'file_too_small.jpg' });
    Object.defineProperty(file, 'size', { value: 1571 });

    console.log(wrapper.html()); // does not have invalid class
    console.log(wrapper.debug()); // does not have invalid class

    await act(async () => {
      await wrapper
        .find('input')
        .props()
        .onChange({ target: { files: [file] } });

      jest.advanceTimersByTime(10);

      console.log(wrapper.html()); // has invalid class
      console.log(wrapper.debug()); // does not have invalid class
    });

    console.log(wrapper.html()); // has invalid class
    console.log(wrapper.debug()); // does not have invalid class

    expect(wrapper.find('.file-name').text()).toBe('Please upload your image');
    expect(wrapper.find('.file-clear-button').hasClass('hidden')).toBe(true);
    expect(wrapper.find('.file-upload-icon').exists()).toBe(true);
    expect(wrapper.find('.file-wrapper').hasClass('invalid')).toBe(true);
  });

Inveth avatar Sep 30 '21 08:09 Inveth

What happens if you add a wrapper.update() after the act call?

ljharb avatar Oct 13 '21 19:10 ljharb