testing-library / react-testing-library

🐐 Simple and complete React DOM testing utilities that encourage good testing practices.
https://testing-library.com/react
MIT License
18.94k stars 1.1k forks source link

scrollHeight/clientHeight for an element is always zero #353

Closed GnanaAjana closed 5 years ago

GnanaAjana commented 5 years ago

In component js,

Based on scrollHeight am displaying footer or binding scroll event. So, in this case, scrollHeight is always 0.

<div id="inner-content"> // this is scrollable element
  <div ref={this.elementRef}></div>
</div>
if (this.elementRef.current) {
      const { document: iFrameDocument, window: iFrameWindow } = iFrameContent;
      const { scrollHeight, offsetLeft, offsetTop, offsetHeight } =
        iFrameDocument.getElementById('inner-content') || {};
      const compareHeight = offsetHeight + offsetLeft + offsetTop;
      if (scrollHeight <= compareHeight) {
        displayFooter();
      } else {
        iFrameWindow.addEventListener('scroll', this.onContentScroll, true);
      }
    }

In this case, due to clientHeight 0, I can't able to cover the scroll event test case.

Am not using 'enzyme' here.

Is there any other way to get clientHeight.

weyert commented 5 years ago

I don't think jsdom actually renders your component so the scroll height might not change even when dispatching the scroll event

GnanaAjana commented 5 years ago

@weyert I checked to print log for this.elementRef.current, its innerHTML has the content which i bind using dangerouslySetInnerHTML={createMarkup(testHtml)} but other height related is 0. ({height: 0, width: 0, offsetHeight: 0, clientHeight: 0, bottom: 0, top: 0 })

In above I missed this.

<div id="inner-content"> // this is scrollable element
  <div ref={this.elementRef} dangerouslySetInnerHTML={createMarkup(testHtml)}></div>
</div>
viniciusavieira commented 5 years ago

I'm note sure, but I think this is because maybe jsdom does not exports this as you may be expecting.

In some cases may be useful to mock this implementation with Jest. https://jestjs.io/docs/en/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom

Them you can export something like: window.mockSize = {height: 0, width: 0, scrollHeight: 0, offsetLeft: 0, offsetTop: 0, offsetHeight: 0 }

Them changes the mocked values to return expected values to trigger the behavior as your test requires.

Don't know if it is the best way, but usually mocking with jest solves this kind of problem to me.

weyert commented 5 years ago

For offsetHeight, scrollHeight to change when using jsdom, then this library would need to actually do layouting which it currently doesn't do so these values wouldn't change. I guess you would need to run the tests in the browser to test this or maybe mock the properties accordingly.

kentcdodds commented 5 years ago

jsdom doesn't support layout. This means measurements like this will always return 0 as it does here.

You have three options:

  1. Put the logic that uses those measurements in a function and test it in isolation.
  2. Mock the values.
  3. Use Cypress (+ cypress-testing-library) to test that particular situation

I typically recommend option 3. Good luck!

kmhigashioka commented 5 years ago

Do you have an example on how to mock these values?

alexkrolick commented 5 years ago

Mock the values = get a reference to an element and set the value of the attribute

Nothing to so with jest mocks or anything

viniciusavieira commented 5 years ago

I use like this, so I can mock the offsetHeight for example

const originalOffsetHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight')
const originalOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetWidth')

beforeAll(() => {
  Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 500 })
  Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 500 })
})

afterAll(() => {
  Object.defineProperty(HTMLElement.prototype, 'offsetHeight', originalOffsetHeight)
  Object.defineProperty(HTMLElement.prototype, 'offsetWidth', originalOffsetWidth)
})

describe('You can test with mocked values here', () => {
  //offsetHeight = 500
  //offsetWidth = 500
})
kentcdodds commented 5 years ago

I have this in one of my tests:

import matchMediaPolyfill from 'mq-polyfill'
// ...

beforeAll(() => {
  matchMediaPolyfill(window)
  window.resizeTo = function resizeTo(width, height) {
    Object.assign(this, {
      innerWidth: width,
      innerHeight: height,
      outerWidth: width,
      outerHeight: height,
    }).dispatchEvent(new this.Event('resize'))
  }
})

// ... then in my test window.resizeTo(800, 300)

I think if you're not going to use Cypress/a real browser, this is probably the recommended way to do this. We should probably add this to the examples: https://github.com/kentcdodds/react-testing-library-examples

nguyenvantruong17 commented 4 years ago

I use like this, so I can mock the offsetHeight for example

const originalOffsetHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight')
const originalOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetWidth')

beforeAll(() => {
  Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 500 })
  Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 500 })
})

afterAll(() => {
  Object.defineProperty(HTMLElement.prototype, 'offsetHeight', originalOffsetHeight)
  Object.defineProperty(HTMLElement.prototype, 'offsetWidth', originalOffsetWidth)
})

describe('You can test with mocked values here', () => {
  //offsetHeight = 500
  //offsetWidth = 500
})

This solution just assign for all element, but how to mock offset for each element? Please tell me if you have an idea for it. Thanks for your solution.

viniciusavieira commented 4 years ago

I use like this, so I can mock the offsetHeight for example

const originalOffsetHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight')
const originalOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetWidth')

beforeAll(() => {
  Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 500 })
  Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 500 })
})

afterAll(() => {
  Object.defineProperty(HTMLElement.prototype, 'offsetHeight', originalOffsetHeight)
  Object.defineProperty(HTMLElement.prototype, 'offsetWidth', originalOffsetWidth)
})

describe('You can test with mocked values here', () => {
  //offsetHeight = 500
  //offsetWidth = 500
})

