mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-09-09 10:09:49 -04:00
Merge pull request #3079 from alphagov/add-js-tests-for-sticky-js
Add js tests for sticky js
This commit is contained in:
@@ -710,21 +710,36 @@
|
|||||||
this.recalculate();
|
this.recalculate();
|
||||||
};
|
};
|
||||||
Sticky.prototype.setEvents = function () {
|
Sticky.prototype.setEvents = function () {
|
||||||
var self = this;
|
this._scrollEvent = this.onScroll.bind(this);
|
||||||
|
this._resizeEvent = this.onResize.bind(this);
|
||||||
|
|
||||||
// flag when scrolling takes place and check (and re-position) sticky elements relative to
|
// flag when scrolling takes place and check (and re-position) sticky elements relative to
|
||||||
// window position
|
// window position
|
||||||
if (self._scrollTimeout === false) {
|
if (this._scrollTimeout === false) {
|
||||||
$(global).scroll(function (e) { self.onScroll(); });
|
$(global).scroll(this._scrollEvent);
|
||||||
self._scrollTimeout = global.setInterval(function (e) { self.checkScroll(); }, 50);
|
this._scrollTimeout = global.setInterval(this.checkScroll.bind(this), 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recalculate all dimensions when the window resizes
|
// Recalculate all dimensions when the window resizes
|
||||||
if (self._resizeTimeout === false) {
|
if (this._resizeTimeout === false) {
|
||||||
$(global).resize(function (e) { self.onResize(); });
|
$(global).resize(this._resizeEvent);
|
||||||
self._resizeTimeout = global.setInterval(function (e) { self.checkResize(); }, 50);
|
this._resizeTimeout = global.setInterval(this.checkResize.bind(this), 50);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Sticky.prototype.clearEvents = function () {
|
||||||
|
if (this._scrollTimeout !== false) {
|
||||||
|
$(global).off('scroll', this._scrollEvent);
|
||||||
|
global.clearInterval(this._scrollTimeout);
|
||||||
|
this._scrollTimeout = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._resizeTimeout !== false) {
|
||||||
|
$(global).off('resize', this._resizeEvent);
|
||||||
|
global.clearInterval(this._resizeTimeout);
|
||||||
|
this._resizeTimeout = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
Sticky.prototype.viewportIsWideEnough = function (windowWidth) {
|
Sticky.prototype.viewportIsWideEnough = function (windowWidth) {
|
||||||
return windowWidth > 768;
|
return windowWidth > 768;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ describe('FullscreenTable', () => {
|
|||||||
test("when the page has scrolled", () => {
|
test("when the page has scrolled", () => {
|
||||||
|
|
||||||
// scroll the window so the table fills the height of the window (768px)
|
// scroll the window so the table fills the height of the window (768px)
|
||||||
windowMock.scrollBy(500);
|
windowMock.scrollTo(500);
|
||||||
|
|
||||||
// the frames should crop to the window height
|
// the frames should crop to the window height
|
||||||
expect(window.getComputedStyle(tableFrame)['height']).toEqual('768px');
|
expect(window.getComputedStyle(tableFrame)['height']).toEqual('768px');
|
||||||
|
|||||||
1336
tests/javascripts/stick-to-window-when-scrolling.test.js
Normal file
1336
tests/javascripts/stick-to-window-when-scrolling.test.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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 triggerEvent = (el, evtType, options) => {
|
||||||
const eventInit = {
|
const eventInit = {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
@@ -69,6 +86,39 @@ const triggerEvent = (el, evtType, options) => {
|
|||||||
el.dispatchEvent(evt);
|
el.dispatchEvent(evt);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getRadios (fields) {
|
||||||
|
const result = '';
|
||||||
|
|
||||||
|
return fields.map((field, idx) => {
|
||||||
|
const count = idx + 1;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="multiple-choice">
|
||||||
|
<input id="choose-${field.name}-1" name="choose-${field.name}-1" type="radio" value="${field.value}" ${field.checked ? 'checked' : ''}>
|
||||||
|
<label class="block-label" for="choose-${field.name}-1">
|
||||||
|
${field.label}
|
||||||
|
</label>
|
||||||
|
</div>`;
|
||||||
|
}).join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
function getRadioGroup (data) {
|
||||||
|
let radioGroup = document.createElement('div');
|
||||||
|
|
||||||
|
data.cssClasses.forEach(cssClass => radioGroup.classList.add(cssClass));
|
||||||
|
radioGroup.innerHTML = `
|
||||||
|
<div class="form-group ">
|
||||||
|
<fieldset id="choose-${data.name}">
|
||||||
|
<legend class="form-label">
|
||||||
|
Choose ${data.label}
|
||||||
|
</legend>
|
||||||
|
${getRadios(data.fields)}
|
||||||
|
</fieldset>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
return radioGroup;
|
||||||
|
};
|
||||||
|
|
||||||
function clickElementWithMouse (el) {
|
function clickElementWithMouse (el) {
|
||||||
triggerEvent(el, 'mousedown');
|
triggerEvent(el, 'mousedown');
|
||||||
triggerEvent(el, 'mouseup');
|
triggerEvent(el, 'mouseup');
|
||||||
@@ -173,27 +223,77 @@ class WindowMock {
|
|||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
width: window.innerWidth
|
width: window.innerWidth
|
||||||
};
|
};
|
||||||
this._spies = {
|
this.spies = {
|
||||||
document: {}
|
document: {},
|
||||||
|
window: {}
|
||||||
};
|
};
|
||||||
this._jest = jest;
|
this._jest = jest;
|
||||||
|
this._setSpies();
|
||||||
|
this._plugJSDOM();
|
||||||
|
}
|
||||||
|
|
||||||
|
get top () {
|
||||||
|
return window.scrollY;
|
||||||
|
}
|
||||||
|
|
||||||
|
get bottom () {
|
||||||
|
return window.scrollY + window.innerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
get height () {
|
||||||
|
return window.innerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
get width () {
|
||||||
|
return window.innerWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
get scrollPosition () {
|
||||||
|
return window.scrollY;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setSpies () {
|
||||||
|
|
||||||
|
// remove calls to document.documentElement.clientHeight when jQuery is gone. It's called to support older browsers like IE8
|
||||||
|
this.spies.document.clientHeight = this._jest.spyOn(document.documentElement, 'clientHeight', 'get').mockImplementation(() => window.innerHeight);
|
||||||
|
|
||||||
|
// remove calls to document.documentElement.clientWidth when jQuery is gone. It's called to support older browsers like IE8
|
||||||
|
this.spies.document.clientWidth = this._jest.spyOn(document.documentElement, 'clientWidth', 'get').mockImplementation(() => window.innerWidth);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_plugJSDOM () {
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
// JSDOM doesn't support .scrollTo
|
||||||
|
this.spies.window.scrollTo = this._jest.fn(function () {
|
||||||
|
let y;
|
||||||
|
|
||||||
|
// data sent as props in an object
|
||||||
|
if (arguments.length === 1) {
|
||||||
|
y = arguments[0].y;
|
||||||
|
} else {
|
||||||
|
y = arguments[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
self.scrollTo(y);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
window.scrollTo = this.spies.window.scrollTo;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setHeightTo (height) {
|
setHeightTo (height) {
|
||||||
|
|
||||||
// mock DOM calls for window height
|
|
||||||
window.innerHeight = height;
|
window.innerHeight = height;
|
||||||
// remove calls to document.documentElement.clientHeight when jQuery is gone. It's called to support older browsers like IE8
|
|
||||||
this._spies.document.clientHeight = this._jest.spyOn(document.documentElement, 'clientHeight', 'get').mockImplementation(() => height);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setWidthTo (width) {
|
setWidthTo (width) {
|
||||||
|
|
||||||
// mock DOM calls for window width
|
|
||||||
window.innerWidth = width;
|
window.innerWidth = width;
|
||||||
// remove calls to document.documentElement.clientWidth when jQuery is gone. It's called to support older browsers like IE8
|
|
||||||
this._spies.document.clientWidth = this._jest.spyOn(document.documentElement, 'clientWidth', 'get').mockImplementation(() => height);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,9 +305,12 @@ class WindowMock {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollBy (scrollPosition) {
|
scrollTo (scrollPosition) {
|
||||||
|
|
||||||
document.documentElement.scrollTop = scrollPosition;
|
document.documentElement.scrollTop = scrollPosition;
|
||||||
|
window.scrollY = scrollPosition;
|
||||||
|
window.pageYOffset = scrollPosition;
|
||||||
|
|
||||||
triggerEvent(window, 'scroll');
|
triggerEvent(window, 'scroll');
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -216,11 +319,11 @@ class WindowMock {
|
|||||||
|
|
||||||
window.innerHeight = this._defaults.height;
|
window.innerHeight = this._defaults.height;
|
||||||
window.innerWidth = this._defaults.width;
|
window.innerWidth = this._defaults.width;
|
||||||
document.documentElement.scrollTop = 0;
|
this.scrollTo(0);
|
||||||
|
|
||||||
// reset all spies
|
// reset all spies
|
||||||
Object.keys(this._spies).forEach(key => {
|
Object.keys(this.spies).forEach(key => {
|
||||||
const objectSpies = this._spies[key];
|
const objectSpies = this.spies[key];
|
||||||
Object.keys(objectSpies).forEach(key => objectSpies[key].mockClear());
|
Object.keys(objectSpies).forEach(key => objectSpies[key].mockClear());
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -270,6 +373,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
|
// function to ask certain questions of a DOM Element
|
||||||
const element = function (el) {
|
const element = function (el) {
|
||||||
@@ -282,5 +541,8 @@ exports.moveSelectionToRadio = moveSelectionToRadio;
|
|||||||
exports.activateRadioWithSpace = activateRadioWithSpace;
|
exports.activateRadioWithSpace = activateRadioWithSpace;
|
||||||
exports.RangeMock = RangeMock;
|
exports.RangeMock = RangeMock;
|
||||||
exports.SelectionMock = SelectionMock;
|
exports.SelectionMock = SelectionMock;
|
||||||
|
exports.getRadioGroup = getRadioGroup;
|
||||||
|
exports.getRadios = getRadios;
|
||||||
exports.element = element;
|
exports.element = element;
|
||||||
exports.WindowMock = WindowMock;
|
exports.WindowMock = WindowMock;
|
||||||
|
exports.ScreenMock = ScreenMock;
|
||||||
|
|||||||
Reference in New Issue
Block a user