diff options
Diffstat (limited to 'preact/test/_util/helpers.js')
-rw-r--r-- | preact/test/_util/helpers.js | 292 |
1 files changed, 292 insertions, 0 deletions
diff --git a/preact/test/_util/helpers.js b/preact/test/_util/helpers.js new file mode 100644 index 0000000..8f0cf02 --- /dev/null +++ b/preact/test/_util/helpers.js @@ -0,0 +1,292 @@ +import { createElement, options } from 'preact'; +import { clearLog, getLog } from './logCall'; +import { teardown as testUtilTeardown } from 'preact/test-utils'; + +/** @jsx createElement */ + +/** + * Assign properties from `props` to `obj` + * @template O, P The obj and props types + * @param {O} obj The object to copy properties to + * @param {P} props The object to copy properties from + * @returns {O & P} + */ +function assign(obj, props) { + for (let i in props) obj[i] = props[i]; + return /** @type {O & P} */ (obj); +} + +export function supportsPassiveEvents() { + let supported = false; + try { + let options = { + get passive() { + supported = true; + return undefined; + } + }; + + window.addEventListener('test', options, options); + window.removeEventListener('test', options, options); + } catch (err) { + supported = false; + } + return supported; +} + +export function supportsDataList() { + return ( + 'list' in document.createElement('input') && + Boolean( + document.createElement('datalist') && 'HTMLDataListElement' in window + ) + ); +} + +const VOID_ELEMENTS = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/; + +function encodeEntities(str) { + return str.replace(/&/g, '&'); +} + +/** + * Normalize svg paths spacing. Some browsers insert spaces around letters, + * others do not. + * @param {string} str + * @returns {string} + */ +function normalizePath(str) { + let len = str.length; + let out = ''; + for (let i = 0; i < len; i++) { + const char = str[i]; + if (/[A-Za-z]/.test(char)) { + if (i == 0) out += char + ' '; + else + out += (str[i - 1] == ' ' ? '' : ' ') + char + (i < len - 1 ? ' ' : ''); + } else if (char == '-' && str[i - 1] !== ' ') out += ' ' + char; + else out += char; + } + + return out.replace(/\s\s+/g, ' ').replace(/z/g, 'Z'); +} + +export function serializeHtml(node) { + let str = ''; + let child = node.firstChild; + while (child) { + str += serializeDomTree(child); + child = child.nextSibling; + } + return str; +} + +/** + * Serialize a DOM tree. + * Uses deterministic sorting where necessary to ensure consistent tests. + * @param {Element|Node} node The root node to serialize + * @returns {string} html + */ +function serializeDomTree(node) { + if (node.nodeType === 3) { + return encodeEntities(node.data); + } else if (node.nodeType === 8) { + return '<!--' + encodeEntities(node.data) + '-->'; + } else if (node.nodeType === 1 || node.nodeType === 9) { + let str = '<' + node.localName; + const attrs = []; + for (let i = 0; i < node.attributes.length; i++) { + attrs.push(node.attributes[i].name); + } + attrs.sort(); + for (let i = 0; i < attrs.length; i++) { + const name = attrs[i]; + let value = node.getAttribute(name); + + // don't render attributes with null or undefined values + if (value == null) continue; + + // normalize empty class attribute + if (!value && name === 'class') continue; + + str += ' ' + name; + value = encodeEntities(value); + + // normalize svg <path d="value"> + if (node.localName === 'path' && name === 'd') { + value = normalizePath(value); + } + str += '="' + value + '"'; + } + str += '>'; + + // For elements that don't have children (e.g. <wbr />) don't descend. + if (!VOID_ELEMENTS.test(node.localName)) { + // IE puts the value of a textarea as its children while other browsers don't. + // Normalize those differences by forcing textarea to not have children. + if (node.localName != 'textarea') { + let child = node.firstChild; + while (child) { + str += serializeDomTree(child); + child = child.nextSibling; + } + } + + str += '</' + node.localName + '>'; + } + return str; + } +} + +/** + * Normalize event creation in browsers + * @param {string} name + * @returns {Event} + */ +export function createEvent(name) { + // Modern browsers + if (typeof Event == 'function') { + return new Event(name); + } + + // IE 11... + let event = document.createEvent('Event'); + event.initEvent(name, true, true); + return event; +} + +/** + * Sort a cssText alphabetically. + * @param {string} cssText + */ +export function sortCss(cssText) { + return ( + cssText + .split(';') + .filter(Boolean) + .map(s => s.replace(/^\s+|\s+$/g, '').replace(/(\s*:\s*)/g, ': ')) + .sort((a, b) => { + // CSS Variables are typically positioned at the start + if (a[0] === '-') { + // If both are a variable we just compare them + if (b[0] === '-') return a.localeCompare(b); + return -1; + } + // b is a css var + if (b[0] === '-') return 1; + + return a.localeCompare(b); + }) + .join('; ') + ';' + ); +} + +/** + * Setup the test environment + * @returns {HTMLDivElement} + */ +export function setupScratch() { + const scratch = document.createElement('div'); + scratch.id = 'scratch'; + (document.body || document.documentElement).appendChild(scratch); + return scratch; +} + +let oldOptions = null; +export function clearOptions() { + oldOptions = assign({}, options); + delete options.vnode; + delete options.diffed; + delete options.unmount; + delete options._diff; +} + +/** + * Teardown test environment and reset preact's internal state + * @param {HTMLDivElement} scratch + */ +export function teardown(scratch) { + if (scratch) { + scratch.parentNode.removeChild(scratch); + } + + if (oldOptions != null) { + assign(options, oldOptions); + oldOptions = null; + } + + testUtilTeardown(); + + if (getLog().length > 0) { + clearLog(); + } + + restoreElementAttributes(); +} + +const Foo = () => 'd'; +export const getMixedArray = () => + // Make it a function so each test gets a new copy of the array + [0, 'a', 'b', <span>c</span>, <Foo />, null, undefined, false, ['e', 'f'], 1]; +export const mixedArrayHTML = '0ab<span>c</span>def1'; + +/** + * Reset obj to empty to keep reference + * @param {object} obj + */ +export function clear(obj) { + Object.keys(obj).forEach(key => delete obj[key]); +} + +/** + * Hacky normalization of attribute order across browsers. + * @param {string} html + */ +export function sortAttributes(html) { + return html.replace( + /<([a-z0-9-]+)((?:\s[a-z0-9:_.-]+=".*?")+)((?:\s*\/)?>)/gi, + (s, pre, attrs, after) => { + let list = attrs + .match(/\s[a-z0-9:_.-]+=".*?"/gi) + .sort((a, b) => (a > b ? 1 : -1)); + if (~after.indexOf('/')) after = '></' + pre + '>'; + return '<' + pre + list.join('') + after; + } + ); +} + +let attributesSpy, originalAttributesPropDescriptor; + +export function spyOnElementAttributes() { + const test = Object.getOwnPropertyDescriptor(Element.prototype, 'attributes'); + + // IE11 doesn't correctly restore the prototype methods so we have to check + // whether this prototype method is already a sinon spy. + if (!attributesSpy && !(test && test.get && test.get.isSinonProxy)) { + if (!originalAttributesPropDescriptor) { + originalAttributesPropDescriptor = Object.getOwnPropertyDescriptor( + Element.prototype, + 'attributes' + ); + } + + attributesSpy = sinon.spy(Element.prototype, 'attributes', ['get']); + } else if (test && test.get && test.get.isSinonProxy) { + // Due to IE11 not resetting we will do this manually when it is a proxy. + test.get.resetHistory(); + } + + return attributesSpy || test; +} + +function restoreElementAttributes() { + if (originalAttributesPropDescriptor) { + // Workaround bug in Sinon where getter/setter spies don't get auto-restored + Object.defineProperty( + Element.prototype, + 'attributes', + originalAttributesPropDescriptor + ); + attributesSpy = null; + } +} |