This solution just assign for all element, but how to mock offset for each element? Please tell me if you have an idea for it. Thank your solution.

I think the same as you can use HTMLElement.prototype you may be able to use it in a html node using a query selector like document.getElementById(id) or you can use a dom-testing-library selector like getByText and insert this prop in the runtime. I did not test this but I think it should work fine.

nguyenvantruong17 commented 4 years ago

I use like this, so I can mock the offsetHeight for example

const originalOffsetHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight')
const originalOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetWidth')

beforeAll(() => {
  Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 500 })
  Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 500 })
})

afterAll(() => {
  Object.defineProperty(HTMLElement.prototype, 'offsetHeight', originalOffsetHeight)
  Object.defineProperty(HTMLElement.prototype, 'offsetWidth', originalOffsetWidth)
})

describe('You can test with mocked values here', () => {
  //offsetHeight = 500
  //offsetWidth = 500
})

This solution just assign for all element, but how to mock offset for each element? Please tell me if you have an idea for it. Thank your solution.

I think the same as you can use HTMLElement.prototype you may be able to use it in a html node using a query selector like document.getElementById(id) or you can use a dom-testing-library selector like getByText and insert this prop in the runtime. I did not test this but I think it should work fine.

Tks for reply. I've implemented const el: HTMLSpanElement = document.createElement('span'); Object.defineProperty(el, 'offsetTop', { configurable: true, value: index }); and it work.

agilgur5 commented 4 years ago

So I recently made a very tiny window-resizeto testing polyfill to help simplify resize tests. Seems to be one of the topics of this issue, so thought I might share here.

If you're using Jest, it's pretty easy to add:

jest.config.js

module.exports = {
  setupFilesAfterEnv: [
    // polyfill window.resizeTo
    'window-resizeto/polyfill'
  ]
}

some-test.spec.js

window.resizeTo(500, 500)
// window is now resize to 500x500

If you wanted to more selectively add it to only certain test suites, you could also do:

import 'window-resizeto/polyfill'

Or if you wanted to just use the ponyfill exactly where needed, you could do:

import { resizeTo } from 'window-resizeto'

// ...
test('something', () => {
  resizeTo(window, 500, 500);
  // window is now resized to 500x500
})

All of these use-cases are covered in the docs

izayl commented 4 years ago

jsdom doesn't support layout. This means measurements like this will always return 0 as it does here.

You have three options:

  1. Put the logic that uses those measurements in a function and test it in isolation.
  2. Mock the values.
  3. Use Cypress (+ cypress-testing-library) to test that particular situation

I typically recommend option 3. Good luck!

@kentcdodds I have run into the similar problem, I want to test a Component with limited style like min-width, but I found that the dom return by querySelector has a zero value with clientWidth and clientHeight property.

as you say jsdom doesn't support layout, so there is no possibility to acquire the dom width or height, right?

kentcdodds commented 4 years ago

Correct.

mjangir commented 4 years ago

@kentcdodds But what if my component appends some text in a div and calculates the clientHeight dynamically?

kentcdodds commented 4 years ago

Then you'll have to use something other than jsdom to test it. There's a testing library for Cypress. That's a good option.

moshfeu commented 3 years ago

In jest it's possible to spy getters like the following

jest
    .spyOn(element, 'clientHeight', 'get')
    .mockImplementation(() => height);

docs: https://jestjs.io/docs/en/jest-object#jestspyonobject-methodname-accesstype reference: https://stackoverflow.com/a/56457850/863110

Personally, I prefer spy (which can be restored afterEach) rather then override properties (which can't be restored afterEach, only set the value "back". Although, this probably what jest does under the hood..)

camilaibs commented 3 years ago

If someone is looking for a sinon solution, you can defines a new getter:

sinon.stub(element, 'clientHeight').get(() => height);
YonatanKra commented 3 years ago

A bit late but... things like this always make me go back to good old Karma ;)

Daavidaviid commented 1 year ago

I came up with this util :

/**
 * Example: properties can be:
 *
 * properties = {
 *   scrollWidth: 1000,
 * }
 */
export const defineHtmlRefProperties = (properties: Record<string, unknown>) => {
  const originalValues: Record<string, unknown> = {};

  Object.keys(properties).forEach(key => {
    originalValues[key] = Object.getOwnPropertyDescriptor(HTMLElement.prototype, key);
  });

  const setHTMLProperties = () => {
    Object.entries(properties).forEach(([key, value]) => {
      Object.defineProperty(HTMLElement.prototype, key, {
        configurable: true,
        value,
      });
    });
  };
  const unsetHTMLProperties = () => {
    Object.keys(properties).forEach(key => {
      if (originalValues[key]) {
        // @ts-ignore
        Object.defineProperty(HTMLElement.prototype, key, originalValues[key]);
      }
    });
  };

  return {
    setHTMLProperties,

    unsetHTMLProperties,
  };
};

And then you can use it like that :

const { setHTMLProperties, unsetHTMLProperties } = defineHtmlRefProperties({
  scrollWidth: 1000,
});

describe('Dummy', () => {
  beforeAll(() => {
    …
    setHTMLProperties();
  });

  afterAll(() => {
    …
    unsetHTMLProperties();
  });
dkilgore-eightfold commented 4 months ago

Unit Tests are the best guard against regressions. Using E2E testing is an option, but it's not ideal and requires compiler time and/or continuous integration. At the time a bug may be found there will be confusion as a lot of QA and infra teams are not in the same room so to speak. Not supporting common browser APIs is a miss here, and it's not really react-testing-library that's the root cause. I tried the above solutions and none work for my simple scenario.