summaryrefslogtreecommitdiff
path: root/preact/test/_util/helpers.js
diff options
context:
space:
mode:
Diffstat (limited to 'preact/test/_util/helpers.js')
-rw-r--r--preact/test/_util/helpers.js292
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;
+ }
+}