From 9ef093cfdabfc2ae7c6d3b1618fdd4e157fd38f8 Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Fri, 19 Jul 2019 11:54:12 +0100 Subject: [PATCH] Add ScreenMock to helpers Mocks DOM API calls for position and dimension of elements and provides an API to allow access to them. --- tests/javascripts/support/helpers.js | 175 +++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/tests/javascripts/support/helpers.js b/tests/javascripts/support/helpers.js index 10a4445b0..ed43d21a2 100644 --- a/tests/javascripts/support/helpers.js +++ b/tests/javascripts/support/helpers.js @@ -1,3 +1,20 @@ +function getDescriptorForProperty (prop, obj) { + const descriptors = Object.getOwnPropertyDescriptors(obj); + const prototype = Object.getPrototypeOf(obj); + + if ((descriptors !== {}) && (prop in descriptors)) { + return descriptors[prop]; + } + + // if not in this object's descriptors, check the prototype chain + if (prototype !== null) { + return getDescriptorForProperty(prop, prototype); + } + + // no descriptor for this prop and no prototypes left in the chain + return null; +}; + const triggerEvent = (el, evtType, options) => { const eventInit = { bubbles: true, @@ -235,6 +252,7 @@ class WindowMock { document.documentElement.scrollTop = scrollPosition; window.scrollY = scrollPosition; window.pageYOffset = scrollPosition; + triggerEvent(window, 'scroll'); } @@ -297,6 +315,162 @@ class SelectionMock extends DOMInterfaceMock { } } +class ScreenRenderItem { + constructor (jest, node) { + + this._jest = jest; + this._node = node; + this._storeProps(); + this._mockAPICalls(); + + } + + setData (itemData) { + + // check all the item data is present + const itemProps = Object.keys(itemData); + const missingKeys = ScreenRenderItem.REQUIRED_PROPS.filter(prop => !itemProps.includes(prop)); + + this._data = {}; + + if (missingKeys.length) { + throw Error(`${itemData.name ? itemData.name : itemProps.join(', ')} is missing these properties: ${missingKeys.join(', ')}`); + } + + // default left if not set + if (!('offsetLeft' in itemData)) { itemData.offsetLeft = 0; } + + // copy onto internal store + Object.assign(this._data, itemData); + + } + + _getBoundingClientRect () { + const {offsetHeight, offsetWidth, offsetTop, offsetLeft} = this._data; + const x = offsetLeft - window.scrollX; + const y = offsetTop - window.scrollY; + + return { + 'x': x, + 'y': y, + 'top': (offsetHeight < 0) ? y + offsetHeight : y, + 'left': (offsetWidth < 0) ? x + offsetWidth : x, + 'bottom': (offsetTop + offsetHeight) - window.scrollY, + 'right': (offsetLeft + offsetWidth) - window.scrollX, + }; + + } + + reset () { + + // reset DOMRect mock + this._node.getBoundingClientRect.mockClear(); + + ScreenRenderItem.OFFSET_PROPS.forEach(prop => { + + if (prop in this._propStore) { + // replace property implementation + Object.defineProperty(this._node, prop, this._propStore[prop]); + } + + }); + + } + + _storeProps () { + + this._propStore = {}; + + ScreenRenderItem.OFFSET_PROPS.forEach(prop => { + const descriptor = getDescriptorForProperty(prop, this._node); + + if (descriptor !== null) { + this._propStore[prop] = descriptor; + } + }); + + } + + // mock any calls to the node's DOM API for position/dimension + _mockAPICalls () { + + // proxy boundingClientRect property calls to item data + this._jest.spyOn(this._node, 'getBoundingClientRect').mockImplementation(() => this._getBoundingClientRect()); + + // handle calls to offset properties + ScreenRenderItem.OFFSET_PROPS.forEach(prop => { + + this._jest.spyOn(this._node, prop, 'get').mockImplementation(() => this._data[prop]); + + // proxy DOM API sets for offsetValues (not possible to mock directly) + Object.defineProperty(this._node, prop, { + configurable: true, + set: jest.fn(value => { + this._data[prop] = value; + return true; + }) + }); + + }); + + } + +} +ScreenRenderItem.OFFSET_PROPS = ['offsetHeight', 'offsetWidth', 'offsetTop', 'offsetLeft']; +ScreenRenderItem.REQUIRED_PROPS = ['name', 'offsetHeight', 'offsetHeight', 'offsetWidth', 'offsetTop']; + +class ScreenMock { + constructor (jest) { + + this._jest = jest + this._items = {}; + + } + + mockPositionAndDimension (itemName, node, itemData) { + + if (itemName in this._items) { throw new Error(`${itemName} already has its position and dimension mocked`); } + + const data = Object.assign({ 'name': itemName }, itemData); + const item = new ScreenRenderItem(this._jest, node); + + item.setData(data); + + this._items[itemName] = item; + + } + + setWindow (windowData) { + + this.window = new WindowMock(this._jest); + + // check all the window data is present + const missingKeys = Object.keys(windowData).filter(key => !ScreenMock.REQUIRED_WINDOW_PROPS.includes(key)); + + if (missingKeys.length) { + throw Error(`Window definition is missing these properties: ${missingKeys.join(', ')}`); + } + + this.window.setHeightTo(windowData.height); + this.window.setWidthTo(windowData.width); + this.window.scrollTo(windowData.scrollTop); + + } + + scrollTo (scrollTop) { + + this.window.scrollTo(scrollTop); + + } + + reset () { + + Object.keys(this._items).forEach(itemName => this._items[itemName].reset()); + + } + +} +ScreenMock.REQUIRED_WINDOW_PROPS = ['height', 'width', 'scrollTop']; // function to ask certain questions of a DOM Element const element = function (el) { @@ -311,3 +485,4 @@ exports.RangeMock = RangeMock; exports.SelectionMock = SelectionMock; exports.element = element; exports.WindowMock = WindowMock; +exports.ScreenMock = ScreenMock;