From a07774e81ac8a6e39cf9686fbd21cf64cff13934 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 12 Apr 2016 14:12:44 +0200 Subject: [PATCH] Implement tests for serialization ensure that complex DOM elements can serialize and deserialize without modifications RTWYSIWYG-54 > implement tests for components of the WYSIWYG editor --- www/assert/hyperjson.js | 124 ++++++++++++ www/assert/hyperscript.js | 400 ++++++++++++++++++++++++++++++++++++++ www/assert/index.html | 25 +++ www/assert/main.js | 50 +++++ 4 files changed, 599 insertions(+) create mode 100644 www/assert/hyperjson.js create mode 100644 www/assert/hyperscript.js create mode 100644 www/assert/index.html create mode 100644 www/assert/main.js diff --git a/www/assert/hyperjson.js b/www/assert/hyperjson.js new file mode 100644 index 000000000..90edb0616 --- /dev/null +++ b/www/assert/hyperjson.js @@ -0,0 +1,124 @@ +define([], function () { + // this makes recursing a lot simpler + var isArray = function (A) { + return Object.prototype.toString.call(A)==='[object Array]'; + }; + + var parseStyle = function(el){ + var style = el.style; + var output = {}; + for (var i = 0; i < style.length; ++i) { + var item = style.item(i); + output[item] = style[item]; + } + return output; + }; + + var callOnHyperJSON = function (hj, cb) { + var children; + + if (hj && hj[2]) { + children = hj[2].map(function (child) { + if (isArray(child)) { + // if the child is an array, recurse + return callOnHyperJSON(child, cb); + } else if (typeof (child) === 'string') { + // string nodes have leading and trailing quotes + return child.replace(/(^"|"$)/g,""); + } else { + // the above branches should cover all methods + // if we hit this, there is a problem + throw new Error(); + } + }); + } else { + children = []; + } + // this should return the top level element of your new DOM + return cb(hj[0], hj[1], children); + }; + + var classify = function (token) { + return '.' + token.trim(); + }; + + var isValidClass = function (x) { + if (x && /\S/.test(x)) { + return true; + } + }; + + var isTruthy = function (x) { + return x; + }; + + var DOM2HyperJSON = function(el, predicate, filter){ + if(!el.tagName && el.nodeType === Node.TEXT_NODE){ + return el.textContent; + } + if(!el.attributes){ + return; + } + if (predicate) { + if (!predicate(el)) { + // shortcircuit + return; + } + } + + var attributes = {}; + + var i = 0; + for(;i < el.attributes.length; i++){ + var attr = el.attributes[i]; + if(attr.name && attr.value){ + if(attr.name === "style"){ + attributes.style = parseStyle(el); + } + else{ + attributes[attr.name] = attr.value; + } + } + } + + // this should never be longer than three elements + var result = []; + + // get the element type, id, and classes of the element + // and push them to the result array + var sel = el.tagName; + + if(attributes.id){ + // we don't have to do much to validate IDs because the browser + // will only permit one id to exist + // unless we come across a strange browser in the wild + sel = sel +'#'+ attributes.id; + delete attributes.id; + } + result.push(sel); + + // second element of the array is the element attributes + result.push(attributes); + + // third element of the array is an array of child nodes + var children = []; + + // js hint complains if we use 'var' here + i = 0; + for(; i < el.childNodes.length; i++){ + children.push(DOM2HyperJSON(el.childNodes[i], predicate, filter)); + } + result.push(children.filter(isTruthy)); + + if (filter) { + return filter(result); + } else { + return result; + } + }; + + return { + fromDOM: DOM2HyperJSON, + callOn: callOnHyperJSON + }; +}); diff --git a/www/assert/hyperscript.js b/www/assert/hyperscript.js new file mode 100644 index 000000000..52282dcfc --- /dev/null +++ b/www/assert/hyperscript.js @@ -0,0 +1,400 @@ +define([], function () { + var Hyperscript; + +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o + * Available under the MIT License + * ECMAScript compliant, uniform cross-browser split method + */ + +/** + * Splits a string into an array of strings using a regex or string separator. Matches of the + * separator are not included in the result array. However, if `separator` is a regex that contains + * capturing groups, backreferences are spliced into the result each time `separator` is matched. + * Fixes browser bugs compared to the native `String.prototype.split` and can be used reliably + * cross-browser. + * @param {String} str String to split. + * @param {RegExp|String} separator Regex or string to use for separating the string. + * @param {Number} [limit] Maximum number of items to include in the result array. + * @returns {Array} Array of substrings. + * @example + * + * // Basic use + * split('a b c d', ' '); + * // -> ['a', 'b', 'c', 'd'] + * + * // With limit + * split('a b c d', ' ', 2); + * // -> ['a', 'b'] + * + * // Backreferences in result array + * split('..word1 word2..', /([a-z]+)(\d+)/i); + * // -> ['..', 'word', '1', ' ', 'word', '2', '..'] + */ +module.exports = (function split(undef) { + + var nativeSplit = String.prototype.split, + compliantExecNpcg = /()??/.exec("")[1] === undef, + // NPCG: nonparticipating capturing group + self; + + self = function(str, separator, limit) { + // If `separator` is not a regex, use `nativeSplit` + if (Object.prototype.toString.call(separator) !== "[object RegExp]") { + return nativeSplit.call(str, separator, limit); + } + var output = [], + flags = (separator.ignoreCase ? "i" : "") + (separator.multiline ? "m" : "") + (separator.extended ? "x" : "") + // Proposed for ES6 + (separator.sticky ? "y" : ""), + // Firefox 3+ + lastLastIndex = 0, + // Make `global` and avoid `lastIndex` issues by working with a copy + separator = new RegExp(separator.source, flags + "g"), + separator2, match, lastIndex, lastLength; + str += ""; // Type-convert + if (!compliantExecNpcg) { + // Doesn't need flags gy, but they don't hurt + separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags); + } + /* Values for `limit`, per the spec: + * If undefined: 4294967295 // Math.pow(2, 32) - 1 + * If 0, Infinity, or NaN: 0 + * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296; + * If negative number: 4294967296 - Math.floor(Math.abs(limit)) + * If other: Type-convert, then use the above rules + */ + limit = limit === undef ? -1 >>> 0 : // Math.pow(2, 32) - 1 + limit >>> 0; // ToUint32(limit) + while (match = separator.exec(str)) { + // `separator.lastIndex` is not reliable cross-browser + lastIndex = match.index + match[0].length; + if (lastIndex > lastLastIndex) { + output.push(str.slice(lastLastIndex, match.index)); + // Fix browsers whose `exec` methods don't consistently return `undefined` for + // nonparticipating capturing groups + if (!compliantExecNpcg && match.length > 1) { + match[0].replace(separator2, function() { + for (var i = 1; i < arguments.length - 2; i++) { + if (arguments[i] === undef) { + match[i] = undef; + } + } + }); + } + if (match.length > 1 && match.index < str.length) { + Array.prototype.push.apply(output, match.slice(1)); + } + lastLength = match[0].length; + lastLastIndex = lastIndex; + if (output.length >= limit) { + break; + } + } + if (separator.lastIndex === match.index) { + separator.lastIndex++; // Avoid an infinite loop + } + } + if (lastLastIndex === str.length) { + if (lastLength || !separator.test("")) { + output.push(""); + } + } else { + output.push(str.slice(lastLastIndex)); + } + return output.length > limit ? output.slice(0, limit) : output; + }; + + return self; +})(); + +},{}],3:[function(require,module,exports){ +// contains, add, remove, toggle +var indexof = require('indexof') + +module.exports = ClassList + +function ClassList(elem) { + var cl = elem.classList + + if (cl) { + return cl + } + + var classList = { + add: add + , remove: remove + , contains: contains + , toggle: toggle + , toString: $toString + , length: 0 + , item: item + } + + return classList + + function add(token) { + var list = getTokens() + if (indexof(list, token) > -1) { + return + } + list.push(token) + setTokens(list) + } + + function remove(token) { + var list = getTokens() + , index = indexof(list, token) + + if (index === -1) { + return + } + + list.splice(index, 1) + setTokens(list) + } + + function contains(token) { + return indexof(getTokens(), token) > -1 + } + + function toggle(token) { + if (contains(token)) { + remove(token) + return false + } else { + add(token) + return true + } + } + + function $toString() { + return elem.className + } + + function item(index) { + var tokens = getTokens() + return tokens[index] || null + } + + function getTokens() { + var className = elem.className + + return filter(className.split(" "), isTruthy) + } + + function setTokens(list) { + var length = list.length + + elem.className = list.join(" ") + classList.length = length + + for (var i = 0; i < list.length; i++) { + classList[i] = list[i] + } + + delete list[length] + } +} + +function filter (arr, fn) { + var ret = [] + for (var i = 0; i < arr.length; i++) { + if (fn(arr[i])) ret.push(arr[i]) + } + return ret +} + +function isTruthy(value) { + return !!value +} + +},{"indexof":4}],4:[function(require,module,exports){ + +var indexOf = [].indexOf; + +module.exports = function(arr, obj){ + if (indexOf) return arr.indexOf(obj); + for (var i = 0; i < arr.length; ++i) { + if (arr[i] === obj) return i; + } + return -1; +}; +},{}],5:[function(require,module,exports){ +var h = require("./index.js"); + +module.exports = h; + +/* +$(function () { + + var newDoc = h('p', + + h('ul', 'bang bang bang'.split(/\s/).map(function (word) { + return h('li', word); + })) + ); + $('body').html(newDoc.outerHTML); +}); + +*/ + +},{"./index.js":1}],6:[function(require,module,exports){ + +},{}]},{},[5]); + + return Hyperscript; +}); diff --git a/www/assert/index.html b/www/assert/index.html new file mode 100644 index 000000000..5feebf5ae --- /dev/null +++ b/www/assert/index.html @@ -0,0 +1,25 @@ + + + + + + + + + +

Serialization tests

+ +

Test 1

+

class strings

+ +

pewpewpew

+ +
+ +

Test 2

+

XWiki Macros

+ + +

Here is a macro

+ +
diff --git a/www/assert/main.js b/www/assert/main.js new file mode 100644 index 000000000..f8c4f7ce1 --- /dev/null +++ b/www/assert/main.js @@ -0,0 +1,50 @@ +define([ + '/bower_components/jquery/dist/jquery.min.js', + '/assert/hyperjson.js', // serializing classes as an attribute + '/assert/hyperscript.js', // using setAttribute + '/common/TextPatcher.js' +], function (jQuery, Hyperjson, Hyperscript, TextPatcher) { + var $ = window.jQuery; + window.Hyperjson = Hyperjson; + window.Hyperscript = Hyperscript; + window.TextPatcher = TextPatcher; + + var assertions = 0; + + var assert = function (test, msg) { + if (test()) { + assertions++; + } else { + throw new Error(msg || ''); + } + }; + + var $body = $('body'); + + var roundTrip = function (target) { + assert(function () { + var hjson = Hyperjson.fromDOM(target); + var cloned = Hyperjson.callOn(hjson, Hyperscript); + + var success = cloned.outerHTML === target.outerHTML; + + if (!success) { + window.DEBUG = { + error: "Expected equality between A and B", + A: target.outerHTML, + B: cloned.outerHTML, + target: target, + diff: TextPatcher.diff(target.outerHTML, cloned.outerHTML) + }; + console.log(JSON.stringify(window.DEBUG, null, 2)); + } + + return success; + }, "Round trip serialization introduced artifacts."); + }; + + roundTrip($('#target')[0]); + roundTrip($('#widget')[0]); + + console.log("%s test%s passed", assertions, assertions === 1? '':'s'); +});