├── .gitignore ├── .travis.yml ├── README.md ├── doc └── doc.md ├── gulpfile.js ├── index.html ├── package.json ├── src ├── module │ ├── event.module.js │ └── test.js ├── van.js └── van2.0.0.js └── unit_test ├── index.html ├── index1.0.0.html ├── qunit ├── qunit.css └── qunit.js └── test ├── testData.js ├── van.each.test.js ├── van.extend.test.js ├── van.map.test.js └── van.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_module 3 | .DS_Store 4 | dest 5 | bower_components 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.2.3" 4 | before_script: 5 | - npm install gulp -g 6 | install: npm install -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## van.js 2 | [![Build Status](https://travis-ci.org/Jiavan/vanjs.svg?branch=master)](https://travis-ci.org/Jiavan/vanjs) 3 | 4 | vanjs2 is coding... 5 | -------------------------------------------------------------------------------- /doc/doc.md: -------------------------------------------------------------------------------- 1 | # 正则 2 | ## 量词符 3 | ``` 4 | - ? 表示某个模式出现0次或者1次,等于{0,1} 5 | - * 表示某个模式出现0次或者多次,等于{0,} 6 | - + 表示某个模式出现1次或者多次,等于{1,} 7 | ``` 8 | 9 | # 页面加载 10 | ```javascript 11 | DOM文档加载的步骤为 12 | 13 | 0. 解析HTML结构。 14 | 1. 加载外部脚本和样式表文件。 15 | 2. 解析并执行脚本代码。 16 | 3. DOM树构建完成。//DOMContentLoaded 17 | 4. 加载图片等外部文件。 18 | 5. 页面加载完毕。//load 19 | 在第4步,会触发DOMContentLoaded事件。在第6步,触发load事件。 20 | ``` 21 | 22 | # CSS部分 23 | ## getComputedStyle 24 | element.style获得的是应用在元素style属性中的样式(不包括css样式的属性)而getComputedStyle获得的属性是元素对象上所有的属性,即使没有为元素设置任何样式,仍然会有。 25 | ```javascript 26 | // getComputedStyle IE 6~8是不支持的,currentStyle为ie属性 27 | // 通过currentStyle或者getComputedStyle获取全部css属性 28 | // getComputedStyle只读不能写,getPropertyValue获得属性值 29 | ``` 30 | 31 | ## el.style.cssText 32 | 这个属性有点类似于el.innerText,可以批量的设置css样式 33 | ```javascript 34 | el.style.cssText = 'width: 200px; color: red'; 35 | ``` 36 | 注意,这样的用法会覆盖掉之前元素的样式,所以一般采用追加的方式。 37 | 38 | 39 | 40 | # DOM操作 41 | ## 性能问题 42 | ```javascript 43 | // 操作DOM,相对于innerHTML,该方法不会重新解析调用该方法的元素 44 | // 因此不会影响到已经存在的元素解析,避免额外的解析操作 45 | // beforebegin 在 element 元素的前面 46 | // afterbegin 在 element 元素的第一个子节点前面 47 | // beforeend 在 element 元素的最后一个子节点后面 48 | // afterend 在 element 元素的后面 49 | // text 是字符串,会被解析成 HTML 或 XML,并插入到 DOM 树中 50 | appendDOM: function (el, position, str) { 51 | el.insertAdjacentHTML(position, str); 52 | } 53 | ``` 54 | ---- 55 | 56 | # 问题 57 | JS遍历数组与对象的方法? 58 | 59 | Array.prototype.forEach用于数组遍历,跳过undefined 60 | for (key in obj)遍历对象,会遍历对象原型中的属性 61 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | browserSync = require('browser-sync'); 3 | 4 | gulp.task('browserSync', function () { 5 | browserSync.init({ 6 | files: ['./unit_test/test/*.js', './unit_test/*.html', './src/*.js'], 7 | server: { 8 | baseDir: './' 9 | }, 10 | port: 2333 11 | }); 12 | }); 13 | 14 | gulp.task('default', ['browserSync']); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 11 | 12 | 13 |
14 |
jiavan1
15 |
jiavan1
16 |
jiavan2
17 |
18 | jiavan3 19 | 20 |
jiavan4
21 |
22 |
23 | 24 | 25 | 27 | 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanjs", 3 | "version": "1.0.0", 4 | "description": "van is jiavan", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Jiavan/vanjs.git" 12 | }, 13 | "keywords": [ 14 | "js", 15 | "vanjs" 16 | ], 17 | "author": "Jiavan", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/Jiavan/vanjs/issues" 21 | }, 22 | "homepage": "https://github.com/Jiavan/vanjs#readme" 23 | } 24 | -------------------------------------------------------------------------------- /src/module/event.module.js: -------------------------------------------------------------------------------- 1 | Van.Event = function (src, props) { 2 | if (!(this instanceof Van.Event)) { 3 | return new Van.Event(src, props); 4 | } 5 | 6 | // src是原生事件对象 7 | if (src && src.type) { 8 | this.originalEvent = src; 9 | this.type = src.type; 10 | this.isDefaultPrevented = src.defaultPrevented; 11 | } else { 12 | this.type = src; 13 | } 14 | }; 15 | 16 | -------------------------------------------------------------------------------- /src/module/test.js: -------------------------------------------------------------------------------- 1 | var a = { 2 | name: 'jiavan', 3 | age: 20 4 | }; 5 | 6 | var b = ['jiavan', '20']; 7 | 8 | var c = { 9 | name: 'jiavan', 10 | age: 20, 11 | length: 2, 12 | splice: Array.prototype.splice 13 | }; -------------------------------------------------------------------------------- /src/van.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jiavan on 16-4-7. 3 | */ 4 | (function (window, document) { 5 | // 缓存window,document对象以及声明部分变量 6 | var doc = document, 7 | Van = function (selector) { 8 | 9 | // 通过直接调用van生成一个van的实例,不必每次都用new生成实例 10 | return new Van.prototype.init(selector); 11 | }; 12 | 13 | // 重写vanjs原型并为一个类数组对象 14 | Van.prototype = { 15 | constructor: Van, 16 | // 通过设置对象的length与splice属性可让对象成为一个array-like object 17 | // 在控制台以数组的形式显示 18 | length: 0, 19 | splice: Array.prototype.splice, 20 | selector: '', 21 | 22 | // 通过选择器选择dom 23 | init: function (selector) { 24 | if (!selector) { 25 | return this; 26 | } 27 | 28 | // 如果传入的是原生DOM对象/对象集或者直接是函数 29 | // 直接通过Van()或者$()形式调用函数,在DOM树解析完成后执行 30 | if (typeof selector === 'object') { 31 | 32 | // 如果传入的是类数组对象,那么转换为数组进行遍历 33 | if (selector.length) { 34 | selector = Array.prototype.slice.call(selector); 35 | for (var i = 0; i < selector.length; i++) { 36 | this[i] = selector[i]; 37 | } 38 | this.length = selector.length; 39 | return this; 40 | } else { 41 | 42 | // 如果是原生的DOM对象 43 | this[0] = selector; 44 | this.length = 1; 45 | return this; 46 | } 47 | } else if (typeof selector === 'function') { 48 | this.ready(selector); 49 | return this; 50 | } 51 | 52 | var element, 53 | i; 54 | selector = selector.trim(); 55 | 56 | // 选择element以及返回van本身以完成链式调用 57 | if (selector.charAt(0) === '#' && !selector.match('\\s')) { 58 | selector = selector.substring(1); 59 | this.selector = selector; 60 | element = document.getElementById(selector); 61 | this[0] = element; 62 | this.length = 1; 63 | 64 | return this; 65 | } else { 66 | element = document.querySelectorAll(selector); 67 | for (i = 0; i < element.length; i++) { 68 | this[i] = element[i]; 69 | } 70 | this.selector = selector; 71 | this.length = element.length; 72 | 73 | return this; 74 | } 75 | }, 76 | 77 | /** 78 | * 设置或者获取元素的css属性 79 | * @param attr 属性名称 80 | * @param val 属性值 81 | * @returns {*} 82 | */ 83 | css: function (attr, val) { 84 | for (var i = 0; i < this.length; i++) { 85 | 86 | if (typeof attr === 'string') { 87 | // 当只有一个参数时为获得元素的某一属性 88 | if (arguments.length === 1) { 89 | 90 | // getComputedStyle IE 6~8是不支持的,currentStyle为ie属性 91 | // 通过currentStyle或者getComputedStyle获取全部css属性 92 | // getComputedStyle只读不能写,getPropertyValue获得属性值 93 | return this[i].currentStyle ? 94 | this[i].currentStyle[attr] : 95 | window.getComputedStyle(this[i], null).getPropertyValue(attr); 96 | } 97 | 98 | // 两个参数时设置对应的属性值 99 | this[i].style[attr] = val; 100 | } else if (typeof attr === 'object') { 101 | var that = this[i]; 102 | 103 | // 将对象css值全部应用在元素上,注意cssText会覆盖掉之前的样式 104 | // 所以应该在原有的css样式基础上追加css 105 | // 注意缓存上面的this 106 | Van.prototype.each(attr, function (key, value) { 107 | that.style.cssText += ' ' + key + ':' + value + ';'; 108 | }); 109 | } 110 | } 111 | 112 | return this; 113 | }, 114 | 115 | /** 116 | * 判断元素/元素集合是否有指定class值 117 | * @param className 样式类名 118 | * @returns {boolean} 119 | */ 120 | hasClass: function (className) { 121 | var reg = new RegExp('(\\s|^)' + className + '(\\s|$)'), 122 | i; 123 | 124 | for (i = 0; i < this.length; i++) { 125 | if (this[i].className.match(reg)) { 126 | return true; 127 | } 128 | } 129 | 130 | return false; 131 | }, 132 | 133 | /** 134 | * 给元素/元素集合添加class 135 | * @param className 136 | * @returns {Van} 137 | */ 138 | addClass: function (className) { 139 | var reg = new RegExp('(\\s|^)' + className + '(\\s|$)'), 140 | i; 141 | 142 | for (i = 0; i < this.length; i++) { 143 | 144 | // 如果当前元素没有该class就添加 145 | if (!this[i].className.match(reg)) { 146 | this[i].className += ' ' + className; 147 | } 148 | } 149 | 150 | return this; 151 | }, 152 | 153 | /** 154 | * 移除元素/元素集合中的某一class 155 | * @param className 156 | * @returns {Van} 157 | */ 158 | removeClass: function (className) { 159 | var reg = new RegExp('(\\s|^)' + className + '(\\s|$)'), 160 | i; 161 | 162 | for (i = 0; i < this.length; i++) { 163 | 164 | // 如果存在该class那么用空字符串替换 165 | if (this[i].className.match(reg)) { 166 | this[i].className = this[i].className.replace(className, ''); 167 | } 168 | } 169 | 170 | return this; 171 | }, 172 | 173 | // 返回元素的下一个相邻节点 174 | next: function () { 175 | return sibling(this[0], 'nextSibling'); 176 | }, 177 | 178 | // 返回元素上一个相邻节点 179 | prev: function () { 180 | return sibling(this[0], 'previousSibling'); 181 | }, 182 | 183 | // 返回元素的父节点,暂时不稳定 184 | _parent: function () { 185 | var parent = this[0].parentNode; 186 | var p = Van(); 187 | parent = parent && parent.nodeType !== 11 ? parent : null; 188 | p[0] = parent; 189 | p.selector = parent.tagName.toLowerCase(); 190 | p.length = 1; 191 | 192 | return p; 193 | }, 194 | 195 | // 返回元素的所有父节点 196 | parents: function () { 197 | var p = Van(), 198 | i = 0; 199 | 200 | // 从当前元素的父节点到document,document.nodeType === 9 201 | while ((this[0] = this[0]['parentNode']) && this[0].nodeType !== 9) { 202 | if (this[0].nodeType === 1) { 203 | p[i] = this[0]; 204 | i++; 205 | } 206 | } 207 | 208 | p.length = i; 209 | 210 | return p; 211 | }, 212 | 213 | // 当DOM树已经生成 214 | ready: function (fn) { 215 | doc.addEventListener('DOMContentLoaded', fn, false); 216 | }, 217 | 218 | // 遍历对象或数组 219 | each: function (obj, callback, args) { 220 | var i = 0, 221 | length = obj.length, 222 | isArray = Array.isArray(obj), 223 | value; 224 | 225 | // 如果带有参数,将参数通过apply传入回调函数 226 | // callback第一个参数是key,第二个参数是value 227 | if (args) { 228 | if (isArray) { 229 | for (; i < length; i++) { 230 | value = callback.apply(obj[i], args); 231 | 232 | if (value === false) { 233 | break; 234 | } 235 | } 236 | } else { 237 | for (i in obj) { 238 | value = callback.apply(obj[i], args); 239 | 240 | if (value === false) { 241 | break; 242 | } 243 | } 244 | } 245 | } else { 246 | // 如果没有带参数,用call方法并传入元素的索引值以及本身 247 | if (isArray) { 248 | for (; i < length; i++) { 249 | value = callback.call(obj[i], i, obj[i]); 250 | if (value === false) { 251 | break; 252 | } 253 | } 254 | } else { 255 | for (i in obj) { 256 | value = callback.call(obj[i], i, obj[i]); 257 | 258 | if (value === false) { 259 | break; 260 | } 261 | } 262 | } 263 | } 264 | 265 | return obj; 266 | }, 267 | 268 | // 查找某一元素集合,返回的是Van对象,不是DOM集合对象 269 | find: function (selector) { 270 | if (!selector) { 271 | return; 272 | } 273 | 274 | var context = this.selector; 275 | return Van(context + ' ' + selector); 276 | }, 277 | 278 | // 获得第一个元素,Van对象类型 279 | first: function () { 280 | return Van(this[0]); 281 | }, 282 | 283 | // 最后一个元素,Van类型 284 | last: function () { 285 | return Van(this[this.length - 1]); 286 | }, 287 | 288 | // 获得指定num的元素,并返回Van对象 289 | eq: function (num) { 290 | var res = num < 0 ? 291 | this[this.length - 1] : 292 | this[num]; 293 | 294 | return Van(res); 295 | }, 296 | 297 | // 获得指定num的原生DOM对象 298 | get: function (num) { 299 | return num < 0 ? 300 | this[this.length - 1 + num] : 301 | this[num]; 302 | }, 303 | 304 | /** 305 | * 设置/获得属性值,仅传一个参数时返回属性值,2个参数或者 306 | * attribute key-value对象时为设置参数 307 | * @param attr 属性名称 308 | * @param val 属性值 309 | * @returns {string} 310 | */ 311 | attr: function (attr, val) { 312 | var i, 313 | that; 314 | 315 | for (i = 0; i < this.length; i++) { 316 | if (typeof attr === 'string') { 317 | 318 | // 获取属性值 319 | if (arguments.length === 1) { 320 | return this[i].getAttribute(attr); 321 | } 322 | 323 | this[i].setAttribute(attr, val); 324 | } else { 325 | 326 | // 设置多个属性值 327 | that = this[i]; 328 | Van.prototype.each(attr, function (attr, val) { 329 | that.setAttribute(attr, val); 330 | }); 331 | } 332 | } 333 | 334 | return this; 335 | }, 336 | 337 | // 设置或者获得data属性 338 | data: function (attr, val) { 339 | var i, 340 | that; 341 | 342 | for (i = 0; i < this.length; i++) { 343 | if (typeof attr === 'string') { 344 | if (arguments.length === 1) { 345 | return this[i].getAttribute('data-' + attr); 346 | } 347 | 348 | this[i].setAttribute('data-' + attr, val); 349 | } else { 350 | that = this[i]; 351 | Van.prototype.each(attr, function (attr, val) { 352 | that.setAttribute('data-' + attr, val); 353 | }); 354 | } 355 | } 356 | 357 | return this; 358 | }, 359 | 360 | // 获得元素的html内容/设置元素的html 361 | html: function (val) { 362 | var i; 363 | if (val === undefined && this[0].nodeType === 1) { 364 | return this[0].innerHTML; 365 | } else { 366 | for (i = 0; i < this.length; i++) { 367 | this[i].innerHTML = val; 368 | } 369 | } 370 | 371 | return this; 372 | }, 373 | 374 | text: function (val) { 375 | if (val === undefined && this[0].nodeType === 1) { 376 | return this[0].innerText; 377 | } else { 378 | for (var i = 0; i < this.length; i++) { 379 | this[i].innerText = val; 380 | } 381 | } 382 | }, 383 | 384 | // 操作DOM,相对于innerHTML,该方法不会重新解析调用该方法的元素 385 | // 因此不会影响到已经存在的元素解析,避免额外的解析操作 386 | // beforebegin 在 element 元素的前面 387 | // afterbegin 在 element 元素的第一个子节点前面 388 | // beforeend 在 element 元素的最后一个子节点后面 389 | // afterend 在 element 元素的后面 390 | // text 是字符串,会被解析成 HTML 或 XML,并插入到 DOM 树中 391 | appendDOM: function (el, position, str) { 392 | el.insertAdjacentHTML(position, str); 393 | }, 394 | 395 | // 添加在当前元素的最后一个子元素后面 396 | append: function (str) { 397 | for (var i = 0; i < this.length; i++) { 398 | console.log(str) 399 | Van.prototype.appendDOM(this[i], 'beforeend', str); 400 | } 401 | return this; 402 | }, 403 | 404 | // 插在当前元素的前方 405 | before: function (str) { 406 | for (var i = 0; i < this.length; i++) { 407 | Van.prototype.appendDOM(this[i], 'beforebegin', str); 408 | } 409 | return this; 410 | }, 411 | 412 | // 插在当前元素的后方 413 | after: function (str) { 414 | for (var i = 0; i < this.length; i++) { 415 | Van.prototype.appendDOM(this[i], 'afterend', str); 416 | } 417 | return this; 418 | }, 419 | 420 | // 移除元素本身 421 | remove: function () { 422 | for (var i = 0; i < this.length; i++) { 423 | this[i].parentNode.removeChild(this[i]); 424 | } 425 | return this; 426 | }, 427 | 428 | /** 429 | * 事件代理实现 430 | * @param agent 代理对象 431 | * @param type 事件类型 432 | * @param selector 选择要设置事件的元素 433 | * @param fn 事件处理程序 434 | * @private 435 | */ 436 | _delegate: function (agent, type, selector, fn) { 437 | 438 | agent.addEventListener(type, function (e) { 439 | var target = e.target, 440 | //ctarget = e.currentTarget, 441 | bubble = true; 442 | 443 | //while(bubble && target != ctarget) { 444 | if(filiter(agent,selector,target)) { 445 | bubble = fn.call(target,e); 446 | return bubble; 447 | } 448 | //target = target.parentNode; 449 | //} 450 | }, false); 451 | 452 | function filiter(agent, selector, target) { 453 | var nodes = agent.querySelectorAll(selector); 454 | for (var i = 0; i < nodes.length; i++) { 455 | if (nodes[i] === target) { 456 | return true; 457 | } 458 | } 459 | } 460 | }, 461 | 462 | // 绑定事件,不使用委托 463 | _on: function (type, selector, fn) { 464 | if (typeof selector === 'function') { 465 | fn = selector; 466 | for (var i = 0; i < this.length; i++) { 467 | if (!this[i].guid) { 468 | this[i].guid = ++Van.guid; 469 | Van.Events[Van.guid] = {}; 470 | 471 | Van.Events[Van.guid][type] = [fn]; 472 | Van._bind(this[i], type, this[i].guid); 473 | } else { 474 | var id = this[i].guid; 475 | if (Van.Events[id][type]) { 476 | Van.Events[id][type].push(fn); 477 | } else { 478 | Van.Events[id][type] = [fn]; 479 | Van._bind(this[i], type, id); 480 | } 481 | } 482 | } 483 | } 484 | }, 485 | 486 | _bind: function (dom, type, guid) { 487 | dom.addEventListener(type, function (e) { 488 | for (var i = 0; i < Van.Events[guid][type].length; i++) { 489 | Van.Events.guid[type][i].call(dom, e); 490 | } 491 | }, false); 492 | }, 493 | 494 | // 设置/获得元素的宽度 495 | width: function (val) { 496 | 497 | // 当获取元素的宽度时 498 | if (arguments.length === 0) { 499 | 500 | // 如果选择的是window,即Van(this) 501 | if (this[0].document === doc) { 502 | return this[0].innerWidth; 503 | // 如果选择的是document 504 | } else if (this[0].nodeType === 9) { 505 | return document.documentElement.clientWidth; 506 | } else { 507 | // 选择的是普通元素 508 | return parseInt(window.getComputedStyle(this[0], null)['width']); 509 | } 510 | } else { 511 | // 设置元素的宽度 512 | for (var i = 0; i < this.length; i++) { 513 | this[i].style.width = val + 'px'; 514 | } 515 | } 516 | }, 517 | 518 | // 设置或者获得元素的高度 519 | height: function (val) { 520 | 521 | // 当获取元素的高度时 522 | if (arguments.length === 0) { 523 | 524 | // 如果选择的是window,即Van(this) 525 | if (this[0].document === doc) { 526 | return this[0].innerHeight; 527 | // 如果选择的是document 528 | } else if (this[0].nodeType === 9) { 529 | return document.documentElement.clientHeight; 530 | } else { 531 | // 选择的是普通元素 532 | return parseInt(window.getComputedStyle(this[0], null)['height']); 533 | } 534 | } else { 535 | // 设置元素的高度 536 | for (var i = 0; i < this.length; i++) { 537 | this[i].style.height = val + 'px'; 538 | } 539 | } 540 | } 541 | }; 542 | 543 | /** 544 | * 筛选是nodeType为1(为元素标签)的子节点 545 | * @param element 目标元素 546 | * @param dest 筛选的类型,previous ? next 547 | * @returns {*} 548 | */ 549 | function sibling(element, dir) { 550 | while ((element = element[dir]) && element.nodeType !== 1) { 551 | } 552 | return element; 553 | } 554 | 555 | /** 556 | * Ajax请求 557 | * @param options 选项配置信息 558 | */ 559 | Van.ajax = function (options) { 560 | 561 | // 默认配置 562 | var defaultOptions = { 563 | url: false, // ajax请求地址 564 | type: 'GET', // 请求方式 565 | data: false, // post时附带的数据 566 | success: false, // ajax成功回调函数 567 | complete: false // ajax完成时的回调 568 | }; 569 | var xhr = null, 570 | url; 571 | 572 | // 配置传入的options,如果存在值为undefined属性,则设为默认options value 573 | for (var i in defaultOptions) { 574 | if (options[i] === undefined) { 575 | options[i] = defaultOptions[i]; 576 | } 577 | } 578 | 579 | xhr = new XMLHttpRequest(); 580 | url = options.url; 581 | xhr.open(options.type, url); 582 | xhr.onreadystatechange = onStateChange; 583 | 584 | if (options.type.toLowerCase() === 'post') { 585 | xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 586 | } 587 | xhr.send(options.data ? options.data : null); 588 | 589 | // ajax回应状态更改 590 | function onStateChange() { 591 | if (xhr.readyState === 4) { 592 | var result, 593 | status = xhr.status; 594 | 595 | if ((status >= 200 && status < 300) || status === 304) { 596 | result = xhr.responseText; 597 | 598 | if (window.JSON) { 599 | result = JSON.parse(result); 600 | } else { 601 | result = eval('(' + result + ')'); 602 | } 603 | 604 | // 数据解析完成,调用触发ajax请求成功 605 | ajaxSuccess(result, xhr); 606 | } else { 607 | console.log('ERR', xhr.status); 608 | } 609 | } 610 | } 611 | 612 | // ajax请求成功 613 | function ajaxSuccess(data, xhr) { 614 | var status = 'success'; 615 | // 如果存在ajax成功回调函数,就调用回调函数 616 | options.success && options.success(data, options, status, xhr); 617 | // ajax请求成功后触发ajax请求过程已完成 618 | ajaxComplete(status); 619 | } 620 | 621 | // ajax请求完成 622 | function ajaxComplete(status) { 623 | options.complete && options.complete(status); 624 | } 625 | }; 626 | 627 | /** 628 | * 通过ajax方法发get请求 629 | * @param url 请求地址 630 | * @param successCallback 成功回调函数 631 | * @param completeCallback 完成回调函数 632 | */ 633 | Van.get = function (url, successCallback, completeCallback) { 634 | var options = { 635 | url: url, 636 | success: successCallback, 637 | complete: completeCallback 638 | }; 639 | Van.ajax(options); 640 | }; 641 | 642 | /** 643 | * 通过ajax方法发post类型请求,二次封装 644 | * @param url 请求地址 645 | * @param data 要post的数据 646 | * @param successCallback 成功回调 647 | * @param completeCallback 完成回调 648 | */ 649 | Van.post = function (url, data, successCallback, completeCallback) { 650 | var options = { 651 | url: url, 652 | type: 'post', 653 | data: data, 654 | success: successCallback, 655 | complete: completeCallback 656 | }; 657 | Van.ajax(options); 658 | }; 659 | 660 | // 挂载事件数组 661 | Van.Events = []; 662 | Van.guid = 0; 663 | 664 | // Van扩展方法,原型扩展或直接对象挂载 665 | Van.prototype.extend = Van.extend = function () { 666 | var options = arguments[0]; 667 | for (var i in options) { 668 | this[i] = options[i]; 669 | } 670 | }; 671 | 672 | // 将init方法的原型指向van的原型,以便生成的实例可以完成链式调用 673 | Van.prototype.init.prototype = Van.prototype; 674 | 675 | // 将van挂载到window全局上 676 | window.Van = Van; 677 | window.$ = Van; 678 | })(window, document); -------------------------------------------------------------------------------- /src/van2.0.0.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param winow 将全局传入作为局部变量 3 | * @param undefined 将undefined在函数内部声明 4 | * 当立即执行函数内需要查找window或者undefined时可直接使用 5 | * 缩短了变量查找时要查找的作用域链长度 6 | */ 7 | (function (window, undefined) { 8 | 9 | var Van = (function () { 10 | var Van = function (selector, context) { 11 | return new Van.fn.init(selector, context, rootVan); 12 | }, 13 | 14 | rootVan, 15 | 16 | // reference core methods 17 | toString = Object.prototype.toString, 18 | hasOwn = Object.prototype.hasOwnProperty, 19 | push = Array.prototype.push, 20 | slice = Array.prototype.slice, 21 | trim = String.prototype.trim, 22 | indexOf = Array.prototype.indexOf; 23 | 24 | // fn为Van原型对象的别名 25 | Van.fn = Van.prototype = { 26 | constructor: Van, 27 | init: function (selector, context, rootVan) { 28 | 29 | // Handle $(''), $(null), $(undefined) 30 | if (!selector) { 31 | return this; 32 | } 33 | 34 | // Handle $(DOMElement),传入原生DOM 35 | if (selector.nodeType) { 36 | this.context = this[0] = selector; 37 | this.length = 1; 38 | return this; 39 | } 40 | 41 | // Handle $(body),传入body 42 | if (selector === 'body' && !context && document.body) { 43 | this.context = document; 44 | this.selector = selector; 45 | this.length = 1; 46 | return this; 47 | } 48 | }, 49 | selector: '', 50 | van: '2.0.0', 51 | 52 | // Van对象的长度 53 | length: 0, 54 | 55 | // Van对象的size 56 | size: function () { 57 | return this.length; 58 | }, 59 | 60 | // 将一个对象转换为数组 61 | toArray: function () { 62 | return slice.call(this, 0); 63 | }, 64 | 65 | // 传入参数返回指定对象,支持负值 66 | // 不传参数默认返回全部元素的数组 67 | get: function (num) { 68 | return num === null ? 69 | this.toArray() : 70 | (num < 0 ? this[this.length + num] : num); 71 | }, 72 | 73 | // 74 | pushStack: function (elems, name, selector) { 75 | 76 | }, 77 | 78 | // 调用了静态方法 79 | each: function (callback, args) { 80 | return Van.each(this, callback, args); 81 | }, 82 | ready: function (fn) { 83 | }, 84 | eq: function (i) { 85 | }, 86 | first: function () { 87 | }, 88 | last: function () { 89 | }, 90 | slice: function () { 91 | }, 92 | map: function (callback) { 93 | }, 94 | end: function () { 95 | }, 96 | push: push, 97 | sort: [].sort, 98 | //splice: [].splice 99 | }; 100 | 101 | // 将init方法的原型指向Van的原型,通过调用Van生成的init实例可调用Van原型上的方法 102 | Van.fn.init.prototype = Van.fn; 103 | 104 | // target[, obj][, obj..] 105 | // 一个参数时,直接对Van进行扩展,多个参数时,将后面的对象合并到target(后面覆盖之前属性) 106 | // Van.extend直接挂载在对象上this指向Van 107 | // Van.fn.extend挂载在Van的原型上,this指向prototype 108 | Van.extend = Van.fn.extend = function () { 109 | var target = arguments[0] || {}, 110 | i = 1, 111 | length = arguments.length, 112 | options, 113 | name, 114 | copy; 115 | 116 | // 当传入的target不为对象或者函数,默认设置为obj 117 | if (typeof target !== 'object' && typeof target !== 'function') { 118 | target = {}; 119 | } 120 | 121 | // 当只有一个对象时,直接在Van上扩展 122 | if (length === 1) { 123 | target = this; 124 | --i; 125 | } 126 | 127 | // 对传入的对象进行合并,合并到target对象 128 | for (; i < length; i++) { 129 | if ((options = arguments[i]) != null) { 130 | for (name in options) { 131 | copy = options[name]; 132 | if (copy !== undefined) { 133 | target[name] = copy; 134 | } 135 | } 136 | } 137 | } 138 | 139 | return target; 140 | }; 141 | 142 | return Van; 143 | })(); 144 | 145 | // 在Van的基础上扩展静态方法 146 | Van.extend({ 147 | 148 | isArray: Array.isArray, 149 | 150 | // 是否是一个函数 151 | isFunction: function (val) { 152 | return typeof val === 'function'; 153 | }, 154 | 155 | // 数组或对象遍历迭代方法 156 | each: function (obj, callbcak, args) { 157 | var i = 0, 158 | key, 159 | length = obj.length, 160 | isArray = Array.isArray; 161 | 162 | if (args) { 163 | 164 | // 存在参数的时候用apply传递参数 165 | if (isArray(obj)) { 166 | 167 | for (; i < length; i++) { 168 | if (callbcak.apply(obj[i], args) === false) { 169 | break; 170 | } 171 | } 172 | } else { 173 | 174 | // obj为对象时用for in循环,能遍历到原型中的属性 175 | for (key in obj) { 176 | if (callbcak.apply(obj[key], args) === false) { 177 | break; 178 | } 179 | } 180 | } 181 | } else { 182 | 183 | // 不存在外部传参时,使用call方法传递参数,回调第一个参数是key,第二个是value 184 | if (isArray(obj)) { 185 | for (; i < length; i++) { 186 | if (callbcak.call(obj[i], i, obj[i]) === false) { 187 | break; 188 | } 189 | } 190 | } else { 191 | for (key in obj) { 192 | if (callbcak.call(obj[key], key, obj[key]) === false) { 193 | break; 194 | } 195 | } 196 | } 197 | } 198 | 199 | return obj; 200 | }, 201 | 202 | map: function (elems, callback, arg) { 203 | var i = 0, 204 | length = elems.length, 205 | set = [], 206 | key, 207 | value; 208 | 209 | if (this.isArray(elems)) { 210 | for (; i < length; i++) { 211 | value = callback.apply(elems[i], arg); 212 | if (value) { 213 | set[set.length] = value; 214 | } 215 | } 216 | } else { 217 | for (key in elems) { 218 | value = elems[key]; 219 | if(value) { 220 | set[set.length] = value; 221 | } 222 | } 223 | } 224 | 225 | return set; 226 | } 227 | }); 228 | 229 | window.Van = window.$ = Van; 230 | 231 | })(window); -------------------------------------------------------------------------------- /unit_test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

Qunit Hello World

20 |

21 |
22 |

23 |
    24 |
    25 | 26 | -------------------------------------------------------------------------------- /unit_test/index1.0.0.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 | 11 | 12 |

    Qunit Hello World

    13 |

    14 |
    15 |

    16 |
      17 |
      18 | 19 | -------------------------------------------------------------------------------- /unit_test/qunit/qunit.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * QUnit 1.23.0 3 | * https://qunitjs.com/ 4 | * 5 | * Copyright jQuery Foundation and other contributors 6 | * Released under the MIT license 7 | * https://jquery.org/license 8 | * 9 | * Date: 2016-03-25T19:37Z 10 | */ 11 | 12 | /** Font Family and Sizes */ 13 | 14 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult { 15 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 16 | } 17 | 18 | #qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 19 | #qunit-tests { font-size: smaller; } 20 | 21 | 22 | /** Resets */ 23 | 24 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | 30 | /** Header */ 31 | 32 | #qunit-header { 33 | padding: 0.5em 0 0.5em 1em; 34 | 35 | color: #8699A4; 36 | background-color: #0D3349; 37 | 38 | font-size: 1.5em; 39 | line-height: 1em; 40 | font-weight: 400; 41 | 42 | border-radius: 5px 5px 0 0; 43 | } 44 | 45 | #qunit-header a { 46 | text-decoration: none; 47 | color: #C2CCD1; 48 | } 49 | 50 | #qunit-header a:hover, 51 | #qunit-header a:focus { 52 | color: #FFF; 53 | } 54 | 55 | #qunit-testrunner-toolbar label { 56 | display: inline-block; 57 | padding: 0 0.5em 0 0.1em; 58 | } 59 | 60 | #qunit-banner { 61 | height: 5px; 62 | } 63 | 64 | #qunit-testrunner-toolbar { 65 | padding: 0.5em 1em 0.5em 1em; 66 | color: #5E740B; 67 | background-color: #EEE; 68 | overflow: hidden; 69 | } 70 | 71 | #qunit-filteredTest { 72 | padding: 0.5em 1em 0.5em 1em; 73 | background-color: #F4FF77; 74 | color: #366097; 75 | } 76 | 77 | #qunit-userAgent { 78 | padding: 0.5em 1em 0.5em 1em; 79 | background-color: #2B81AF; 80 | color: #FFF; 81 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 82 | } 83 | 84 | #qunit-modulefilter-container { 85 | float: right; 86 | padding: 0.2em; 87 | } 88 | 89 | .qunit-url-config { 90 | display: inline-block; 91 | padding: 0.1em; 92 | } 93 | 94 | .qunit-filter { 95 | display: block; 96 | float: right; 97 | margin-left: 1em; 98 | } 99 | 100 | /** Tests: Pass/Fail */ 101 | 102 | #qunit-tests { 103 | list-style-position: inside; 104 | } 105 | 106 | #qunit-tests li { 107 | padding: 0.4em 1em 0.4em 1em; 108 | border-bottom: 1px solid #FFF; 109 | list-style-position: inside; 110 | } 111 | 112 | #qunit-tests > li { 113 | display: none; 114 | } 115 | 116 | #qunit-tests li.running, 117 | #qunit-tests li.pass, 118 | #qunit-tests li.fail, 119 | #qunit-tests li.skipped { 120 | display: list-item; 121 | } 122 | 123 | #qunit-tests.hidepass { 124 | position: relative; 125 | } 126 | 127 | #qunit-tests.hidepass li.running, 128 | #qunit-tests.hidepass li.pass { 129 | visibility: hidden; 130 | position: absolute; 131 | width: 0; 132 | height: 0; 133 | padding: 0; 134 | border: 0; 135 | margin: 0; 136 | } 137 | 138 | #qunit-tests li strong { 139 | cursor: pointer; 140 | } 141 | 142 | #qunit-tests li.skipped strong { 143 | cursor: default; 144 | } 145 | 146 | #qunit-tests li a { 147 | padding: 0.5em; 148 | color: #C2CCD1; 149 | text-decoration: none; 150 | } 151 | 152 | #qunit-tests li p a { 153 | padding: 0.25em; 154 | color: #6B6464; 155 | } 156 | #qunit-tests li a:hover, 157 | #qunit-tests li a:focus { 158 | color: #000; 159 | } 160 | 161 | #qunit-tests li .runtime { 162 | float: right; 163 | font-size: smaller; 164 | } 165 | 166 | .qunit-assert-list { 167 | margin-top: 0.5em; 168 | padding: 0.5em; 169 | 170 | background-color: #FFF; 171 | 172 | border-radius: 5px; 173 | } 174 | 175 | .qunit-source { 176 | margin: 0.6em 0 0.3em; 177 | } 178 | 179 | .qunit-collapsed { 180 | display: none; 181 | } 182 | 183 | #qunit-tests table { 184 | border-collapse: collapse; 185 | margin-top: 0.2em; 186 | } 187 | 188 | #qunit-tests th { 189 | text-align: right; 190 | vertical-align: top; 191 | padding: 0 0.5em 0 0; 192 | } 193 | 194 | #qunit-tests td { 195 | vertical-align: top; 196 | } 197 | 198 | #qunit-tests pre { 199 | margin: 0; 200 | white-space: pre-wrap; 201 | word-wrap: break-word; 202 | } 203 | 204 | #qunit-tests del { 205 | background-color: #E0F2BE; 206 | color: #374E0C; 207 | text-decoration: none; 208 | } 209 | 210 | #qunit-tests ins { 211 | background-color: #FFCACA; 212 | color: #500; 213 | text-decoration: none; 214 | } 215 | 216 | /*** Test Counts */ 217 | 218 | #qunit-tests b.counts { color: #000; } 219 | #qunit-tests b.passed { color: #5E740B; } 220 | #qunit-tests b.failed { color: #710909; } 221 | 222 | #qunit-tests li li { 223 | padding: 5px; 224 | background-color: #FFF; 225 | border-bottom: none; 226 | list-style-position: inside; 227 | } 228 | 229 | /*** Passing Styles */ 230 | 231 | #qunit-tests li li.pass { 232 | color: #3C510C; 233 | background-color: #FFF; 234 | border-left: 10px solid #C6E746; 235 | } 236 | 237 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 238 | #qunit-tests .pass .test-name { color: #366097; } 239 | 240 | #qunit-tests .pass .test-actual, 241 | #qunit-tests .pass .test-expected { color: #999; } 242 | 243 | #qunit-banner.qunit-pass { background-color: #C6E746; } 244 | 245 | /*** Failing Styles */ 246 | 247 | #qunit-tests li li.fail { 248 | color: #710909; 249 | background-color: #FFF; 250 | border-left: 10px solid #EE5757; 251 | white-space: pre; 252 | } 253 | 254 | #qunit-tests > li:last-child { 255 | border-radius: 0 0 5px 5px; 256 | } 257 | 258 | #qunit-tests .fail { color: #000; background-color: #EE5757; } 259 | #qunit-tests .fail .test-name, 260 | #qunit-tests .fail .module-name { color: #000; } 261 | 262 | #qunit-tests .fail .test-actual { color: #EE5757; } 263 | #qunit-tests .fail .test-expected { color: #008000; } 264 | 265 | #qunit-banner.qunit-fail { background-color: #EE5757; } 266 | 267 | /*** Skipped tests */ 268 | 269 | #qunit-tests .skipped { 270 | background-color: #EBECE9; 271 | } 272 | 273 | #qunit-tests .qunit-skipped-label { 274 | background-color: #F4FF77; 275 | display: inline-block; 276 | font-style: normal; 277 | color: #366097; 278 | line-height: 1.8em; 279 | padding: 0 0.5em; 280 | margin: -0.4em 0.4em -0.4em 0; 281 | } 282 | 283 | /** Result */ 284 | 285 | #qunit-testresult { 286 | padding: 0.5em 1em 0.5em 1em; 287 | 288 | color: #2B81AF; 289 | background-color: #D2E0E6; 290 | 291 | border-bottom: 1px solid #FFF; 292 | } 293 | #qunit-testresult .module-name { 294 | font-weight: 700; 295 | } 296 | 297 | /** Fixture */ 298 | 299 | #qunit-fixture { 300 | position: absolute; 301 | top: -10000px; 302 | left: -10000px; 303 | width: 1000px; 304 | height: 1000px; 305 | } 306 | -------------------------------------------------------------------------------- /unit_test/qunit/qunit.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * QUnit 1.23.0 3 | * https://qunitjs.com/ 4 | * 5 | * Copyright jQuery Foundation and other contributors 6 | * Released under the MIT license 7 | * https://jquery.org/license 8 | * 9 | * Date: 2016-03-25T19:37Z 10 | */ 11 | 12 | ( function( global ) { 13 | 14 | var QUnit = {}; 15 | 16 | var Date = global.Date; 17 | var now = Date.now || function() { 18 | return new Date().getTime(); 19 | }; 20 | 21 | var setTimeout = global.setTimeout; 22 | var clearTimeout = global.clearTimeout; 23 | 24 | // Store a local window from the global to allow direct references. 25 | var window = global.window; 26 | 27 | var defined = { 28 | document: window && window.document !== undefined, 29 | setTimeout: setTimeout !== undefined, 30 | sessionStorage: ( function() { 31 | var x = "qunit-test-string"; 32 | try { 33 | sessionStorage.setItem( x, x ); 34 | sessionStorage.removeItem( x ); 35 | return true; 36 | } catch ( e ) { 37 | return false; 38 | } 39 | }() ) 40 | }; 41 | 42 | var fileName = ( sourceFromStacktrace( 0 ) || "" ).replace( /(:\d+)+\)?/, "" ).replace( /.+\//, "" ); 43 | var globalStartCalled = false; 44 | var runStarted = false; 45 | 46 | var toString = Object.prototype.toString, 47 | hasOwn = Object.prototype.hasOwnProperty; 48 | 49 | // Returns a new Array with the elements that are in a but not in b 50 | function diff( a, b ) { 51 | var i, j, 52 | result = a.slice(); 53 | 54 | for ( i = 0; i < result.length; i++ ) { 55 | for ( j = 0; j < b.length; j++ ) { 56 | if ( result[ i ] === b[ j ] ) { 57 | result.splice( i, 1 ); 58 | i--; 59 | break; 60 | } 61 | } 62 | } 63 | return result; 64 | } 65 | 66 | // From jquery.js 67 | function inArray( elem, array ) { 68 | if ( array.indexOf ) { 69 | return array.indexOf( elem ); 70 | } 71 | 72 | for ( var i = 0, length = array.length; i < length; i++ ) { 73 | if ( array[ i ] === elem ) { 74 | return i; 75 | } 76 | } 77 | 78 | return -1; 79 | } 80 | 81 | /** 82 | * Makes a clone of an object using only Array or Object as base, 83 | * and copies over the own enumerable properties. 84 | * 85 | * @param {Object} obj 86 | * @return {Object} New object with only the own properties (recursively). 87 | */ 88 | function objectValues ( obj ) { 89 | var key, val, 90 | vals = QUnit.is( "array", obj ) ? [] : {}; 91 | for ( key in obj ) { 92 | if ( hasOwn.call( obj, key ) ) { 93 | val = obj[ key ]; 94 | vals[ key ] = val === Object( val ) ? objectValues( val ) : val; 95 | } 96 | } 97 | return vals; 98 | } 99 | 100 | function extend( a, b, undefOnly ) { 101 | for ( var prop in b ) { 102 | if ( hasOwn.call( b, prop ) ) { 103 | 104 | // Avoid "Member not found" error in IE8 caused by messing with window.constructor 105 | // This block runs on every environment, so `global` is being used instead of `window` 106 | // to avoid errors on node. 107 | if ( prop !== "constructor" || a !== global ) { 108 | if ( b[ prop ] === undefined ) { 109 | delete a[ prop ]; 110 | } else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) { 111 | a[ prop ] = b[ prop ]; 112 | } 113 | } 114 | } 115 | } 116 | 117 | return a; 118 | } 119 | 120 | function objectType( obj ) { 121 | if ( typeof obj === "undefined" ) { 122 | return "undefined"; 123 | } 124 | 125 | // Consider: typeof null === object 126 | if ( obj === null ) { 127 | return "null"; 128 | } 129 | 130 | var match = toString.call( obj ).match( /^\[object\s(.*)\]$/ ), 131 | type = match && match[ 1 ]; 132 | 133 | switch ( type ) { 134 | case "Number": 135 | if ( isNaN( obj ) ) { 136 | return "nan"; 137 | } 138 | return "number"; 139 | case "String": 140 | case "Boolean": 141 | case "Array": 142 | case "Set": 143 | case "Map": 144 | case "Date": 145 | case "RegExp": 146 | case "Function": 147 | case "Symbol": 148 | return type.toLowerCase(); 149 | } 150 | if ( typeof obj === "object" ) { 151 | return "object"; 152 | } 153 | } 154 | 155 | // Safe object type checking 156 | function is( type, obj ) { 157 | return QUnit.objectType( obj ) === type; 158 | } 159 | 160 | // Doesn't support IE6 to IE9, it will return undefined on these browsers 161 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack 162 | function extractStacktrace( e, offset ) { 163 | offset = offset === undefined ? 4 : offset; 164 | 165 | var stack, include, i; 166 | 167 | if ( e.stack ) { 168 | stack = e.stack.split( "\n" ); 169 | if ( /^error$/i.test( stack[ 0 ] ) ) { 170 | stack.shift(); 171 | } 172 | if ( fileName ) { 173 | include = []; 174 | for ( i = offset; i < stack.length; i++ ) { 175 | if ( stack[ i ].indexOf( fileName ) !== -1 ) { 176 | break; 177 | } 178 | include.push( stack[ i ] ); 179 | } 180 | if ( include.length ) { 181 | return include.join( "\n" ); 182 | } 183 | } 184 | return stack[ offset ]; 185 | 186 | // Support: Safari <=6 only 187 | } else if ( e.sourceURL ) { 188 | 189 | // Exclude useless self-reference for generated Error objects 190 | if ( /qunit.js$/.test( e.sourceURL ) ) { 191 | return; 192 | } 193 | 194 | // For actual exceptions, this is useful 195 | return e.sourceURL + ":" + e.line; 196 | } 197 | } 198 | 199 | function sourceFromStacktrace( offset ) { 200 | var error = new Error(); 201 | 202 | // Support: Safari <=7 only, IE <=10 - 11 only 203 | // Not all browsers generate the `stack` property for `new Error()`, see also #636 204 | if ( !error.stack ) { 205 | try { 206 | throw error; 207 | } catch ( err ) { 208 | error = err; 209 | } 210 | } 211 | 212 | return extractStacktrace( error, offset ); 213 | } 214 | 215 | /** 216 | * Config object: Maintain internal state 217 | * Later exposed as QUnit.config 218 | * `config` initialized at top of scope 219 | */ 220 | var config = { 221 | 222 | // The queue of tests to run 223 | queue: [], 224 | 225 | // Block until document ready 226 | blocking: true, 227 | 228 | // By default, run previously failed tests first 229 | // very useful in combination with "Hide passed tests" checked 230 | reorder: true, 231 | 232 | // By default, modify document.title when suite is done 233 | altertitle: true, 234 | 235 | // HTML Reporter: collapse every test except the first failing test 236 | // If false, all failing tests will be expanded 237 | collapse: true, 238 | 239 | // By default, scroll to top of the page when suite is done 240 | scrolltop: true, 241 | 242 | // Depth up-to which object will be dumped 243 | maxDepth: 5, 244 | 245 | // When enabled, all tests must call expect() 246 | requireExpects: false, 247 | 248 | // Placeholder for user-configurable form-exposed URL parameters 249 | urlConfig: [], 250 | 251 | // Set of all modules. 252 | modules: [], 253 | 254 | // Stack of nested modules 255 | moduleStack: [], 256 | 257 | // The first unnamed module 258 | currentModule: { 259 | name: "", 260 | tests: [] 261 | }, 262 | 263 | callbacks: {} 264 | }; 265 | 266 | // Push a loose unnamed module to the modules collection 267 | config.modules.push( config.currentModule ); 268 | 269 | var loggingCallbacks = {}; 270 | 271 | // Register logging callbacks 272 | function registerLoggingCallbacks( obj ) { 273 | var i, l, key, 274 | callbackNames = [ "begin", "done", "log", "testStart", "testDone", 275 | "moduleStart", "moduleDone" ]; 276 | 277 | function registerLoggingCallback( key ) { 278 | var loggingCallback = function( callback ) { 279 | if ( objectType( callback ) !== "function" ) { 280 | throw new Error( 281 | "QUnit logging methods require a callback function as their first parameters." 282 | ); 283 | } 284 | 285 | config.callbacks[ key ].push( callback ); 286 | }; 287 | 288 | // DEPRECATED: This will be removed on QUnit 2.0.0+ 289 | // Stores the registered functions allowing restoring 290 | // at verifyLoggingCallbacks() if modified 291 | loggingCallbacks[ key ] = loggingCallback; 292 | 293 | return loggingCallback; 294 | } 295 | 296 | for ( i = 0, l = callbackNames.length; i < l; i++ ) { 297 | key = callbackNames[ i ]; 298 | 299 | // Initialize key collection of logging callback 300 | if ( objectType( config.callbacks[ key ] ) === "undefined" ) { 301 | config.callbacks[ key ] = []; 302 | } 303 | 304 | obj[ key ] = registerLoggingCallback( key ); 305 | } 306 | } 307 | 308 | function runLoggingCallbacks( key, args ) { 309 | var i, l, callbacks; 310 | 311 | callbacks = config.callbacks[ key ]; 312 | for ( i = 0, l = callbacks.length; i < l; i++ ) { 313 | callbacks[ i ]( args ); 314 | } 315 | } 316 | 317 | // DEPRECATED: This will be removed on 2.0.0+ 318 | // This function verifies if the loggingCallbacks were modified by the user 319 | // If so, it will restore it, assign the given callback and print a console warning 320 | function verifyLoggingCallbacks() { 321 | var loggingCallback, userCallback; 322 | 323 | for ( loggingCallback in loggingCallbacks ) { 324 | if ( QUnit[ loggingCallback ] !== loggingCallbacks[ loggingCallback ] ) { 325 | 326 | userCallback = QUnit[ loggingCallback ]; 327 | 328 | // Restore the callback function 329 | QUnit[ loggingCallback ] = loggingCallbacks[ loggingCallback ]; 330 | 331 | // Assign the deprecated given callback 332 | QUnit[ loggingCallback ]( userCallback ); 333 | 334 | if ( global.console && global.console.warn ) { 335 | global.console.warn( 336 | "QUnit." + loggingCallback + " was replaced with a new value.\n" + 337 | "Please, check out the documentation on how to apply logging callbacks.\n" + 338 | "Reference: https://api.qunitjs.com/category/callbacks/" 339 | ); 340 | } 341 | } 342 | } 343 | } 344 | 345 | ( function() { 346 | if ( !defined.document ) { 347 | return; 348 | } 349 | 350 | // `onErrorFnPrev` initialized at top of scope 351 | // Preserve other handlers 352 | var onErrorFnPrev = window.onerror; 353 | 354 | // Cover uncaught exceptions 355 | // Returning true will suppress the default browser handler, 356 | // returning false will let it run. 357 | window.onerror = function( error, filePath, linerNr ) { 358 | var ret = false; 359 | if ( onErrorFnPrev ) { 360 | ret = onErrorFnPrev( error, filePath, linerNr ); 361 | } 362 | 363 | // Treat return value as window.onerror itself does, 364 | // Only do our handling if not suppressed. 365 | if ( ret !== true ) { 366 | if ( QUnit.config.current ) { 367 | if ( QUnit.config.current.ignoreGlobalErrors ) { 368 | return true; 369 | } 370 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 371 | } else { 372 | QUnit.test( "global failure", extend( function() { 373 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 374 | }, { validTest: true } ) ); 375 | } 376 | return false; 377 | } 378 | 379 | return ret; 380 | }; 381 | }() ); 382 | 383 | // Figure out if we're running the tests from a server or not 384 | QUnit.isLocal = !( defined.document && window.location.protocol !== "file:" ); 385 | 386 | // Expose the current QUnit version 387 | QUnit.version = "1.23.0"; 388 | 389 | extend( QUnit, { 390 | 391 | // Call on start of module test to prepend name to all tests 392 | module: function( name, testEnvironment, executeNow ) { 393 | var module, moduleFns; 394 | var currentModule = config.currentModule; 395 | 396 | if ( arguments.length === 2 ) { 397 | if ( testEnvironment instanceof Function ) { 398 | executeNow = testEnvironment; 399 | testEnvironment = undefined; 400 | } 401 | } 402 | 403 | // DEPRECATED: handles setup/teardown functions, 404 | // beforeEach and afterEach should be used instead 405 | if ( testEnvironment && testEnvironment.setup ) { 406 | testEnvironment.beforeEach = testEnvironment.setup; 407 | delete testEnvironment.setup; 408 | } 409 | if ( testEnvironment && testEnvironment.teardown ) { 410 | testEnvironment.afterEach = testEnvironment.teardown; 411 | delete testEnvironment.teardown; 412 | } 413 | 414 | module = createModule(); 415 | 416 | moduleFns = { 417 | beforeEach: setHook( module, "beforeEach" ), 418 | afterEach: setHook( module, "afterEach" ) 419 | }; 420 | 421 | if ( executeNow instanceof Function ) { 422 | config.moduleStack.push( module ); 423 | setCurrentModule( module ); 424 | executeNow.call( module.testEnvironment, moduleFns ); 425 | config.moduleStack.pop(); 426 | module = module.parentModule || currentModule; 427 | } 428 | 429 | setCurrentModule( module ); 430 | 431 | function createModule() { 432 | var parentModule = config.moduleStack.length ? 433 | config.moduleStack.slice( -1 )[ 0 ] : null; 434 | var moduleName = parentModule !== null ? 435 | [ parentModule.name, name ].join( " > " ) : name; 436 | var module = { 437 | name: moduleName, 438 | parentModule: parentModule, 439 | tests: [], 440 | moduleId: generateHash( moduleName ) 441 | }; 442 | 443 | var env = {}; 444 | if ( parentModule ) { 445 | extend( env, parentModule.testEnvironment ); 446 | delete env.beforeEach; 447 | delete env.afterEach; 448 | } 449 | extend( env, testEnvironment ); 450 | module.testEnvironment = env; 451 | 452 | config.modules.push( module ); 453 | return module; 454 | } 455 | 456 | function setCurrentModule( module ) { 457 | config.currentModule = module; 458 | } 459 | 460 | }, 461 | 462 | // DEPRECATED: QUnit.asyncTest() will be removed in QUnit 2.0. 463 | asyncTest: asyncTest, 464 | 465 | test: test, 466 | 467 | skip: skip, 468 | 469 | only: only, 470 | 471 | // DEPRECATED: The functionality of QUnit.start() will be altered in QUnit 2.0. 472 | // In QUnit 2.0, invoking it will ONLY affect the `QUnit.config.autostart` blocking behavior. 473 | start: function( count ) { 474 | var globalStartAlreadyCalled = globalStartCalled; 475 | 476 | if ( !config.current ) { 477 | globalStartCalled = true; 478 | 479 | if ( runStarted ) { 480 | throw new Error( "Called start() outside of a test context while already started" ); 481 | } else if ( globalStartAlreadyCalled || count > 1 ) { 482 | throw new Error( "Called start() outside of a test context too many times" ); 483 | } else if ( config.autostart ) { 484 | throw new Error( "Called start() outside of a test context when " + 485 | "QUnit.config.autostart was true" ); 486 | } else if ( !config.pageLoaded ) { 487 | 488 | // The page isn't completely loaded yet, so bail out and let `QUnit.load` handle it 489 | config.autostart = true; 490 | return; 491 | } 492 | } else { 493 | 494 | // If a test is running, adjust its semaphore 495 | config.current.semaphore -= count || 1; 496 | 497 | // If semaphore is non-numeric, throw error 498 | if ( isNaN( config.current.semaphore ) ) { 499 | config.current.semaphore = 0; 500 | 501 | QUnit.pushFailure( 502 | "Called start() with a non-numeric decrement.", 503 | sourceFromStacktrace( 2 ) 504 | ); 505 | return; 506 | } 507 | 508 | // Don't start until equal number of stop-calls 509 | if ( config.current.semaphore > 0 ) { 510 | return; 511 | } 512 | 513 | // Throw an Error if start is called more often than stop 514 | if ( config.current.semaphore < 0 ) { 515 | config.current.semaphore = 0; 516 | 517 | QUnit.pushFailure( 518 | "Called start() while already started (test's semaphore was 0 already)", 519 | sourceFromStacktrace( 2 ) 520 | ); 521 | return; 522 | } 523 | } 524 | 525 | resumeProcessing(); 526 | }, 527 | 528 | // DEPRECATED: QUnit.stop() will be removed in QUnit 2.0. 529 | stop: function( count ) { 530 | 531 | // If there isn't a test running, don't allow QUnit.stop() to be called 532 | if ( !config.current ) { 533 | throw new Error( "Called stop() outside of a test context" ); 534 | } 535 | 536 | // If a test is running, adjust its semaphore 537 | config.current.semaphore += count || 1; 538 | 539 | pauseProcessing(); 540 | }, 541 | 542 | config: config, 543 | 544 | is: is, 545 | 546 | objectType: objectType, 547 | 548 | extend: extend, 549 | 550 | load: function() { 551 | config.pageLoaded = true; 552 | 553 | // Initialize the configuration options 554 | extend( config, { 555 | stats: { all: 0, bad: 0 }, 556 | moduleStats: { all: 0, bad: 0 }, 557 | started: 0, 558 | updateRate: 1000, 559 | autostart: true, 560 | filter: "" 561 | }, true ); 562 | 563 | config.blocking = false; 564 | 565 | if ( config.autostart ) { 566 | resumeProcessing(); 567 | } 568 | }, 569 | 570 | stack: function( offset ) { 571 | offset = ( offset || 0 ) + 2; 572 | return sourceFromStacktrace( offset ); 573 | } 574 | } ); 575 | 576 | registerLoggingCallbacks( QUnit ); 577 | 578 | function begin() { 579 | var i, l, 580 | modulesLog = []; 581 | 582 | // If the test run hasn't officially begun yet 583 | if ( !config.started ) { 584 | 585 | // Record the time of the test run's beginning 586 | config.started = now(); 587 | 588 | verifyLoggingCallbacks(); 589 | 590 | // Delete the loose unnamed module if unused. 591 | if ( config.modules[ 0 ].name === "" && config.modules[ 0 ].tests.length === 0 ) { 592 | config.modules.shift(); 593 | } 594 | 595 | // Avoid unnecessary information by not logging modules' test environments 596 | for ( i = 0, l = config.modules.length; i < l; i++ ) { 597 | modulesLog.push( { 598 | name: config.modules[ i ].name, 599 | tests: config.modules[ i ].tests 600 | } ); 601 | } 602 | 603 | // The test run is officially beginning now 604 | runLoggingCallbacks( "begin", { 605 | totalTests: Test.count, 606 | modules: modulesLog 607 | } ); 608 | } 609 | 610 | config.blocking = false; 611 | process( true ); 612 | } 613 | 614 | function process( last ) { 615 | function next() { 616 | process( last ); 617 | } 618 | var start = now(); 619 | config.depth = ( config.depth || 0 ) + 1; 620 | 621 | while ( config.queue.length && !config.blocking ) { 622 | if ( !defined.setTimeout || config.updateRate <= 0 || 623 | ( ( now() - start ) < config.updateRate ) ) { 624 | if ( config.current ) { 625 | 626 | // Reset async tracking for each phase of the Test lifecycle 627 | config.current.usedAsync = false; 628 | } 629 | config.queue.shift()(); 630 | } else { 631 | setTimeout( next, 13 ); 632 | break; 633 | } 634 | } 635 | config.depth--; 636 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 637 | done(); 638 | } 639 | } 640 | 641 | function pauseProcessing() { 642 | config.blocking = true; 643 | 644 | if ( config.testTimeout && defined.setTimeout ) { 645 | clearTimeout( config.timeout ); 646 | config.timeout = setTimeout( function() { 647 | if ( config.current ) { 648 | config.current.semaphore = 0; 649 | QUnit.pushFailure( "Test timed out", sourceFromStacktrace( 2 ) ); 650 | } else { 651 | throw new Error( "Test timed out" ); 652 | } 653 | resumeProcessing(); 654 | }, config.testTimeout ); 655 | } 656 | } 657 | 658 | function resumeProcessing() { 659 | runStarted = true; 660 | 661 | // A slight delay to allow this iteration of the event loop to finish (more assertions, etc.) 662 | if ( defined.setTimeout ) { 663 | setTimeout( function() { 664 | if ( config.current && config.current.semaphore > 0 ) { 665 | return; 666 | } 667 | if ( config.timeout ) { 668 | clearTimeout( config.timeout ); 669 | } 670 | 671 | begin(); 672 | }, 13 ); 673 | } else { 674 | begin(); 675 | } 676 | } 677 | 678 | function done() { 679 | var runtime, passed; 680 | 681 | config.autorun = true; 682 | 683 | // Log the last module results 684 | if ( config.previousModule ) { 685 | runLoggingCallbacks( "moduleDone", { 686 | name: config.previousModule.name, 687 | tests: config.previousModule.tests, 688 | failed: config.moduleStats.bad, 689 | passed: config.moduleStats.all - config.moduleStats.bad, 690 | total: config.moduleStats.all, 691 | runtime: now() - config.moduleStats.started 692 | } ); 693 | } 694 | delete config.previousModule; 695 | 696 | runtime = now() - config.started; 697 | passed = config.stats.all - config.stats.bad; 698 | 699 | runLoggingCallbacks( "done", { 700 | failed: config.stats.bad, 701 | passed: passed, 702 | total: config.stats.all, 703 | runtime: runtime 704 | } ); 705 | } 706 | 707 | function setHook( module, hookName ) { 708 | if ( module.testEnvironment === undefined ) { 709 | module.testEnvironment = {}; 710 | } 711 | 712 | return function( callback ) { 713 | module.testEnvironment[ hookName ] = callback; 714 | }; 715 | } 716 | 717 | var focused = false; 718 | var priorityCount = 0; 719 | var unitSampler; 720 | 721 | function Test( settings ) { 722 | var i, l; 723 | 724 | ++Test.count; 725 | 726 | extend( this, settings ); 727 | this.assertions = []; 728 | this.semaphore = 0; 729 | this.usedAsync = false; 730 | this.module = config.currentModule; 731 | this.stack = sourceFromStacktrace( 3 ); 732 | 733 | // Register unique strings 734 | for ( i = 0, l = this.module.tests; i < l.length; i++ ) { 735 | if ( this.module.tests[ i ].name === this.testName ) { 736 | this.testName += " "; 737 | } 738 | } 739 | 740 | this.testId = generateHash( this.module.name, this.testName ); 741 | 742 | this.module.tests.push( { 743 | name: this.testName, 744 | testId: this.testId 745 | } ); 746 | 747 | if ( settings.skip ) { 748 | 749 | // Skipped tests will fully ignore any sent callback 750 | this.callback = function() {}; 751 | this.async = false; 752 | this.expected = 0; 753 | } else { 754 | this.assert = new Assert( this ); 755 | } 756 | } 757 | 758 | Test.count = 0; 759 | 760 | Test.prototype = { 761 | before: function() { 762 | if ( 763 | 764 | // Emit moduleStart when we're switching from one module to another 765 | this.module !== config.previousModule || 766 | 767 | // They could be equal (both undefined) but if the previousModule property doesn't 768 | // yet exist it means this is the first test in a suite that isn't wrapped in a 769 | // module, in which case we'll just emit a moduleStart event for 'undefined'. 770 | // Without this, reporters can get testStart before moduleStart which is a problem. 771 | !hasOwn.call( config, "previousModule" ) 772 | ) { 773 | if ( hasOwn.call( config, "previousModule" ) ) { 774 | runLoggingCallbacks( "moduleDone", { 775 | name: config.previousModule.name, 776 | tests: config.previousModule.tests, 777 | failed: config.moduleStats.bad, 778 | passed: config.moduleStats.all - config.moduleStats.bad, 779 | total: config.moduleStats.all, 780 | runtime: now() - config.moduleStats.started 781 | } ); 782 | } 783 | config.previousModule = this.module; 784 | config.moduleStats = { all: 0, bad: 0, started: now() }; 785 | runLoggingCallbacks( "moduleStart", { 786 | name: this.module.name, 787 | tests: this.module.tests 788 | } ); 789 | } 790 | 791 | config.current = this; 792 | 793 | if ( this.module.testEnvironment ) { 794 | delete this.module.testEnvironment.beforeEach; 795 | delete this.module.testEnvironment.afterEach; 796 | } 797 | this.testEnvironment = extend( {}, this.module.testEnvironment ); 798 | 799 | this.started = now(); 800 | runLoggingCallbacks( "testStart", { 801 | name: this.testName, 802 | module: this.module.name, 803 | testId: this.testId 804 | } ); 805 | 806 | if ( !config.pollution ) { 807 | saveGlobal(); 808 | } 809 | }, 810 | 811 | run: function() { 812 | var promise; 813 | 814 | config.current = this; 815 | 816 | if ( this.async ) { 817 | QUnit.stop(); 818 | } 819 | 820 | this.callbackStarted = now(); 821 | 822 | if ( config.notrycatch ) { 823 | runTest( this ); 824 | return; 825 | } 826 | 827 | try { 828 | runTest( this ); 829 | } catch ( e ) { 830 | this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " + 831 | this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); 832 | 833 | // Else next test will carry the responsibility 834 | saveGlobal(); 835 | 836 | // Restart the tests if they're blocking 837 | if ( config.blocking ) { 838 | QUnit.start(); 839 | } 840 | } 841 | 842 | function runTest( test ) { 843 | promise = test.callback.call( test.testEnvironment, test.assert ); 844 | test.resolvePromise( promise ); 845 | } 846 | }, 847 | 848 | after: function() { 849 | checkPollution(); 850 | }, 851 | 852 | queueHook: function( hook, hookName ) { 853 | var promise, 854 | test = this; 855 | return function runHook() { 856 | config.current = test; 857 | if ( config.notrycatch ) { 858 | callHook(); 859 | return; 860 | } 861 | try { 862 | callHook(); 863 | } catch ( error ) { 864 | test.pushFailure( hookName + " failed on " + test.testName + ": " + 865 | ( error.message || error ), extractStacktrace( error, 0 ) ); 866 | } 867 | 868 | function callHook() { 869 | promise = hook.call( test.testEnvironment, test.assert ); 870 | test.resolvePromise( promise, hookName ); 871 | } 872 | }; 873 | }, 874 | 875 | // Currently only used for module level hooks, can be used to add global level ones 876 | hooks: function( handler ) { 877 | var hooks = []; 878 | 879 | function processHooks( test, module ) { 880 | if ( module.parentModule ) { 881 | processHooks( test, module.parentModule ); 882 | } 883 | if ( module.testEnvironment && 884 | QUnit.objectType( module.testEnvironment[ handler ] ) === "function" ) { 885 | hooks.push( test.queueHook( module.testEnvironment[ handler ], handler ) ); 886 | } 887 | } 888 | 889 | // Hooks are ignored on skipped tests 890 | if ( !this.skip ) { 891 | processHooks( this, this.module ); 892 | } 893 | return hooks; 894 | }, 895 | 896 | finish: function() { 897 | config.current = this; 898 | if ( config.requireExpects && this.expected === null ) { 899 | this.pushFailure( "Expected number of assertions to be defined, but expect() was " + 900 | "not called.", this.stack ); 901 | } else if ( this.expected !== null && this.expected !== this.assertions.length ) { 902 | this.pushFailure( "Expected " + this.expected + " assertions, but " + 903 | this.assertions.length + " were run", this.stack ); 904 | } else if ( this.expected === null && !this.assertions.length ) { 905 | this.pushFailure( "Expected at least one assertion, but none were run - call " + 906 | "expect(0) to accept zero assertions.", this.stack ); 907 | } 908 | 909 | var i, 910 | bad = 0; 911 | 912 | this.runtime = now() - this.started; 913 | config.stats.all += this.assertions.length; 914 | config.moduleStats.all += this.assertions.length; 915 | 916 | for ( i = 0; i < this.assertions.length; i++ ) { 917 | if ( !this.assertions[ i ].result ) { 918 | bad++; 919 | config.stats.bad++; 920 | config.moduleStats.bad++; 921 | } 922 | } 923 | 924 | runLoggingCallbacks( "testDone", { 925 | name: this.testName, 926 | module: this.module.name, 927 | skipped: !!this.skip, 928 | failed: bad, 929 | passed: this.assertions.length - bad, 930 | total: this.assertions.length, 931 | runtime: this.runtime, 932 | 933 | // HTML Reporter use 934 | assertions: this.assertions, 935 | testId: this.testId, 936 | 937 | // Source of Test 938 | source: this.stack, 939 | 940 | // DEPRECATED: this property will be removed in 2.0.0, use runtime instead 941 | duration: this.runtime 942 | } ); 943 | 944 | // QUnit.reset() is deprecated and will be replaced for a new 945 | // fixture reset function on QUnit 2.0/2.1. 946 | // It's still called here for backwards compatibility handling 947 | QUnit.reset(); 948 | 949 | config.current = undefined; 950 | }, 951 | 952 | queue: function() { 953 | var priority, 954 | test = this; 955 | 956 | if ( !this.valid() ) { 957 | return; 958 | } 959 | 960 | function run() { 961 | 962 | // Each of these can by async 963 | synchronize( [ 964 | function() { 965 | test.before(); 966 | }, 967 | 968 | test.hooks( "beforeEach" ), 969 | function() { 970 | test.run(); 971 | }, 972 | 973 | test.hooks( "afterEach" ).reverse(), 974 | 975 | function() { 976 | test.after(); 977 | }, 978 | function() { 979 | test.finish(); 980 | } 981 | ] ); 982 | } 983 | 984 | // Prioritize previously failed tests, detected from sessionStorage 985 | priority = QUnit.config.reorder && defined.sessionStorage && 986 | +sessionStorage.getItem( "qunit-test-" + this.module.name + "-" + this.testName ); 987 | 988 | return synchronize( run, priority, config.seed ); 989 | }, 990 | 991 | pushResult: function( resultInfo ) { 992 | 993 | // Destructure of resultInfo = { result, actual, expected, message, negative } 994 | var source, 995 | details = { 996 | module: this.module.name, 997 | name: this.testName, 998 | result: resultInfo.result, 999 | message: resultInfo.message, 1000 | actual: resultInfo.actual, 1001 | expected: resultInfo.expected, 1002 | testId: this.testId, 1003 | negative: resultInfo.negative || false, 1004 | runtime: now() - this.started 1005 | }; 1006 | 1007 | if ( !resultInfo.result ) { 1008 | source = sourceFromStacktrace(); 1009 | 1010 | if ( source ) { 1011 | details.source = source; 1012 | } 1013 | } 1014 | 1015 | runLoggingCallbacks( "log", details ); 1016 | 1017 | this.assertions.push( { 1018 | result: !!resultInfo.result, 1019 | message: resultInfo.message 1020 | } ); 1021 | }, 1022 | 1023 | pushFailure: function( message, source, actual ) { 1024 | if ( !( this instanceof Test ) ) { 1025 | throw new Error( "pushFailure() assertion outside test context, was " + 1026 | sourceFromStacktrace( 2 ) ); 1027 | } 1028 | 1029 | var details = { 1030 | module: this.module.name, 1031 | name: this.testName, 1032 | result: false, 1033 | message: message || "error", 1034 | actual: actual || null, 1035 | testId: this.testId, 1036 | runtime: now() - this.started 1037 | }; 1038 | 1039 | if ( source ) { 1040 | details.source = source; 1041 | } 1042 | 1043 | runLoggingCallbacks( "log", details ); 1044 | 1045 | this.assertions.push( { 1046 | result: false, 1047 | message: message 1048 | } ); 1049 | }, 1050 | 1051 | resolvePromise: function( promise, phase ) { 1052 | var then, message, 1053 | test = this; 1054 | if ( promise != null ) { 1055 | then = promise.then; 1056 | if ( QUnit.objectType( then ) === "function" ) { 1057 | QUnit.stop(); 1058 | then.call( 1059 | promise, 1060 | function() { QUnit.start(); }, 1061 | function( error ) { 1062 | message = "Promise rejected " + 1063 | ( !phase ? "during" : phase.replace( /Each$/, "" ) ) + 1064 | " " + test.testName + ": " + ( error.message || error ); 1065 | test.pushFailure( message, extractStacktrace( error, 0 ) ); 1066 | 1067 | // Else next test will carry the responsibility 1068 | saveGlobal(); 1069 | 1070 | // Unblock 1071 | QUnit.start(); 1072 | } 1073 | ); 1074 | } 1075 | } 1076 | }, 1077 | 1078 | valid: function() { 1079 | var filter = config.filter, 1080 | regexFilter = /^(!?)\/([\w\W]*)\/(i?$)/.exec( filter ), 1081 | module = config.module && config.module.toLowerCase(), 1082 | fullName = ( this.module.name + ": " + this.testName ); 1083 | 1084 | function moduleChainNameMatch( testModule ) { 1085 | var testModuleName = testModule.name ? testModule.name.toLowerCase() : null; 1086 | if ( testModuleName === module ) { 1087 | return true; 1088 | } else if ( testModule.parentModule ) { 1089 | return moduleChainNameMatch( testModule.parentModule ); 1090 | } else { 1091 | return false; 1092 | } 1093 | } 1094 | 1095 | function moduleChainIdMatch( testModule ) { 1096 | return inArray( testModule.moduleId, config.moduleId ) > -1 || 1097 | testModule.parentModule && moduleChainIdMatch( testModule.parentModule ); 1098 | } 1099 | 1100 | // Internally-generated tests are always valid 1101 | if ( this.callback && this.callback.validTest ) { 1102 | return true; 1103 | } 1104 | 1105 | if ( config.moduleId && config.moduleId.length > 0 && 1106 | !moduleChainIdMatch( this.module ) ) { 1107 | 1108 | return false; 1109 | } 1110 | 1111 | if ( config.testId && config.testId.length > 0 && 1112 | inArray( this.testId, config.testId ) < 0 ) { 1113 | 1114 | return false; 1115 | } 1116 | 1117 | if ( module && !moduleChainNameMatch( this.module ) ) { 1118 | return false; 1119 | } 1120 | 1121 | if ( !filter ) { 1122 | return true; 1123 | } 1124 | 1125 | return regexFilter ? 1126 | this.regexFilter( !!regexFilter[ 1 ], regexFilter[ 2 ], regexFilter[ 3 ], fullName ) : 1127 | this.stringFilter( filter, fullName ); 1128 | }, 1129 | 1130 | regexFilter: function( exclude, pattern, flags, fullName ) { 1131 | var regex = new RegExp( pattern, flags ); 1132 | var match = regex.test( fullName ); 1133 | 1134 | return match !== exclude; 1135 | }, 1136 | 1137 | stringFilter: function( filter, fullName ) { 1138 | filter = filter.toLowerCase(); 1139 | fullName = fullName.toLowerCase(); 1140 | 1141 | var include = filter.charAt( 0 ) !== "!"; 1142 | if ( !include ) { 1143 | filter = filter.slice( 1 ); 1144 | } 1145 | 1146 | // If the filter matches, we need to honour include 1147 | if ( fullName.indexOf( filter ) !== -1 ) { 1148 | return include; 1149 | } 1150 | 1151 | // Otherwise, do the opposite 1152 | return !include; 1153 | } 1154 | }; 1155 | 1156 | // Resets the test setup. Useful for tests that modify the DOM. 1157 | /* 1158 | DEPRECATED: Use multiple tests instead of resetting inside a test. 1159 | Use testStart or testDone for custom cleanup. 1160 | This method will throw an error in 2.0, and will be removed in 2.1 1161 | */ 1162 | QUnit.reset = function() { 1163 | 1164 | // Return on non-browser environments 1165 | // This is necessary to not break on node tests 1166 | if ( !defined.document ) { 1167 | return; 1168 | } 1169 | 1170 | var fixture = defined.document && document.getElementById && 1171 | document.getElementById( "qunit-fixture" ); 1172 | 1173 | if ( fixture ) { 1174 | fixture.innerHTML = config.fixture; 1175 | } 1176 | }; 1177 | 1178 | QUnit.pushFailure = function() { 1179 | if ( !QUnit.config.current ) { 1180 | throw new Error( "pushFailure() assertion outside test context, in " + 1181 | sourceFromStacktrace( 2 ) ); 1182 | } 1183 | 1184 | // Gets current test obj 1185 | var currentTest = QUnit.config.current; 1186 | 1187 | return currentTest.pushFailure.apply( currentTest, arguments ); 1188 | }; 1189 | 1190 | // Based on Java's String.hashCode, a simple but not 1191 | // rigorously collision resistant hashing function 1192 | function generateHash( module, testName ) { 1193 | var hex, 1194 | i = 0, 1195 | hash = 0, 1196 | str = module + "\x1C" + testName, 1197 | len = str.length; 1198 | 1199 | for ( ; i < len; i++ ) { 1200 | hash = ( ( hash << 5 ) - hash ) + str.charCodeAt( i ); 1201 | hash |= 0; 1202 | } 1203 | 1204 | // Convert the possibly negative integer hash code into an 8 character hex string, which isn't 1205 | // strictly necessary but increases user understanding that the id is a SHA-like hash 1206 | hex = ( 0x100000000 + hash ).toString( 16 ); 1207 | if ( hex.length < 8 ) { 1208 | hex = "0000000" + hex; 1209 | } 1210 | 1211 | return hex.slice( -8 ); 1212 | } 1213 | 1214 | function synchronize( callback, priority, seed ) { 1215 | var last = !priority, 1216 | index; 1217 | 1218 | if ( QUnit.objectType( callback ) === "array" ) { 1219 | while ( callback.length ) { 1220 | synchronize( callback.shift() ); 1221 | } 1222 | return; 1223 | } 1224 | 1225 | if ( priority ) { 1226 | config.queue.splice( priorityCount++, 0, callback ); 1227 | } else if ( seed ) { 1228 | if ( !unitSampler ) { 1229 | unitSampler = unitSamplerGenerator( seed ); 1230 | } 1231 | 1232 | // Insert into a random position after all priority items 1233 | index = Math.floor( unitSampler() * ( config.queue.length - priorityCount + 1 ) ); 1234 | config.queue.splice( priorityCount + index, 0, callback ); 1235 | } else { 1236 | config.queue.push( callback ); 1237 | } 1238 | 1239 | if ( config.autorun && !config.blocking ) { 1240 | process( last ); 1241 | } 1242 | } 1243 | 1244 | function unitSamplerGenerator( seed ) { 1245 | 1246 | // 32-bit xorshift, requires only a nonzero seed 1247 | // http://excamera.com/sphinx/article-xorshift.html 1248 | var sample = parseInt( generateHash( seed ), 16 ) || -1; 1249 | return function() { 1250 | sample ^= sample << 13; 1251 | sample ^= sample >>> 17; 1252 | sample ^= sample << 5; 1253 | 1254 | // ECMAScript has no unsigned number type 1255 | if ( sample < 0 ) { 1256 | sample += 0x100000000; 1257 | } 1258 | 1259 | return sample / 0x100000000; 1260 | }; 1261 | } 1262 | 1263 | function saveGlobal() { 1264 | config.pollution = []; 1265 | 1266 | if ( config.noglobals ) { 1267 | for ( var key in global ) { 1268 | if ( hasOwn.call( global, key ) ) { 1269 | 1270 | // In Opera sometimes DOM element ids show up here, ignore them 1271 | if ( /^qunit-test-output/.test( key ) ) { 1272 | continue; 1273 | } 1274 | config.pollution.push( key ); 1275 | } 1276 | } 1277 | } 1278 | } 1279 | 1280 | function checkPollution() { 1281 | var newGlobals, 1282 | deletedGlobals, 1283 | old = config.pollution; 1284 | 1285 | saveGlobal(); 1286 | 1287 | newGlobals = diff( config.pollution, old ); 1288 | if ( newGlobals.length > 0 ) { 1289 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) ); 1290 | } 1291 | 1292 | deletedGlobals = diff( old, config.pollution ); 1293 | if ( deletedGlobals.length > 0 ) { 1294 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join( ", " ) ); 1295 | } 1296 | } 1297 | 1298 | // Will be exposed as QUnit.asyncTest 1299 | function asyncTest( testName, expected, callback ) { 1300 | if ( arguments.length === 2 ) { 1301 | callback = expected; 1302 | expected = null; 1303 | } 1304 | 1305 | QUnit.test( testName, expected, callback, true ); 1306 | } 1307 | 1308 | // Will be exposed as QUnit.test 1309 | function test( testName, expected, callback, async ) { 1310 | if ( focused ) { return; } 1311 | 1312 | var newTest; 1313 | 1314 | if ( arguments.length === 2 ) { 1315 | callback = expected; 1316 | expected = null; 1317 | } 1318 | 1319 | newTest = new Test( { 1320 | testName: testName, 1321 | expected: expected, 1322 | async: async, 1323 | callback: callback 1324 | } ); 1325 | 1326 | newTest.queue(); 1327 | } 1328 | 1329 | // Will be exposed as QUnit.skip 1330 | function skip( testName ) { 1331 | if ( focused ) { return; } 1332 | 1333 | var test = new Test( { 1334 | testName: testName, 1335 | skip: true 1336 | } ); 1337 | 1338 | test.queue(); 1339 | } 1340 | 1341 | // Will be exposed as QUnit.only 1342 | function only( testName, expected, callback, async ) { 1343 | var newTest; 1344 | 1345 | if ( focused ) { return; } 1346 | 1347 | QUnit.config.queue.length = 0; 1348 | focused = true; 1349 | 1350 | if ( arguments.length === 2 ) { 1351 | callback = expected; 1352 | expected = null; 1353 | } 1354 | 1355 | newTest = new Test( { 1356 | testName: testName, 1357 | expected: expected, 1358 | async: async, 1359 | callback: callback 1360 | } ); 1361 | 1362 | newTest.queue(); 1363 | } 1364 | 1365 | function Assert( testContext ) { 1366 | this.test = testContext; 1367 | } 1368 | 1369 | // Assert helpers 1370 | QUnit.assert = Assert.prototype = { 1371 | 1372 | // Specify the number of expected assertions to guarantee that failed test 1373 | // (no assertions are run at all) don't slip through. 1374 | expect: function( asserts ) { 1375 | if ( arguments.length === 1 ) { 1376 | this.test.expected = asserts; 1377 | } else { 1378 | return this.test.expected; 1379 | } 1380 | }, 1381 | 1382 | // Increment this Test's semaphore counter, then return a function that 1383 | // decrements that counter a maximum of once. 1384 | async: function( count ) { 1385 | var test = this.test, 1386 | popped = false, 1387 | acceptCallCount = count; 1388 | 1389 | if ( typeof acceptCallCount === "undefined" ) { 1390 | acceptCallCount = 1; 1391 | } 1392 | 1393 | test.semaphore += 1; 1394 | test.usedAsync = true; 1395 | pauseProcessing(); 1396 | 1397 | return function done() { 1398 | 1399 | if ( popped ) { 1400 | test.pushFailure( "Too many calls to the `assert.async` callback", 1401 | sourceFromStacktrace( 2 ) ); 1402 | return; 1403 | } 1404 | acceptCallCount -= 1; 1405 | if ( acceptCallCount > 0 ) { 1406 | return; 1407 | } 1408 | 1409 | test.semaphore -= 1; 1410 | popped = true; 1411 | resumeProcessing(); 1412 | }; 1413 | }, 1414 | 1415 | // Exports test.push() to the user API 1416 | // Alias of pushResult. 1417 | push: function( result, actual, expected, message, negative ) { 1418 | var currentAssert = this instanceof Assert ? this : QUnit.config.current.assert; 1419 | return currentAssert.pushResult( { 1420 | result: result, 1421 | actual: actual, 1422 | expected: expected, 1423 | message: message, 1424 | negative: negative 1425 | } ); 1426 | }, 1427 | 1428 | pushResult: function( resultInfo ) { 1429 | 1430 | // Destructure of resultInfo = { result, actual, expected, message, negative } 1431 | var assert = this, 1432 | currentTest = ( assert instanceof Assert && assert.test ) || QUnit.config.current; 1433 | 1434 | // Backwards compatibility fix. 1435 | // Allows the direct use of global exported assertions and QUnit.assert.* 1436 | // Although, it's use is not recommended as it can leak assertions 1437 | // to other tests from async tests, because we only get a reference to the current test, 1438 | // not exactly the test where assertion were intended to be called. 1439 | if ( !currentTest ) { 1440 | throw new Error( "assertion outside test context, in " + sourceFromStacktrace( 2 ) ); 1441 | } 1442 | 1443 | if ( currentTest.usedAsync === true && currentTest.semaphore === 0 ) { 1444 | currentTest.pushFailure( "Assertion after the final `assert.async` was resolved", 1445 | sourceFromStacktrace( 2 ) ); 1446 | 1447 | // Allow this assertion to continue running anyway... 1448 | } 1449 | 1450 | if ( !( assert instanceof Assert ) ) { 1451 | assert = currentTest.assert; 1452 | } 1453 | 1454 | return assert.test.pushResult( resultInfo ); 1455 | }, 1456 | 1457 | ok: function( result, message ) { 1458 | message = message || ( result ? "okay" : "failed, expected argument to be truthy, was: " + 1459 | QUnit.dump.parse( result ) ); 1460 | this.pushResult( { 1461 | result: !!result, 1462 | actual: result, 1463 | expected: true, 1464 | message: message 1465 | } ); 1466 | }, 1467 | 1468 | notOk: function( result, message ) { 1469 | message = message || ( !result ? "okay" : "failed, expected argument to be falsy, was: " + 1470 | QUnit.dump.parse( result ) ); 1471 | this.pushResult( { 1472 | result: !result, 1473 | actual: result, 1474 | expected: false, 1475 | message: message 1476 | } ); 1477 | }, 1478 | 1479 | equal: function( actual, expected, message ) { 1480 | /*jshint eqeqeq:false */ 1481 | this.pushResult( { 1482 | result: expected == actual, 1483 | actual: actual, 1484 | expected: expected, 1485 | message: message 1486 | } ); 1487 | }, 1488 | 1489 | notEqual: function( actual, expected, message ) { 1490 | /*jshint eqeqeq:false */ 1491 | this.pushResult( { 1492 | result: expected != actual, 1493 | actual: actual, 1494 | expected: expected, 1495 | message: message, 1496 | negative: true 1497 | } ); 1498 | }, 1499 | 1500 | propEqual: function( actual, expected, message ) { 1501 | actual = objectValues( actual ); 1502 | expected = objectValues( expected ); 1503 | this.pushResult( { 1504 | result: QUnit.equiv( actual, expected ), 1505 | actual: actual, 1506 | expected: expected, 1507 | message: message 1508 | } ); 1509 | }, 1510 | 1511 | notPropEqual: function( actual, expected, message ) { 1512 | actual = objectValues( actual ); 1513 | expected = objectValues( expected ); 1514 | this.pushResult( { 1515 | result: !QUnit.equiv( actual, expected ), 1516 | actual: actual, 1517 | expected: expected, 1518 | message: message, 1519 | negative: true 1520 | } ); 1521 | }, 1522 | 1523 | deepEqual: function( actual, expected, message ) { 1524 | this.pushResult( { 1525 | result: QUnit.equiv( actual, expected ), 1526 | actual: actual, 1527 | expected: expected, 1528 | message: message 1529 | } ); 1530 | }, 1531 | 1532 | notDeepEqual: function( actual, expected, message ) { 1533 | this.pushResult( { 1534 | result: !QUnit.equiv( actual, expected ), 1535 | actual: actual, 1536 | expected: expected, 1537 | message: message, 1538 | negative: true 1539 | } ); 1540 | }, 1541 | 1542 | strictEqual: function( actual, expected, message ) { 1543 | this.pushResult( { 1544 | result: expected === actual, 1545 | actual: actual, 1546 | expected: expected, 1547 | message: message 1548 | } ); 1549 | }, 1550 | 1551 | notStrictEqual: function( actual, expected, message ) { 1552 | this.pushResult( { 1553 | result: expected !== actual, 1554 | actual: actual, 1555 | expected: expected, 1556 | message: message, 1557 | negative: true 1558 | } ); 1559 | }, 1560 | 1561 | "throws": function( block, expected, message ) { 1562 | var actual, expectedType, 1563 | expectedOutput = expected, 1564 | ok = false, 1565 | currentTest = ( this instanceof Assert && this.test ) || QUnit.config.current; 1566 | 1567 | // 'expected' is optional unless doing string comparison 1568 | if ( message == null && typeof expected === "string" ) { 1569 | message = expected; 1570 | expected = null; 1571 | } 1572 | 1573 | currentTest.ignoreGlobalErrors = true; 1574 | try { 1575 | block.call( currentTest.testEnvironment ); 1576 | } catch ( e ) { 1577 | actual = e; 1578 | } 1579 | currentTest.ignoreGlobalErrors = false; 1580 | 1581 | if ( actual ) { 1582 | expectedType = QUnit.objectType( expected ); 1583 | 1584 | // We don't want to validate thrown error 1585 | if ( !expected ) { 1586 | ok = true; 1587 | expectedOutput = null; 1588 | 1589 | // Expected is a regexp 1590 | } else if ( expectedType === "regexp" ) { 1591 | ok = expected.test( errorString( actual ) ); 1592 | 1593 | // Expected is a string 1594 | } else if ( expectedType === "string" ) { 1595 | ok = expected === errorString( actual ); 1596 | 1597 | // Expected is a constructor, maybe an Error constructor 1598 | } else if ( expectedType === "function" && actual instanceof expected ) { 1599 | ok = true; 1600 | 1601 | // Expected is an Error object 1602 | } else if ( expectedType === "object" ) { 1603 | ok = actual instanceof expected.constructor && 1604 | actual.name === expected.name && 1605 | actual.message === expected.message; 1606 | 1607 | // Expected is a validation function which returns true if validation passed 1608 | } else if ( expectedType === "function" && expected.call( {}, actual ) === true ) { 1609 | expectedOutput = null; 1610 | ok = true; 1611 | } 1612 | } 1613 | 1614 | currentTest.assert.pushResult( { 1615 | result: ok, 1616 | actual: actual, 1617 | expected: expectedOutput, 1618 | message: message 1619 | } ); 1620 | } 1621 | }; 1622 | 1623 | // Provide an alternative to assert.throws(), for environments that consider throws a reserved word 1624 | // Known to us are: Closure Compiler, Narwhal 1625 | ( function() { 1626 | /*jshint sub:true */ 1627 | Assert.prototype.raises = Assert.prototype.throws; 1628 | }() ); 1629 | 1630 | function errorString( error ) { 1631 | var name, message, 1632 | resultErrorString = error.toString(); 1633 | if ( resultErrorString.substring( 0, 7 ) === "[object" ) { 1634 | name = error.name ? error.name.toString() : "Error"; 1635 | message = error.message ? error.message.toString() : ""; 1636 | if ( name && message ) { 1637 | return name + ": " + message; 1638 | } else if ( name ) { 1639 | return name; 1640 | } else if ( message ) { 1641 | return message; 1642 | } else { 1643 | return "Error"; 1644 | } 1645 | } else { 1646 | return resultErrorString; 1647 | } 1648 | } 1649 | 1650 | // Test for equality any JavaScript type. 1651 | // Author: Philippe Rathé 1652 | QUnit.equiv = ( function() { 1653 | 1654 | // Stack to decide between skip/abort functions 1655 | var callers = []; 1656 | 1657 | // Stack to avoiding loops from circular referencing 1658 | var parents = []; 1659 | var parentsB = []; 1660 | 1661 | var getProto = Object.getPrototypeOf || function( obj ) { 1662 | 1663 | /*jshint proto: true */ 1664 | return obj.__proto__; 1665 | }; 1666 | 1667 | function useStrictEquality( b, a ) { 1668 | 1669 | // To catch short annotation VS 'new' annotation of a declaration. e.g.: 1670 | // `var i = 1;` 1671 | // `var j = new Number(1);` 1672 | if ( typeof a === "object" ) { 1673 | a = a.valueOf(); 1674 | } 1675 | if ( typeof b === "object" ) { 1676 | b = b.valueOf(); 1677 | } 1678 | 1679 | return a === b; 1680 | } 1681 | 1682 | function compareConstructors( a, b ) { 1683 | var protoA = getProto( a ); 1684 | var protoB = getProto( b ); 1685 | 1686 | // Comparing constructors is more strict than using `instanceof` 1687 | if ( a.constructor === b.constructor ) { 1688 | return true; 1689 | } 1690 | 1691 | // Ref #851 1692 | // If the obj prototype descends from a null constructor, treat it 1693 | // as a null prototype. 1694 | if ( protoA && protoA.constructor === null ) { 1695 | protoA = null; 1696 | } 1697 | if ( protoB && protoB.constructor === null ) { 1698 | protoB = null; 1699 | } 1700 | 1701 | // Allow objects with no prototype to be equivalent to 1702 | // objects with Object as their constructor. 1703 | if ( ( protoA === null && protoB === Object.prototype ) || 1704 | ( protoB === null && protoA === Object.prototype ) ) { 1705 | return true; 1706 | } 1707 | 1708 | return false; 1709 | } 1710 | 1711 | function getRegExpFlags( regexp ) { 1712 | return "flags" in regexp ? regexp.flags : regexp.toString().match( /[gimuy]*$/ )[ 0 ]; 1713 | } 1714 | 1715 | var callbacks = { 1716 | "string": useStrictEquality, 1717 | "boolean": useStrictEquality, 1718 | "number": useStrictEquality, 1719 | "null": useStrictEquality, 1720 | "undefined": useStrictEquality, 1721 | "symbol": useStrictEquality, 1722 | "date": useStrictEquality, 1723 | 1724 | "nan": function() { 1725 | return true; 1726 | }, 1727 | 1728 | "regexp": function( b, a ) { 1729 | return a.source === b.source && 1730 | 1731 | // Include flags in the comparison 1732 | getRegExpFlags( a ) === getRegExpFlags( b ); 1733 | }, 1734 | 1735 | // - skip when the property is a method of an instance (OOP) 1736 | // - abort otherwise, 1737 | // initial === would have catch identical references anyway 1738 | "function": function() { 1739 | var caller = callers[ callers.length - 1 ]; 1740 | return caller !== Object && typeof caller !== "undefined"; 1741 | }, 1742 | 1743 | "array": function( b, a ) { 1744 | var i, j, len, loop, aCircular, bCircular; 1745 | 1746 | len = a.length; 1747 | if ( len !== b.length ) { 1748 | 1749 | // Safe and faster 1750 | return false; 1751 | } 1752 | 1753 | // Track reference to avoid circular references 1754 | parents.push( a ); 1755 | parentsB.push( b ); 1756 | for ( i = 0; i < len; i++ ) { 1757 | loop = false; 1758 | for ( j = 0; j < parents.length; j++ ) { 1759 | aCircular = parents[ j ] === a[ i ]; 1760 | bCircular = parentsB[ j ] === b[ i ]; 1761 | if ( aCircular || bCircular ) { 1762 | if ( a[ i ] === b[ i ] || aCircular && bCircular ) { 1763 | loop = true; 1764 | } else { 1765 | parents.pop(); 1766 | parentsB.pop(); 1767 | return false; 1768 | } 1769 | } 1770 | } 1771 | if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { 1772 | parents.pop(); 1773 | parentsB.pop(); 1774 | return false; 1775 | } 1776 | } 1777 | parents.pop(); 1778 | parentsB.pop(); 1779 | return true; 1780 | }, 1781 | 1782 | "set": function( b, a ) { 1783 | var innerEq, 1784 | outerEq = true; 1785 | 1786 | if ( a.size !== b.size ) { 1787 | return false; 1788 | } 1789 | 1790 | a.forEach( function( aVal ) { 1791 | innerEq = false; 1792 | 1793 | b.forEach( function( bVal ) { 1794 | if ( innerEquiv( bVal, aVal ) ) { 1795 | innerEq = true; 1796 | } 1797 | } ); 1798 | 1799 | if ( !innerEq ) { 1800 | outerEq = false; 1801 | } 1802 | } ); 1803 | 1804 | return outerEq; 1805 | }, 1806 | 1807 | "map": function( b, a ) { 1808 | var innerEq, 1809 | outerEq = true; 1810 | 1811 | if ( a.size !== b.size ) { 1812 | return false; 1813 | } 1814 | 1815 | a.forEach( function( aVal, aKey ) { 1816 | innerEq = false; 1817 | 1818 | b.forEach( function( bVal, bKey ) { 1819 | if ( innerEquiv( [ bVal, bKey ], [ aVal, aKey ] ) ) { 1820 | innerEq = true; 1821 | } 1822 | } ); 1823 | 1824 | if ( !innerEq ) { 1825 | outerEq = false; 1826 | } 1827 | } ); 1828 | 1829 | return outerEq; 1830 | }, 1831 | 1832 | "object": function( b, a ) { 1833 | var i, j, loop, aCircular, bCircular; 1834 | 1835 | // Default to true 1836 | var eq = true; 1837 | var aProperties = []; 1838 | var bProperties = []; 1839 | 1840 | if ( compareConstructors( a, b ) === false ) { 1841 | return false; 1842 | } 1843 | 1844 | // Stack constructor before traversing properties 1845 | callers.push( a.constructor ); 1846 | 1847 | // Track reference to avoid circular references 1848 | parents.push( a ); 1849 | parentsB.push( b ); 1850 | 1851 | // Be strict: don't ensure hasOwnProperty and go deep 1852 | for ( i in a ) { 1853 | loop = false; 1854 | for ( j = 0; j < parents.length; j++ ) { 1855 | aCircular = parents[ j ] === a[ i ]; 1856 | bCircular = parentsB[ j ] === b[ i ]; 1857 | if ( aCircular || bCircular ) { 1858 | if ( a[ i ] === b[ i ] || aCircular && bCircular ) { 1859 | loop = true; 1860 | } else { 1861 | eq = false; 1862 | break; 1863 | } 1864 | } 1865 | } 1866 | aProperties.push( i ); 1867 | if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { 1868 | eq = false; 1869 | break; 1870 | } 1871 | } 1872 | 1873 | parents.pop(); 1874 | parentsB.pop(); 1875 | 1876 | // Unstack, we are done 1877 | callers.pop(); 1878 | 1879 | for ( i in b ) { 1880 | 1881 | // Collect b's properties 1882 | bProperties.push( i ); 1883 | } 1884 | 1885 | // Ensures identical properties name 1886 | return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); 1887 | } 1888 | }; 1889 | 1890 | function typeEquiv( a, b ) { 1891 | var type = QUnit.objectType( a ); 1892 | return QUnit.objectType( b ) === type && callbacks[ type ]( b, a ); 1893 | } 1894 | 1895 | // The real equiv function 1896 | function innerEquiv( a, b ) { 1897 | 1898 | // We're done when there's nothing more to compare 1899 | if ( arguments.length < 2 ) { 1900 | return true; 1901 | } 1902 | 1903 | // Require type-specific equality 1904 | return ( a === b || typeEquiv( a, b ) ) && 1905 | 1906 | // ...across all consecutive argument pairs 1907 | ( arguments.length === 2 || innerEquiv.apply( this, [].slice.call( arguments, 1 ) ) ); 1908 | } 1909 | 1910 | return innerEquiv; 1911 | }() ); 1912 | 1913 | // Based on jsDump by Ariel Flesler 1914 | // http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html 1915 | QUnit.dump = ( function() { 1916 | function quote( str ) { 1917 | return "\"" + str.toString().replace( /\\/g, "\\\\" ).replace( /"/g, "\\\"" ) + "\""; 1918 | } 1919 | function literal( o ) { 1920 | return o + ""; 1921 | } 1922 | function join( pre, arr, post ) { 1923 | var s = dump.separator(), 1924 | base = dump.indent(), 1925 | inner = dump.indent( 1 ); 1926 | if ( arr.join ) { 1927 | arr = arr.join( "," + s + inner ); 1928 | } 1929 | if ( !arr ) { 1930 | return pre + post; 1931 | } 1932 | return [ pre, inner + arr, base + post ].join( s ); 1933 | } 1934 | function array( arr, stack ) { 1935 | var i = arr.length, 1936 | ret = new Array( i ); 1937 | 1938 | if ( dump.maxDepth && dump.depth > dump.maxDepth ) { 1939 | return "[object Array]"; 1940 | } 1941 | 1942 | this.up(); 1943 | while ( i-- ) { 1944 | ret[ i ] = this.parse( arr[ i ], undefined, stack ); 1945 | } 1946 | this.down(); 1947 | return join( "[", ret, "]" ); 1948 | } 1949 | 1950 | var reName = /^function (\w+)/, 1951 | dump = { 1952 | 1953 | // The objType is used mostly internally, you can fix a (custom) type in advance 1954 | parse: function( obj, objType, stack ) { 1955 | stack = stack || []; 1956 | var res, parser, parserType, 1957 | inStack = inArray( obj, stack ); 1958 | 1959 | if ( inStack !== -1 ) { 1960 | return "recursion(" + ( inStack - stack.length ) + ")"; 1961 | } 1962 | 1963 | objType = objType || this.typeOf( obj ); 1964 | parser = this.parsers[ objType ]; 1965 | parserType = typeof parser; 1966 | 1967 | if ( parserType === "function" ) { 1968 | stack.push( obj ); 1969 | res = parser.call( this, obj, stack ); 1970 | stack.pop(); 1971 | return res; 1972 | } 1973 | return ( parserType === "string" ) ? parser : this.parsers.error; 1974 | }, 1975 | typeOf: function( obj ) { 1976 | var type; 1977 | if ( obj === null ) { 1978 | type = "null"; 1979 | } else if ( typeof obj === "undefined" ) { 1980 | type = "undefined"; 1981 | } else if ( QUnit.is( "regexp", obj ) ) { 1982 | type = "regexp"; 1983 | } else if ( QUnit.is( "date", obj ) ) { 1984 | type = "date"; 1985 | } else if ( QUnit.is( "function", obj ) ) { 1986 | type = "function"; 1987 | } else if ( obj.setInterval !== undefined && 1988 | obj.document !== undefined && 1989 | obj.nodeType === undefined ) { 1990 | type = "window"; 1991 | } else if ( obj.nodeType === 9 ) { 1992 | type = "document"; 1993 | } else if ( obj.nodeType ) { 1994 | type = "node"; 1995 | } else if ( 1996 | 1997 | // Native arrays 1998 | toString.call( obj ) === "[object Array]" || 1999 | 2000 | // NodeList objects 2001 | ( typeof obj.length === "number" && obj.item !== undefined && 2002 | ( obj.length ? obj.item( 0 ) === obj[ 0 ] : ( obj.item( 0 ) === null && 2003 | obj[ 0 ] === undefined ) ) ) 2004 | ) { 2005 | type = "array"; 2006 | } else if ( obj.constructor === Error.prototype.constructor ) { 2007 | type = "error"; 2008 | } else { 2009 | type = typeof obj; 2010 | } 2011 | return type; 2012 | }, 2013 | 2014 | separator: function() { 2015 | return this.multiline ? this.HTML ? "
      " : "\n" : this.HTML ? " " : " "; 2016 | }, 2017 | 2018 | // Extra can be a number, shortcut for increasing-calling-decreasing 2019 | indent: function( extra ) { 2020 | if ( !this.multiline ) { 2021 | return ""; 2022 | } 2023 | var chr = this.indentChar; 2024 | if ( this.HTML ) { 2025 | chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); 2026 | } 2027 | return new Array( this.depth + ( extra || 0 ) ).join( chr ); 2028 | }, 2029 | up: function( a ) { 2030 | this.depth += a || 1; 2031 | }, 2032 | down: function( a ) { 2033 | this.depth -= a || 1; 2034 | }, 2035 | setParser: function( name, parser ) { 2036 | this.parsers[ name ] = parser; 2037 | }, 2038 | 2039 | // The next 3 are exposed so you can use them 2040 | quote: quote, 2041 | literal: literal, 2042 | join: join, 2043 | depth: 1, 2044 | maxDepth: QUnit.config.maxDepth, 2045 | 2046 | // This is the list of parsers, to modify them, use dump.setParser 2047 | parsers: { 2048 | window: "[Window]", 2049 | document: "[Document]", 2050 | error: function( error ) { 2051 | return "Error(\"" + error.message + "\")"; 2052 | }, 2053 | unknown: "[Unknown]", 2054 | "null": "null", 2055 | "undefined": "undefined", 2056 | "function": function( fn ) { 2057 | var ret = "function", 2058 | 2059 | // Functions never have name in IE 2060 | name = "name" in fn ? fn.name : ( reName.exec( fn ) || [] )[ 1 ]; 2061 | 2062 | if ( name ) { 2063 | ret += " " + name; 2064 | } 2065 | ret += "("; 2066 | 2067 | ret = [ ret, dump.parse( fn, "functionArgs" ), "){" ].join( "" ); 2068 | return join( ret, dump.parse( fn, "functionCode" ), "}" ); 2069 | }, 2070 | array: array, 2071 | nodelist: array, 2072 | "arguments": array, 2073 | object: function( map, stack ) { 2074 | var keys, key, val, i, nonEnumerableProperties, 2075 | ret = []; 2076 | 2077 | if ( dump.maxDepth && dump.depth > dump.maxDepth ) { 2078 | return "[object Object]"; 2079 | } 2080 | 2081 | dump.up(); 2082 | keys = []; 2083 | for ( key in map ) { 2084 | keys.push( key ); 2085 | } 2086 | 2087 | // Some properties are not always enumerable on Error objects. 2088 | nonEnumerableProperties = [ "message", "name" ]; 2089 | for ( i in nonEnumerableProperties ) { 2090 | key = nonEnumerableProperties[ i ]; 2091 | if ( key in map && inArray( key, keys ) < 0 ) { 2092 | keys.push( key ); 2093 | } 2094 | } 2095 | keys.sort(); 2096 | for ( i = 0; i < keys.length; i++ ) { 2097 | key = keys[ i ]; 2098 | val = map[ key ]; 2099 | ret.push( dump.parse( key, "key" ) + ": " + 2100 | dump.parse( val, undefined, stack ) ); 2101 | } 2102 | dump.down(); 2103 | return join( "{", ret, "}" ); 2104 | }, 2105 | node: function( node ) { 2106 | var len, i, val, 2107 | open = dump.HTML ? "<" : "<", 2108 | close = dump.HTML ? ">" : ">", 2109 | tag = node.nodeName.toLowerCase(), 2110 | ret = open + tag, 2111 | attrs = node.attributes; 2112 | 2113 | if ( attrs ) { 2114 | for ( i = 0, len = attrs.length; i < len; i++ ) { 2115 | val = attrs[ i ].nodeValue; 2116 | 2117 | // IE6 includes all attributes in .attributes, even ones not explicitly 2118 | // set. Those have values like undefined, null, 0, false, "" or 2119 | // "inherit". 2120 | if ( val && val !== "inherit" ) { 2121 | ret += " " + attrs[ i ].nodeName + "=" + 2122 | dump.parse( val, "attribute" ); 2123 | } 2124 | } 2125 | } 2126 | ret += close; 2127 | 2128 | // Show content of TextNode or CDATASection 2129 | if ( node.nodeType === 3 || node.nodeType === 4 ) { 2130 | ret += node.nodeValue; 2131 | } 2132 | 2133 | return ret + open + "/" + tag + close; 2134 | }, 2135 | 2136 | // Function calls it internally, it's the arguments part of the function 2137 | functionArgs: function( fn ) { 2138 | var args, 2139 | l = fn.length; 2140 | 2141 | if ( !l ) { 2142 | return ""; 2143 | } 2144 | 2145 | args = new Array( l ); 2146 | while ( l-- ) { 2147 | 2148 | // 97 is 'a' 2149 | args[ l ] = String.fromCharCode( 97 + l ); 2150 | } 2151 | return " " + args.join( ", " ) + " "; 2152 | }, 2153 | 2154 | // Object calls it internally, the key part of an item in a map 2155 | key: quote, 2156 | 2157 | // Function calls it internally, it's the content of the function 2158 | functionCode: "[code]", 2159 | 2160 | // Node calls it internally, it's a html attribute value 2161 | attribute: quote, 2162 | string: quote, 2163 | date: quote, 2164 | regexp: literal, 2165 | number: literal, 2166 | "boolean": literal 2167 | }, 2168 | 2169 | // If true, entities are escaped ( <, >, \t, space and \n ) 2170 | HTML: false, 2171 | 2172 | // Indentation unit 2173 | indentChar: " ", 2174 | 2175 | // If true, items in a collection, are separated by a \n, else just a space. 2176 | multiline: true 2177 | }; 2178 | 2179 | return dump; 2180 | }() ); 2181 | 2182 | // Back compat 2183 | QUnit.jsDump = QUnit.dump; 2184 | 2185 | // Deprecated 2186 | // Extend assert methods to QUnit for Backwards compatibility 2187 | ( function() { 2188 | var i, 2189 | assertions = Assert.prototype; 2190 | 2191 | function applyCurrent( current ) { 2192 | return function() { 2193 | var assert = new Assert( QUnit.config.current ); 2194 | current.apply( assert, arguments ); 2195 | }; 2196 | } 2197 | 2198 | for ( i in assertions ) { 2199 | QUnit[ i ] = applyCurrent( assertions[ i ] ); 2200 | } 2201 | }() ); 2202 | 2203 | // For browser, export only select globals 2204 | if ( defined.document ) { 2205 | 2206 | ( function() { 2207 | var i, l, 2208 | keys = [ 2209 | "test", 2210 | "module", 2211 | "expect", 2212 | "asyncTest", 2213 | "start", 2214 | "stop", 2215 | "ok", 2216 | "notOk", 2217 | "equal", 2218 | "notEqual", 2219 | "propEqual", 2220 | "notPropEqual", 2221 | "deepEqual", 2222 | "notDeepEqual", 2223 | "strictEqual", 2224 | "notStrictEqual", 2225 | "throws", 2226 | "raises" 2227 | ]; 2228 | 2229 | for ( i = 0, l = keys.length; i < l; i++ ) { 2230 | window[ keys[ i ] ] = QUnit[ keys[ i ] ]; 2231 | } 2232 | }() ); 2233 | 2234 | window.QUnit = QUnit; 2235 | } 2236 | 2237 | // For nodejs 2238 | if ( typeof module !== "undefined" && module && module.exports ) { 2239 | module.exports = QUnit; 2240 | 2241 | // For consistency with CommonJS environments' exports 2242 | module.exports.QUnit = QUnit; 2243 | } 2244 | 2245 | // For CommonJS with exports, but without module.exports, like Rhino 2246 | if ( typeof exports !== "undefined" && exports ) { 2247 | exports.QUnit = QUnit; 2248 | } 2249 | 2250 | if ( typeof define === "function" && define.amd ) { 2251 | define( function() { 2252 | return QUnit; 2253 | } ); 2254 | QUnit.config.autostart = false; 2255 | } 2256 | 2257 | // Get a reference to the global object, like window in browsers 2258 | }( ( function() { 2259 | return this; 2260 | }() ) ) ); 2261 | 2262 | ( function() { 2263 | 2264 | // Only interact with URLs via window.location 2265 | var location = typeof window !== "undefined" && window.location; 2266 | if ( !location ) { 2267 | return; 2268 | } 2269 | 2270 | var urlParams = getUrlParams(); 2271 | 2272 | QUnit.urlParams = urlParams; 2273 | 2274 | // Match module/test by inclusion in an array 2275 | QUnit.config.moduleId = [].concat( urlParams.moduleId || [] ); 2276 | QUnit.config.testId = [].concat( urlParams.testId || [] ); 2277 | 2278 | // Exact case-insensitive match of the module name 2279 | QUnit.config.module = urlParams.module; 2280 | 2281 | // Regular expression or case-insenstive substring match against "moduleName: testName" 2282 | QUnit.config.filter = urlParams.filter; 2283 | 2284 | // Test order randomization 2285 | if ( urlParams.seed === true ) { 2286 | 2287 | // Generate a random seed if the option is specified without a value 2288 | QUnit.config.seed = Math.random().toString( 36 ).slice( 2 ); 2289 | } else if ( urlParams.seed ) { 2290 | QUnit.config.seed = urlParams.seed; 2291 | } 2292 | 2293 | // Add URL-parameter-mapped config values with UI form rendering data 2294 | QUnit.config.urlConfig.push( 2295 | { 2296 | id: "hidepassed", 2297 | label: "Hide passed tests", 2298 | tooltip: "Only show tests and assertions that fail. Stored as query-strings." 2299 | }, 2300 | { 2301 | id: "noglobals", 2302 | label: "Check for Globals", 2303 | tooltip: "Enabling this will test if any test introduces new properties on the " + 2304 | "global object (`window` in Browsers). Stored as query-strings." 2305 | }, 2306 | { 2307 | id: "notrycatch", 2308 | label: "No try-catch", 2309 | tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging " + 2310 | "exceptions in IE reasonable. Stored as query-strings." 2311 | } 2312 | ); 2313 | 2314 | QUnit.begin( function() { 2315 | var i, option, 2316 | urlConfig = QUnit.config.urlConfig; 2317 | 2318 | for ( i = 0; i < urlConfig.length; i++ ) { 2319 | 2320 | // Options can be either strings or objects with nonempty "id" properties 2321 | option = QUnit.config.urlConfig[ i ]; 2322 | if ( typeof option !== "string" ) { 2323 | option = option.id; 2324 | } 2325 | 2326 | if ( QUnit.config[ option ] === undefined ) { 2327 | QUnit.config[ option ] = urlParams[ option ]; 2328 | } 2329 | } 2330 | } ); 2331 | 2332 | function getUrlParams() { 2333 | var i, param, name, value; 2334 | var urlParams = {}; 2335 | var params = location.search.slice( 1 ).split( "&" ); 2336 | var length = params.length; 2337 | 2338 | for ( i = 0; i < length; i++ ) { 2339 | if ( params[ i ] ) { 2340 | param = params[ i ].split( "=" ); 2341 | name = decodeURIComponent( param[ 0 ] ); 2342 | 2343 | // Allow just a key to turn on a flag, e.g., test.html?noglobals 2344 | value = param.length === 1 || 2345 | decodeURIComponent( param.slice( 1 ).join( "=" ) ) ; 2346 | if ( urlParams[ name ] ) { 2347 | urlParams[ name ] = [].concat( urlParams[ name ], value ); 2348 | } else { 2349 | urlParams[ name ] = value; 2350 | } 2351 | } 2352 | } 2353 | 2354 | return urlParams; 2355 | } 2356 | 2357 | // Don't load the HTML Reporter on non-browser environments 2358 | if ( typeof window === "undefined" || !window.document ) { 2359 | return; 2360 | } 2361 | 2362 | // Deprecated QUnit.init - Ref #530 2363 | // Re-initialize the configuration options 2364 | QUnit.init = function() { 2365 | var config = QUnit.config; 2366 | 2367 | config.stats = { all: 0, bad: 0 }; 2368 | config.moduleStats = { all: 0, bad: 0 }; 2369 | config.started = 0; 2370 | config.updateRate = 1000; 2371 | config.blocking = false; 2372 | config.autostart = true; 2373 | config.autorun = false; 2374 | config.filter = ""; 2375 | config.queue = []; 2376 | 2377 | appendInterface(); 2378 | }; 2379 | 2380 | var config = QUnit.config, 2381 | document = window.document, 2382 | collapseNext = false, 2383 | hasOwn = Object.prototype.hasOwnProperty, 2384 | unfilteredUrl = setUrl( { filter: undefined, module: undefined, 2385 | moduleId: undefined, testId: undefined } ), 2386 | defined = { 2387 | sessionStorage: ( function() { 2388 | var x = "qunit-test-string"; 2389 | try { 2390 | sessionStorage.setItem( x, x ); 2391 | sessionStorage.removeItem( x ); 2392 | return true; 2393 | } catch ( e ) { 2394 | return false; 2395 | } 2396 | }() ) 2397 | }, 2398 | modulesList = []; 2399 | 2400 | /** 2401 | * Escape text for attribute or text content. 2402 | */ 2403 | function escapeText( s ) { 2404 | if ( !s ) { 2405 | return ""; 2406 | } 2407 | s = s + ""; 2408 | 2409 | // Both single quotes and double quotes (for attributes) 2410 | return s.replace( /['"<>&]/g, function( s ) { 2411 | switch ( s ) { 2412 | case "'": 2413 | return "'"; 2414 | case "\"": 2415 | return """; 2416 | case "<": 2417 | return "<"; 2418 | case ">": 2419 | return ">"; 2420 | case "&": 2421 | return "&"; 2422 | } 2423 | } ); 2424 | } 2425 | 2426 | /** 2427 | * @param {HTMLElement} elem 2428 | * @param {string} type 2429 | * @param {Function} fn 2430 | */ 2431 | function addEvent( elem, type, fn ) { 2432 | if ( elem.addEventListener ) { 2433 | 2434 | // Standards-based browsers 2435 | elem.addEventListener( type, fn, false ); 2436 | } else if ( elem.attachEvent ) { 2437 | 2438 | // Support: IE <9 2439 | elem.attachEvent( "on" + type, function() { 2440 | var event = window.event; 2441 | if ( !event.target ) { 2442 | event.target = event.srcElement || document; 2443 | } 2444 | 2445 | fn.call( elem, event ); 2446 | } ); 2447 | } 2448 | } 2449 | 2450 | /** 2451 | * @param {Array|NodeList} elems 2452 | * @param {string} type 2453 | * @param {Function} fn 2454 | */ 2455 | function addEvents( elems, type, fn ) { 2456 | var i = elems.length; 2457 | while ( i-- ) { 2458 | addEvent( elems[ i ], type, fn ); 2459 | } 2460 | } 2461 | 2462 | function hasClass( elem, name ) { 2463 | return ( " " + elem.className + " " ).indexOf( " " + name + " " ) >= 0; 2464 | } 2465 | 2466 | function addClass( elem, name ) { 2467 | if ( !hasClass( elem, name ) ) { 2468 | elem.className += ( elem.className ? " " : "" ) + name; 2469 | } 2470 | } 2471 | 2472 | function toggleClass( elem, name, force ) { 2473 | if ( force || typeof force === "undefined" && !hasClass( elem, name ) ) { 2474 | addClass( elem, name ); 2475 | } else { 2476 | removeClass( elem, name ); 2477 | } 2478 | } 2479 | 2480 | function removeClass( elem, name ) { 2481 | var set = " " + elem.className + " "; 2482 | 2483 | // Class name may appear multiple times 2484 | while ( set.indexOf( " " + name + " " ) >= 0 ) { 2485 | set = set.replace( " " + name + " ", " " ); 2486 | } 2487 | 2488 | // Trim for prettiness 2489 | elem.className = typeof set.trim === "function" ? set.trim() : set.replace( /^\s+|\s+$/g, "" ); 2490 | } 2491 | 2492 | function id( name ) { 2493 | return document.getElementById && document.getElementById( name ); 2494 | } 2495 | 2496 | function getUrlConfigHtml() { 2497 | var i, j, val, 2498 | escaped, escapedTooltip, 2499 | selection = false, 2500 | urlConfig = config.urlConfig, 2501 | urlConfigHtml = ""; 2502 | 2503 | for ( i = 0; i < urlConfig.length; i++ ) { 2504 | 2505 | // Options can be either strings or objects with nonempty "id" properties 2506 | val = config.urlConfig[ i ]; 2507 | if ( typeof val === "string" ) { 2508 | val = { 2509 | id: val, 2510 | label: val 2511 | }; 2512 | } 2513 | 2514 | escaped = escapeText( val.id ); 2515 | escapedTooltip = escapeText( val.tooltip ); 2516 | 2517 | if ( !val.value || typeof val.value === "string" ) { 2518 | urlConfigHtml += ""; 2524 | } else { 2525 | urlConfigHtml += ""; 2554 | } 2555 | } 2556 | 2557 | return urlConfigHtml; 2558 | } 2559 | 2560 | // Handle "click" events on toolbar checkboxes and "change" for select menus. 2561 | // Updates the URL with the new state of `config.urlConfig` values. 2562 | function toolbarChanged() { 2563 | var updatedUrl, value, tests, 2564 | field = this, 2565 | params = {}; 2566 | 2567 | // Detect if field is a select menu or a checkbox 2568 | if ( "selectedIndex" in field ) { 2569 | value = field.options[ field.selectedIndex ].value || undefined; 2570 | } else { 2571 | value = field.checked ? ( field.defaultValue || true ) : undefined; 2572 | } 2573 | 2574 | params[ field.name ] = value; 2575 | updatedUrl = setUrl( params ); 2576 | 2577 | // Check if we can apply the change without a page refresh 2578 | if ( "hidepassed" === field.name && "replaceState" in window.history ) { 2579 | QUnit.urlParams[ field.name ] = value; 2580 | config[ field.name ] = value || false; 2581 | tests = id( "qunit-tests" ); 2582 | if ( tests ) { 2583 | toggleClass( tests, "hidepass", value || false ); 2584 | } 2585 | window.history.replaceState( null, "", updatedUrl ); 2586 | } else { 2587 | window.location = updatedUrl; 2588 | } 2589 | } 2590 | 2591 | function setUrl( params ) { 2592 | var key, arrValue, i, 2593 | querystring = "?", 2594 | location = window.location; 2595 | 2596 | params = QUnit.extend( QUnit.extend( {}, QUnit.urlParams ), params ); 2597 | 2598 | for ( key in params ) { 2599 | 2600 | // Skip inherited or undefined properties 2601 | if ( hasOwn.call( params, key ) && params[ key ] !== undefined ) { 2602 | 2603 | // Output a parameter for each value of this key (but usually just one) 2604 | arrValue = [].concat( params[ key ] ); 2605 | for ( i = 0; i < arrValue.length; i++ ) { 2606 | querystring += encodeURIComponent( key ); 2607 | if ( arrValue[ i ] !== true ) { 2608 | querystring += "=" + encodeURIComponent( arrValue[ i ] ); 2609 | } 2610 | querystring += "&"; 2611 | } 2612 | } 2613 | } 2614 | return location.protocol + "//" + location.host + 2615 | location.pathname + querystring.slice( 0, -1 ); 2616 | } 2617 | 2618 | function applyUrlParams() { 2619 | var selectedModule, 2620 | modulesList = id( "qunit-modulefilter" ), 2621 | filter = id( "qunit-filter-input" ).value; 2622 | 2623 | selectedModule = modulesList ? 2624 | decodeURIComponent( modulesList.options[ modulesList.selectedIndex ].value ) : 2625 | undefined; 2626 | 2627 | window.location = setUrl( { 2628 | module: ( selectedModule === "" ) ? undefined : selectedModule, 2629 | filter: ( filter === "" ) ? undefined : filter, 2630 | 2631 | // Remove moduleId and testId filters 2632 | moduleId: undefined, 2633 | testId: undefined 2634 | } ); 2635 | } 2636 | 2637 | function toolbarUrlConfigContainer() { 2638 | var urlConfigContainer = document.createElement( "span" ); 2639 | 2640 | urlConfigContainer.innerHTML = getUrlConfigHtml(); 2641 | addClass( urlConfigContainer, "qunit-url-config" ); 2642 | 2643 | // For oldIE support: 2644 | // * Add handlers to the individual elements instead of the container 2645 | // * Use "click" instead of "change" for checkboxes 2646 | addEvents( urlConfigContainer.getElementsByTagName( "input" ), "click", toolbarChanged ); 2647 | addEvents( urlConfigContainer.getElementsByTagName( "select" ), "change", toolbarChanged ); 2648 | 2649 | return urlConfigContainer; 2650 | } 2651 | 2652 | function toolbarLooseFilter() { 2653 | var filter = document.createElement( "form" ), 2654 | label = document.createElement( "label" ), 2655 | input = document.createElement( "input" ), 2656 | button = document.createElement( "button" ); 2657 | 2658 | addClass( filter, "qunit-filter" ); 2659 | 2660 | label.innerHTML = "Filter: "; 2661 | 2662 | input.type = "text"; 2663 | input.value = config.filter || ""; 2664 | input.name = "filter"; 2665 | input.id = "qunit-filter-input"; 2666 | 2667 | button.innerHTML = "Go"; 2668 | 2669 | label.appendChild( input ); 2670 | 2671 | filter.appendChild( label ); 2672 | filter.appendChild( button ); 2673 | addEvent( filter, "submit", function( ev ) { 2674 | applyUrlParams(); 2675 | 2676 | if ( ev && ev.preventDefault ) { 2677 | ev.preventDefault(); 2678 | } 2679 | 2680 | return false; 2681 | } ); 2682 | 2683 | return filter; 2684 | } 2685 | 2686 | function toolbarModuleFilterHtml() { 2687 | var i, 2688 | moduleFilterHtml = ""; 2689 | 2690 | if ( !modulesList.length ) { 2691 | return false; 2692 | } 2693 | 2694 | moduleFilterHtml += "" + 2695 | ""; 2706 | 2707 | return moduleFilterHtml; 2708 | } 2709 | 2710 | function toolbarModuleFilter() { 2711 | var toolbar = id( "qunit-testrunner-toolbar" ), 2712 | moduleFilter = document.createElement( "span" ), 2713 | moduleFilterHtml = toolbarModuleFilterHtml(); 2714 | 2715 | if ( !toolbar || !moduleFilterHtml ) { 2716 | return false; 2717 | } 2718 | 2719 | moduleFilter.setAttribute( "id", "qunit-modulefilter-container" ); 2720 | moduleFilter.innerHTML = moduleFilterHtml; 2721 | 2722 | addEvent( moduleFilter.lastChild, "change", applyUrlParams ); 2723 | 2724 | toolbar.appendChild( moduleFilter ); 2725 | } 2726 | 2727 | function appendToolbar() { 2728 | var toolbar = id( "qunit-testrunner-toolbar" ); 2729 | 2730 | if ( toolbar ) { 2731 | toolbar.appendChild( toolbarUrlConfigContainer() ); 2732 | toolbar.appendChild( toolbarLooseFilter() ); 2733 | toolbarModuleFilter(); 2734 | } 2735 | } 2736 | 2737 | function appendHeader() { 2738 | var header = id( "qunit-header" ); 2739 | 2740 | if ( header ) { 2741 | header.innerHTML = "" + header.innerHTML + 2742 | " "; 2743 | } 2744 | } 2745 | 2746 | function appendBanner() { 2747 | var banner = id( "qunit-banner" ); 2748 | 2749 | if ( banner ) { 2750 | banner.className = ""; 2751 | } 2752 | } 2753 | 2754 | function appendTestResults() { 2755 | var tests = id( "qunit-tests" ), 2756 | result = id( "qunit-testresult" ); 2757 | 2758 | if ( result ) { 2759 | result.parentNode.removeChild( result ); 2760 | } 2761 | 2762 | if ( tests ) { 2763 | tests.innerHTML = ""; 2764 | result = document.createElement( "p" ); 2765 | result.id = "qunit-testresult"; 2766 | result.className = "result"; 2767 | tests.parentNode.insertBefore( result, tests ); 2768 | result.innerHTML = "Running...
       "; 2769 | } 2770 | } 2771 | 2772 | function storeFixture() { 2773 | var fixture = id( "qunit-fixture" ); 2774 | if ( fixture ) { 2775 | config.fixture = fixture.innerHTML; 2776 | } 2777 | } 2778 | 2779 | function appendFilteredTest() { 2780 | var testId = QUnit.config.testId; 2781 | if ( !testId || testId.length <= 0 ) { 2782 | return ""; 2783 | } 2784 | return "
      Rerunning selected tests: " + 2785 | escapeText( testId.join( ", " ) ) + 2786 | " Run all tests
      "; 2789 | } 2790 | 2791 | function appendUserAgent() { 2792 | var userAgent = id( "qunit-userAgent" ); 2793 | 2794 | if ( userAgent ) { 2795 | userAgent.innerHTML = ""; 2796 | userAgent.appendChild( 2797 | document.createTextNode( 2798 | "QUnit " + QUnit.version + "; " + navigator.userAgent 2799 | ) 2800 | ); 2801 | } 2802 | } 2803 | 2804 | function appendInterface() { 2805 | var qunit = id( "qunit" ); 2806 | 2807 | if ( qunit ) { 2808 | qunit.innerHTML = 2809 | "

      " + escapeText( document.title ) + "

      " + 2810 | "

      " + 2811 | "
      " + 2812 | appendFilteredTest() + 2813 | "

      " + 2814 | "
        "; 2815 | } 2816 | 2817 | appendHeader(); 2818 | appendBanner(); 2819 | appendTestResults(); 2820 | appendUserAgent(); 2821 | appendToolbar(); 2822 | } 2823 | 2824 | function appendTestsList( modules ) { 2825 | var i, l, x, z, test, moduleObj; 2826 | 2827 | for ( i = 0, l = modules.length; i < l; i++ ) { 2828 | moduleObj = modules[ i ]; 2829 | 2830 | for ( x = 0, z = moduleObj.tests.length; x < z; x++ ) { 2831 | test = moduleObj.tests[ x ]; 2832 | 2833 | appendTest( test.name, test.testId, moduleObj.name ); 2834 | } 2835 | } 2836 | } 2837 | 2838 | function appendTest( name, testId, moduleName ) { 2839 | var title, rerunTrigger, testBlock, assertList, 2840 | tests = id( "qunit-tests" ); 2841 | 2842 | if ( !tests ) { 2843 | return; 2844 | } 2845 | 2846 | title = document.createElement( "strong" ); 2847 | title.innerHTML = getNameHtml( name, moduleName ); 2848 | 2849 | rerunTrigger = document.createElement( "a" ); 2850 | rerunTrigger.innerHTML = "Rerun"; 2851 | rerunTrigger.href = setUrl( { testId: testId } ); 2852 | 2853 | testBlock = document.createElement( "li" ); 2854 | testBlock.appendChild( title ); 2855 | testBlock.appendChild( rerunTrigger ); 2856 | testBlock.id = "qunit-test-output-" + testId; 2857 | 2858 | assertList = document.createElement( "ol" ); 2859 | assertList.className = "qunit-assert-list"; 2860 | 2861 | testBlock.appendChild( assertList ); 2862 | 2863 | tests.appendChild( testBlock ); 2864 | } 2865 | 2866 | // HTML Reporter initialization and load 2867 | QUnit.begin( function( details ) { 2868 | var i, moduleObj, tests; 2869 | 2870 | // Sort modules by name for the picker 2871 | for ( i = 0; i < details.modules.length; i++ ) { 2872 | moduleObj = details.modules[ i ]; 2873 | if ( moduleObj.name ) { 2874 | modulesList.push( moduleObj.name ); 2875 | } 2876 | } 2877 | modulesList.sort( function( a, b ) { 2878 | return a.localeCompare( b ); 2879 | } ); 2880 | 2881 | // Capture fixture HTML from the page 2882 | storeFixture(); 2883 | 2884 | // Initialize QUnit elements 2885 | appendInterface(); 2886 | appendTestsList( details.modules ); 2887 | tests = id( "qunit-tests" ); 2888 | if ( tests && config.hidepassed ) { 2889 | addClass( tests, "hidepass" ); 2890 | } 2891 | } ); 2892 | 2893 | QUnit.done( function( details ) { 2894 | var i, key, 2895 | banner = id( "qunit-banner" ), 2896 | tests = id( "qunit-tests" ), 2897 | html = [ 2898 | "Tests completed in ", 2899 | details.runtime, 2900 | " milliseconds.
        ", 2901 | "", 2902 | details.passed, 2903 | " assertions of ", 2904 | details.total, 2905 | " passed, ", 2906 | details.failed, 2907 | " failed." 2908 | ].join( "" ); 2909 | 2910 | if ( banner ) { 2911 | banner.className = details.failed ? "qunit-fail" : "qunit-pass"; 2912 | } 2913 | 2914 | if ( tests ) { 2915 | id( "qunit-testresult" ).innerHTML = html; 2916 | } 2917 | 2918 | if ( config.altertitle && document.title ) { 2919 | 2920 | // Show ✖ for good, ✔ for bad suite result in title 2921 | // use escape sequences in case file gets loaded with non-utf-8-charset 2922 | document.title = [ 2923 | ( details.failed ? "\u2716" : "\u2714" ), 2924 | document.title.replace( /^[\u2714\u2716] /i, "" ) 2925 | ].join( " " ); 2926 | } 2927 | 2928 | // Clear own sessionStorage items if all tests passed 2929 | if ( config.reorder && defined.sessionStorage && details.failed === 0 ) { 2930 | for ( i = 0; i < sessionStorage.length; i++ ) { 2931 | key = sessionStorage.key( i++ ); 2932 | if ( key.indexOf( "qunit-test-" ) === 0 ) { 2933 | sessionStorage.removeItem( key ); 2934 | } 2935 | } 2936 | } 2937 | 2938 | // Scroll back to top to show results 2939 | if ( config.scrolltop && window.scrollTo ) { 2940 | window.scrollTo( 0, 0 ); 2941 | } 2942 | } ); 2943 | 2944 | function getNameHtml( name, module ) { 2945 | var nameHtml = ""; 2946 | 2947 | if ( module ) { 2948 | nameHtml = "" + escapeText( module ) + ": "; 2949 | } 2950 | 2951 | nameHtml += "" + escapeText( name ) + ""; 2952 | 2953 | return nameHtml; 2954 | } 2955 | 2956 | QUnit.testStart( function( details ) { 2957 | var running, testBlock, bad; 2958 | 2959 | testBlock = id( "qunit-test-output-" + details.testId ); 2960 | if ( testBlock ) { 2961 | testBlock.className = "running"; 2962 | } else { 2963 | 2964 | // Report later registered tests 2965 | appendTest( details.name, details.testId, details.module ); 2966 | } 2967 | 2968 | running = id( "qunit-testresult" ); 2969 | if ( running ) { 2970 | bad = QUnit.config.reorder && defined.sessionStorage && 2971 | +sessionStorage.getItem( "qunit-test-" + details.module + "-" + details.name ); 2972 | 2973 | running.innerHTML = ( bad ? 2974 | "Rerunning previously failed test:
        " : 2975 | "Running:
        " ) + 2976 | getNameHtml( details.name, details.module ); 2977 | } 2978 | 2979 | } ); 2980 | 2981 | function stripHtml( string ) { 2982 | 2983 | // Strip tags, html entity and whitespaces 2984 | return string.replace( /<\/?[^>]+(>|$)/g, "" ).replace( /\"/g, "" ).replace( /\s+/g, "" ); 2985 | } 2986 | 2987 | QUnit.log( function( details ) { 2988 | var assertList, assertLi, 2989 | message, expected, actual, diff, 2990 | showDiff = false, 2991 | testItem = id( "qunit-test-output-" + details.testId ); 2992 | 2993 | if ( !testItem ) { 2994 | return; 2995 | } 2996 | 2997 | message = escapeText( details.message ) || ( details.result ? "okay" : "failed" ); 2998 | message = "" + message + ""; 2999 | message += "@ " + details.runtime + " ms"; 3000 | 3001 | // The pushFailure doesn't provide details.expected 3002 | // when it calls, it's implicit to also not show expected and diff stuff 3003 | // Also, we need to check details.expected existence, as it can exist and be undefined 3004 | if ( !details.result && hasOwn.call( details, "expected" ) ) { 3005 | if ( details.negative ) { 3006 | expected = "NOT " + QUnit.dump.parse( details.expected ); 3007 | } else { 3008 | expected = QUnit.dump.parse( details.expected ); 3009 | } 3010 | 3011 | actual = QUnit.dump.parse( details.actual ); 3012 | message += ""; 3015 | 3016 | if ( actual !== expected ) { 3017 | 3018 | message += ""; 3020 | 3021 | // Don't show diff if actual or expected are booleans 3022 | if ( !( /^(true|false)$/.test( actual ) ) && 3023 | !( /^(true|false)$/.test( expected ) ) ) { 3024 | diff = QUnit.diff( expected, actual ); 3025 | showDiff = stripHtml( diff ).length !== 3026 | stripHtml( expected ).length + 3027 | stripHtml( actual ).length; 3028 | } 3029 | 3030 | // Don't show diff if expected and actual are totally different 3031 | if ( showDiff ) { 3032 | message += ""; 3034 | } 3035 | } else if ( expected.indexOf( "[object Array]" ) !== -1 || 3036 | expected.indexOf( "[object Object]" ) !== -1 ) { 3037 | message += ""; 3043 | } else { 3044 | message += ""; 3047 | } 3048 | 3049 | if ( details.source ) { 3050 | message += ""; 3052 | } 3053 | 3054 | message += "
        Expected:
        " +
        3013 | 			escapeText( expected ) +
        3014 | 			"
        Result:
        " +
        3019 | 				escapeText( actual ) + "
        Diff:
        " +
        3033 | 					diff + "
        Message: " + 3038 | "Diff suppressed as the depth of object is more than current max depth (" + 3039 | QUnit.config.maxDepth + ").

        Hint: Use QUnit.dump.maxDepth to " + 3040 | " run with a higher max depth or " + 3042 | "Rerun without max depth.

        Message: " + 3045 | "Diff suppressed as the expected and actual results have an equivalent" + 3046 | " serialization
        Source:
        " +
        3051 | 				escapeText( details.source ) + "
        "; 3055 | 3056 | // This occurs when pushFailure is set and we have an extracted stack trace 3057 | } else if ( !details.result && details.source ) { 3058 | message += "" + 3059 | "" + 3061 | "
        Source:
        " +
        3060 | 			escapeText( details.source ) + "
        "; 3062 | } 3063 | 3064 | assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; 3065 | 3066 | assertLi = document.createElement( "li" ); 3067 | assertLi.className = details.result ? "pass" : "fail"; 3068 | assertLi.innerHTML = message; 3069 | assertList.appendChild( assertLi ); 3070 | } ); 3071 | 3072 | QUnit.testDone( function( details ) { 3073 | var testTitle, time, testItem, assertList, 3074 | good, bad, testCounts, skipped, sourceName, 3075 | tests = id( "qunit-tests" ); 3076 | 3077 | if ( !tests ) { 3078 | return; 3079 | } 3080 | 3081 | testItem = id( "qunit-test-output-" + details.testId ); 3082 | 3083 | assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; 3084 | 3085 | good = details.passed; 3086 | bad = details.failed; 3087 | 3088 | // Store result when possible 3089 | if ( config.reorder && defined.sessionStorage ) { 3090 | if ( bad ) { 3091 | sessionStorage.setItem( "qunit-test-" + details.module + "-" + details.name, bad ); 3092 | } else { 3093 | sessionStorage.removeItem( "qunit-test-" + details.module + "-" + details.name ); 3094 | } 3095 | } 3096 | 3097 | if ( bad === 0 ) { 3098 | 3099 | // Collapse the passing tests 3100 | addClass( assertList, "qunit-collapsed" ); 3101 | } else if ( bad && config.collapse && !collapseNext ) { 3102 | 3103 | // Skip collapsing the first failing test 3104 | collapseNext = true; 3105 | } else { 3106 | 3107 | // Collapse remaining tests 3108 | addClass( assertList, "qunit-collapsed" ); 3109 | } 3110 | 3111 | // The testItem.firstChild is the test name 3112 | testTitle = testItem.firstChild; 3113 | 3114 | testCounts = bad ? 3115 | "" + bad + ", " + "" + good + ", " : 3116 | ""; 3117 | 3118 | testTitle.innerHTML += " (" + testCounts + 3119 | details.assertions.length + ")"; 3120 | 3121 | if ( details.skipped ) { 3122 | testItem.className = "skipped"; 3123 | skipped = document.createElement( "em" ); 3124 | skipped.className = "qunit-skipped-label"; 3125 | skipped.innerHTML = "skipped"; 3126 | testItem.insertBefore( skipped, testTitle ); 3127 | } else { 3128 | addEvent( testTitle, "click", function() { 3129 | toggleClass( assertList, "qunit-collapsed" ); 3130 | } ); 3131 | 3132 | testItem.className = bad ? "fail" : "pass"; 3133 | 3134 | time = document.createElement( "span" ); 3135 | time.className = "runtime"; 3136 | time.innerHTML = details.runtime + " ms"; 3137 | testItem.insertBefore( time, assertList ); 3138 | } 3139 | 3140 | // Show the source of the test when showing assertions 3141 | if ( details.source ) { 3142 | sourceName = document.createElement( "p" ); 3143 | sourceName.innerHTML = "Source: " + details.source; 3144 | addClass( sourceName, "qunit-source" ); 3145 | if ( bad === 0 ) { 3146 | addClass( sourceName, "qunit-collapsed" ); 3147 | } 3148 | addEvent( testTitle, "click", function() { 3149 | toggleClass( sourceName, "qunit-collapsed" ); 3150 | } ); 3151 | testItem.appendChild( sourceName ); 3152 | } 3153 | } ); 3154 | 3155 | // Avoid readyState issue with phantomjs 3156 | // Ref: #818 3157 | var notPhantom = ( function( p ) { 3158 | return !( p && p.version && p.version.major > 0 ); 3159 | } )( window.phantom ); 3160 | 3161 | if ( notPhantom && document.readyState === "complete" ) { 3162 | QUnit.load(); 3163 | } else { 3164 | addEvent( window, "load", QUnit.load ); 3165 | } 3166 | 3167 | /* 3168 | * This file is a modified version of google-diff-match-patch's JavaScript implementation 3169 | * (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js), 3170 | * modifications are licensed as more fully set forth in LICENSE.txt. 3171 | * 3172 | * The original source of google-diff-match-patch is attributable and licensed as follows: 3173 | * 3174 | * Copyright 2006 Google Inc. 3175 | * https://code.google.com/p/google-diff-match-patch/ 3176 | * 3177 | * Licensed under the Apache License, Version 2.0 (the "License"); 3178 | * you may not use this file except in compliance with the License. 3179 | * You may obtain a copy of the License at 3180 | * 3181 | * https://www.apache.org/licenses/LICENSE-2.0 3182 | * 3183 | * Unless required by applicable law or agreed to in writing, software 3184 | * distributed under the License is distributed on an "AS IS" BASIS, 3185 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 3186 | * See the License for the specific language governing permissions and 3187 | * limitations under the License. 3188 | * 3189 | * More Info: 3190 | * https://code.google.com/p/google-diff-match-patch/ 3191 | * 3192 | * Usage: QUnit.diff(expected, actual) 3193 | * 3194 | */ 3195 | QUnit.diff = ( function() { 3196 | function DiffMatchPatch() { 3197 | } 3198 | 3199 | // DIFF FUNCTIONS 3200 | 3201 | /** 3202 | * The data structure representing a diff is an array of tuples: 3203 | * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] 3204 | * which means: delete 'Hello', add 'Goodbye' and keep ' world.' 3205 | */ 3206 | var DIFF_DELETE = -1, 3207 | DIFF_INSERT = 1, 3208 | DIFF_EQUAL = 0; 3209 | 3210 | /** 3211 | * Find the differences between two texts. Simplifies the problem by stripping 3212 | * any common prefix or suffix off the texts before diffing. 3213 | * @param {string} text1 Old string to be diffed. 3214 | * @param {string} text2 New string to be diffed. 3215 | * @param {boolean=} optChecklines Optional speedup flag. If present and false, 3216 | * then don't run a line-level diff first to identify the changed areas. 3217 | * Defaults to true, which does a faster, slightly less optimal diff. 3218 | * @return {!Array.} Array of diff tuples. 3219 | */ 3220 | DiffMatchPatch.prototype.DiffMain = function( text1, text2, optChecklines ) { 3221 | var deadline, checklines, commonlength, 3222 | commonprefix, commonsuffix, diffs; 3223 | 3224 | // The diff must be complete in up to 1 second. 3225 | deadline = ( new Date() ).getTime() + 1000; 3226 | 3227 | // Check for null inputs. 3228 | if ( text1 === null || text2 === null ) { 3229 | throw new Error( "Null input. (DiffMain)" ); 3230 | } 3231 | 3232 | // Check for equality (speedup). 3233 | if ( text1 === text2 ) { 3234 | if ( text1 ) { 3235 | return [ 3236 | [ DIFF_EQUAL, text1 ] 3237 | ]; 3238 | } 3239 | return []; 3240 | } 3241 | 3242 | if ( typeof optChecklines === "undefined" ) { 3243 | optChecklines = true; 3244 | } 3245 | 3246 | checklines = optChecklines; 3247 | 3248 | // Trim off common prefix (speedup). 3249 | commonlength = this.diffCommonPrefix( text1, text2 ); 3250 | commonprefix = text1.substring( 0, commonlength ); 3251 | text1 = text1.substring( commonlength ); 3252 | text2 = text2.substring( commonlength ); 3253 | 3254 | // Trim off common suffix (speedup). 3255 | commonlength = this.diffCommonSuffix( text1, text2 ); 3256 | commonsuffix = text1.substring( text1.length - commonlength ); 3257 | text1 = text1.substring( 0, text1.length - commonlength ); 3258 | text2 = text2.substring( 0, text2.length - commonlength ); 3259 | 3260 | // Compute the diff on the middle block. 3261 | diffs = this.diffCompute( text1, text2, checklines, deadline ); 3262 | 3263 | // Restore the prefix and suffix. 3264 | if ( commonprefix ) { 3265 | diffs.unshift( [ DIFF_EQUAL, commonprefix ] ); 3266 | } 3267 | if ( commonsuffix ) { 3268 | diffs.push( [ DIFF_EQUAL, commonsuffix ] ); 3269 | } 3270 | this.diffCleanupMerge( diffs ); 3271 | return diffs; 3272 | }; 3273 | 3274 | /** 3275 | * Reduce the number of edits by eliminating operationally trivial equalities. 3276 | * @param {!Array.} diffs Array of diff tuples. 3277 | */ 3278 | DiffMatchPatch.prototype.diffCleanupEfficiency = function( diffs ) { 3279 | var changes, equalities, equalitiesLength, lastequality, 3280 | pointer, preIns, preDel, postIns, postDel; 3281 | changes = false; 3282 | equalities = []; // Stack of indices where equalities are found. 3283 | equalitiesLength = 0; // Keeping our own length var is faster in JS. 3284 | /** @type {?string} */ 3285 | lastequality = null; 3286 | 3287 | // Always equal to diffs[equalities[equalitiesLength - 1]][1] 3288 | pointer = 0; // Index of current position. 3289 | 3290 | // Is there an insertion operation before the last equality. 3291 | preIns = false; 3292 | 3293 | // Is there a deletion operation before the last equality. 3294 | preDel = false; 3295 | 3296 | // Is there an insertion operation after the last equality. 3297 | postIns = false; 3298 | 3299 | // Is there a deletion operation after the last equality. 3300 | postDel = false; 3301 | while ( pointer < diffs.length ) { 3302 | 3303 | // Equality found. 3304 | if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { 3305 | if ( diffs[ pointer ][ 1 ].length < 4 && ( postIns || postDel ) ) { 3306 | 3307 | // Candidate found. 3308 | equalities[ equalitiesLength++ ] = pointer; 3309 | preIns = postIns; 3310 | preDel = postDel; 3311 | lastequality = diffs[ pointer ][ 1 ]; 3312 | } else { 3313 | 3314 | // Not a candidate, and can never become one. 3315 | equalitiesLength = 0; 3316 | lastequality = null; 3317 | } 3318 | postIns = postDel = false; 3319 | 3320 | // An insertion or deletion. 3321 | } else { 3322 | 3323 | if ( diffs[ pointer ][ 0 ] === DIFF_DELETE ) { 3324 | postDel = true; 3325 | } else { 3326 | postIns = true; 3327 | } 3328 | 3329 | /* 3330 | * Five types to be split: 3331 | * ABXYCD 3332 | * AXCD 3333 | * ABXC 3334 | * AXCD 3335 | * ABXC 3336 | */ 3337 | if ( lastequality && ( ( preIns && preDel && postIns && postDel ) || 3338 | ( ( lastequality.length < 2 ) && 3339 | ( preIns + preDel + postIns + postDel ) === 3 ) ) ) { 3340 | 3341 | // Duplicate record. 3342 | diffs.splice( 3343 | equalities[ equalitiesLength - 1 ], 3344 | 0, 3345 | [ DIFF_DELETE, lastequality ] 3346 | ); 3347 | 3348 | // Change second copy to insert. 3349 | diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT; 3350 | equalitiesLength--; // Throw away the equality we just deleted; 3351 | lastequality = null; 3352 | if ( preIns && preDel ) { 3353 | 3354 | // No changes made which could affect previous entry, keep going. 3355 | postIns = postDel = true; 3356 | equalitiesLength = 0; 3357 | } else { 3358 | equalitiesLength--; // Throw away the previous equality. 3359 | pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1; 3360 | postIns = postDel = false; 3361 | } 3362 | changes = true; 3363 | } 3364 | } 3365 | pointer++; 3366 | } 3367 | 3368 | if ( changes ) { 3369 | this.diffCleanupMerge( diffs ); 3370 | } 3371 | }; 3372 | 3373 | /** 3374 | * Convert a diff array into a pretty HTML report. 3375 | * @param {!Array.} diffs Array of diff tuples. 3376 | * @param {integer} string to be beautified. 3377 | * @return {string} HTML representation. 3378 | */ 3379 | DiffMatchPatch.prototype.diffPrettyHtml = function( diffs ) { 3380 | var op, data, x, 3381 | html = []; 3382 | for ( x = 0; x < diffs.length; x++ ) { 3383 | op = diffs[ x ][ 0 ]; // Operation (insert, delete, equal) 3384 | data = diffs[ x ][ 1 ]; // Text of change. 3385 | switch ( op ) { 3386 | case DIFF_INSERT: 3387 | html[ x ] = "" + escapeText( data ) + ""; 3388 | break; 3389 | case DIFF_DELETE: 3390 | html[ x ] = "" + escapeText( data ) + ""; 3391 | break; 3392 | case DIFF_EQUAL: 3393 | html[ x ] = "" + escapeText( data ) + ""; 3394 | break; 3395 | } 3396 | } 3397 | return html.join( "" ); 3398 | }; 3399 | 3400 | /** 3401 | * Determine the common prefix of two strings. 3402 | * @param {string} text1 First string. 3403 | * @param {string} text2 Second string. 3404 | * @return {number} The number of characters common to the start of each 3405 | * string. 3406 | */ 3407 | DiffMatchPatch.prototype.diffCommonPrefix = function( text1, text2 ) { 3408 | var pointermid, pointermax, pointermin, pointerstart; 3409 | 3410 | // Quick check for common null cases. 3411 | if ( !text1 || !text2 || text1.charAt( 0 ) !== text2.charAt( 0 ) ) { 3412 | return 0; 3413 | } 3414 | 3415 | // Binary search. 3416 | // Performance analysis: https://neil.fraser.name/news/2007/10/09/ 3417 | pointermin = 0; 3418 | pointermax = Math.min( text1.length, text2.length ); 3419 | pointermid = pointermax; 3420 | pointerstart = 0; 3421 | while ( pointermin < pointermid ) { 3422 | if ( text1.substring( pointerstart, pointermid ) === 3423 | text2.substring( pointerstart, pointermid ) ) { 3424 | pointermin = pointermid; 3425 | pointerstart = pointermin; 3426 | } else { 3427 | pointermax = pointermid; 3428 | } 3429 | pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin ); 3430 | } 3431 | return pointermid; 3432 | }; 3433 | 3434 | /** 3435 | * Determine the common suffix of two strings. 3436 | * @param {string} text1 First string. 3437 | * @param {string} text2 Second string. 3438 | * @return {number} The number of characters common to the end of each string. 3439 | */ 3440 | DiffMatchPatch.prototype.diffCommonSuffix = function( text1, text2 ) { 3441 | var pointermid, pointermax, pointermin, pointerend; 3442 | 3443 | // Quick check for common null cases. 3444 | if ( !text1 || 3445 | !text2 || 3446 | text1.charAt( text1.length - 1 ) !== text2.charAt( text2.length - 1 ) ) { 3447 | return 0; 3448 | } 3449 | 3450 | // Binary search. 3451 | // Performance analysis: https://neil.fraser.name/news/2007/10/09/ 3452 | pointermin = 0; 3453 | pointermax = Math.min( text1.length, text2.length ); 3454 | pointermid = pointermax; 3455 | pointerend = 0; 3456 | while ( pointermin < pointermid ) { 3457 | if ( text1.substring( text1.length - pointermid, text1.length - pointerend ) === 3458 | text2.substring( text2.length - pointermid, text2.length - pointerend ) ) { 3459 | pointermin = pointermid; 3460 | pointerend = pointermin; 3461 | } else { 3462 | pointermax = pointermid; 3463 | } 3464 | pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin ); 3465 | } 3466 | return pointermid; 3467 | }; 3468 | 3469 | /** 3470 | * Find the differences between two texts. Assumes that the texts do not 3471 | * have any common prefix or suffix. 3472 | * @param {string} text1 Old string to be diffed. 3473 | * @param {string} text2 New string to be diffed. 3474 | * @param {boolean} checklines Speedup flag. If false, then don't run a 3475 | * line-level diff first to identify the changed areas. 3476 | * If true, then run a faster, slightly less optimal diff. 3477 | * @param {number} deadline Time when the diff should be complete by. 3478 | * @return {!Array.} Array of diff tuples. 3479 | * @private 3480 | */ 3481 | DiffMatchPatch.prototype.diffCompute = function( text1, text2, checklines, deadline ) { 3482 | var diffs, longtext, shorttext, i, hm, 3483 | text1A, text2A, text1B, text2B, 3484 | midCommon, diffsA, diffsB; 3485 | 3486 | if ( !text1 ) { 3487 | 3488 | // Just add some text (speedup). 3489 | return [ 3490 | [ DIFF_INSERT, text2 ] 3491 | ]; 3492 | } 3493 | 3494 | if ( !text2 ) { 3495 | 3496 | // Just delete some text (speedup). 3497 | return [ 3498 | [ DIFF_DELETE, text1 ] 3499 | ]; 3500 | } 3501 | 3502 | longtext = text1.length > text2.length ? text1 : text2; 3503 | shorttext = text1.length > text2.length ? text2 : text1; 3504 | i = longtext.indexOf( shorttext ); 3505 | if ( i !== -1 ) { 3506 | 3507 | // Shorter text is inside the longer text (speedup). 3508 | diffs = [ 3509 | [ DIFF_INSERT, longtext.substring( 0, i ) ], 3510 | [ DIFF_EQUAL, shorttext ], 3511 | [ DIFF_INSERT, longtext.substring( i + shorttext.length ) ] 3512 | ]; 3513 | 3514 | // Swap insertions for deletions if diff is reversed. 3515 | if ( text1.length > text2.length ) { 3516 | diffs[ 0 ][ 0 ] = diffs[ 2 ][ 0 ] = DIFF_DELETE; 3517 | } 3518 | return diffs; 3519 | } 3520 | 3521 | if ( shorttext.length === 1 ) { 3522 | 3523 | // Single character string. 3524 | // After the previous speedup, the character can't be an equality. 3525 | return [ 3526 | [ DIFF_DELETE, text1 ], 3527 | [ DIFF_INSERT, text2 ] 3528 | ]; 3529 | } 3530 | 3531 | // Check to see if the problem can be split in two. 3532 | hm = this.diffHalfMatch( text1, text2 ); 3533 | if ( hm ) { 3534 | 3535 | // A half-match was found, sort out the return data. 3536 | text1A = hm[ 0 ]; 3537 | text1B = hm[ 1 ]; 3538 | text2A = hm[ 2 ]; 3539 | text2B = hm[ 3 ]; 3540 | midCommon = hm[ 4 ]; 3541 | 3542 | // Send both pairs off for separate processing. 3543 | diffsA = this.DiffMain( text1A, text2A, checklines, deadline ); 3544 | diffsB = this.DiffMain( text1B, text2B, checklines, deadline ); 3545 | 3546 | // Merge the results. 3547 | return diffsA.concat( [ 3548 | [ DIFF_EQUAL, midCommon ] 3549 | ], diffsB ); 3550 | } 3551 | 3552 | if ( checklines && text1.length > 100 && text2.length > 100 ) { 3553 | return this.diffLineMode( text1, text2, deadline ); 3554 | } 3555 | 3556 | return this.diffBisect( text1, text2, deadline ); 3557 | }; 3558 | 3559 | /** 3560 | * Do the two texts share a substring which is at least half the length of the 3561 | * longer text? 3562 | * This speedup can produce non-minimal diffs. 3563 | * @param {string} text1 First string. 3564 | * @param {string} text2 Second string. 3565 | * @return {Array.} Five element Array, containing the prefix of 3566 | * text1, the suffix of text1, the prefix of text2, the suffix of 3567 | * text2 and the common middle. Or null if there was no match. 3568 | * @private 3569 | */ 3570 | DiffMatchPatch.prototype.diffHalfMatch = function( text1, text2 ) { 3571 | var longtext, shorttext, dmp, 3572 | text1A, text2B, text2A, text1B, midCommon, 3573 | hm1, hm2, hm; 3574 | 3575 | longtext = text1.length > text2.length ? text1 : text2; 3576 | shorttext = text1.length > text2.length ? text2 : text1; 3577 | if ( longtext.length < 4 || shorttext.length * 2 < longtext.length ) { 3578 | return null; // Pointless. 3579 | } 3580 | dmp = this; // 'this' becomes 'window' in a closure. 3581 | 3582 | /** 3583 | * Does a substring of shorttext exist within longtext such that the substring 3584 | * is at least half the length of longtext? 3585 | * Closure, but does not reference any external variables. 3586 | * @param {string} longtext Longer string. 3587 | * @param {string} shorttext Shorter string. 3588 | * @param {number} i Start index of quarter length substring within longtext. 3589 | * @return {Array.} Five element Array, containing the prefix of 3590 | * longtext, the suffix of longtext, the prefix of shorttext, the suffix 3591 | * of shorttext and the common middle. Or null if there was no match. 3592 | * @private 3593 | */ 3594 | function diffHalfMatchI( longtext, shorttext, i ) { 3595 | var seed, j, bestCommon, prefixLength, suffixLength, 3596 | bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB; 3597 | 3598 | // Start with a 1/4 length substring at position i as a seed. 3599 | seed = longtext.substring( i, i + Math.floor( longtext.length / 4 ) ); 3600 | j = -1; 3601 | bestCommon = ""; 3602 | while ( ( j = shorttext.indexOf( seed, j + 1 ) ) !== -1 ) { 3603 | prefixLength = dmp.diffCommonPrefix( longtext.substring( i ), 3604 | shorttext.substring( j ) ); 3605 | suffixLength = dmp.diffCommonSuffix( longtext.substring( 0, i ), 3606 | shorttext.substring( 0, j ) ); 3607 | if ( bestCommon.length < suffixLength + prefixLength ) { 3608 | bestCommon = shorttext.substring( j - suffixLength, j ) + 3609 | shorttext.substring( j, j + prefixLength ); 3610 | bestLongtextA = longtext.substring( 0, i - suffixLength ); 3611 | bestLongtextB = longtext.substring( i + prefixLength ); 3612 | bestShorttextA = shorttext.substring( 0, j - suffixLength ); 3613 | bestShorttextB = shorttext.substring( j + prefixLength ); 3614 | } 3615 | } 3616 | if ( bestCommon.length * 2 >= longtext.length ) { 3617 | return [ bestLongtextA, bestLongtextB, 3618 | bestShorttextA, bestShorttextB, bestCommon 3619 | ]; 3620 | } else { 3621 | return null; 3622 | } 3623 | } 3624 | 3625 | // First check if the second quarter is the seed for a half-match. 3626 | hm1 = diffHalfMatchI( longtext, shorttext, 3627 | Math.ceil( longtext.length / 4 ) ); 3628 | 3629 | // Check again based on the third quarter. 3630 | hm2 = diffHalfMatchI( longtext, shorttext, 3631 | Math.ceil( longtext.length / 2 ) ); 3632 | if ( !hm1 && !hm2 ) { 3633 | return null; 3634 | } else if ( !hm2 ) { 3635 | hm = hm1; 3636 | } else if ( !hm1 ) { 3637 | hm = hm2; 3638 | } else { 3639 | 3640 | // Both matched. Select the longest. 3641 | hm = hm1[ 4 ].length > hm2[ 4 ].length ? hm1 : hm2; 3642 | } 3643 | 3644 | // A half-match was found, sort out the return data. 3645 | text1A, text1B, text2A, text2B; 3646 | if ( text1.length > text2.length ) { 3647 | text1A = hm[ 0 ]; 3648 | text1B = hm[ 1 ]; 3649 | text2A = hm[ 2 ]; 3650 | text2B = hm[ 3 ]; 3651 | } else { 3652 | text2A = hm[ 0 ]; 3653 | text2B = hm[ 1 ]; 3654 | text1A = hm[ 2 ]; 3655 | text1B = hm[ 3 ]; 3656 | } 3657 | midCommon = hm[ 4 ]; 3658 | return [ text1A, text1B, text2A, text2B, midCommon ]; 3659 | }; 3660 | 3661 | /** 3662 | * Do a quick line-level diff on both strings, then rediff the parts for 3663 | * greater accuracy. 3664 | * This speedup can produce non-minimal diffs. 3665 | * @param {string} text1 Old string to be diffed. 3666 | * @param {string} text2 New string to be diffed. 3667 | * @param {number} deadline Time when the diff should be complete by. 3668 | * @return {!Array.} Array of diff tuples. 3669 | * @private 3670 | */ 3671 | DiffMatchPatch.prototype.diffLineMode = function( text1, text2, deadline ) { 3672 | var a, diffs, linearray, pointer, countInsert, 3673 | countDelete, textInsert, textDelete, j; 3674 | 3675 | // Scan the text on a line-by-line basis first. 3676 | a = this.diffLinesToChars( text1, text2 ); 3677 | text1 = a.chars1; 3678 | text2 = a.chars2; 3679 | linearray = a.lineArray; 3680 | 3681 | diffs = this.DiffMain( text1, text2, false, deadline ); 3682 | 3683 | // Convert the diff back to original text. 3684 | this.diffCharsToLines( diffs, linearray ); 3685 | 3686 | // Eliminate freak matches (e.g. blank lines) 3687 | this.diffCleanupSemantic( diffs ); 3688 | 3689 | // Rediff any replacement blocks, this time character-by-character. 3690 | // Add a dummy entry at the end. 3691 | diffs.push( [ DIFF_EQUAL, "" ] ); 3692 | pointer = 0; 3693 | countDelete = 0; 3694 | countInsert = 0; 3695 | textDelete = ""; 3696 | textInsert = ""; 3697 | while ( pointer < diffs.length ) { 3698 | switch ( diffs[ pointer ][ 0 ] ) { 3699 | case DIFF_INSERT: 3700 | countInsert++; 3701 | textInsert += diffs[ pointer ][ 1 ]; 3702 | break; 3703 | case DIFF_DELETE: 3704 | countDelete++; 3705 | textDelete += diffs[ pointer ][ 1 ]; 3706 | break; 3707 | case DIFF_EQUAL: 3708 | 3709 | // Upon reaching an equality, check for prior redundancies. 3710 | if ( countDelete >= 1 && countInsert >= 1 ) { 3711 | 3712 | // Delete the offending records and add the merged ones. 3713 | diffs.splice( pointer - countDelete - countInsert, 3714 | countDelete + countInsert ); 3715 | pointer = pointer - countDelete - countInsert; 3716 | a = this.DiffMain( textDelete, textInsert, false, deadline ); 3717 | for ( j = a.length - 1; j >= 0; j-- ) { 3718 | diffs.splice( pointer, 0, a[ j ] ); 3719 | } 3720 | pointer = pointer + a.length; 3721 | } 3722 | countInsert = 0; 3723 | countDelete = 0; 3724 | textDelete = ""; 3725 | textInsert = ""; 3726 | break; 3727 | } 3728 | pointer++; 3729 | } 3730 | diffs.pop(); // Remove the dummy entry at the end. 3731 | 3732 | return diffs; 3733 | }; 3734 | 3735 | /** 3736 | * Find the 'middle snake' of a diff, split the problem in two 3737 | * and return the recursively constructed diff. 3738 | * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. 3739 | * @param {string} text1 Old string to be diffed. 3740 | * @param {string} text2 New string to be diffed. 3741 | * @param {number} deadline Time at which to bail if not yet complete. 3742 | * @return {!Array.} Array of diff tuples. 3743 | * @private 3744 | */ 3745 | DiffMatchPatch.prototype.diffBisect = function( text1, text2, deadline ) { 3746 | var text1Length, text2Length, maxD, vOffset, vLength, 3747 | v1, v2, x, delta, front, k1start, k1end, k2start, 3748 | k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2; 3749 | 3750 | // Cache the text lengths to prevent multiple calls. 3751 | text1Length = text1.length; 3752 | text2Length = text2.length; 3753 | maxD = Math.ceil( ( text1Length + text2Length ) / 2 ); 3754 | vOffset = maxD; 3755 | vLength = 2 * maxD; 3756 | v1 = new Array( vLength ); 3757 | v2 = new Array( vLength ); 3758 | 3759 | // Setting all elements to -1 is faster in Chrome & Firefox than mixing 3760 | // integers and undefined. 3761 | for ( x = 0; x < vLength; x++ ) { 3762 | v1[ x ] = -1; 3763 | v2[ x ] = -1; 3764 | } 3765 | v1[ vOffset + 1 ] = 0; 3766 | v2[ vOffset + 1 ] = 0; 3767 | delta = text1Length - text2Length; 3768 | 3769 | // If the total number of characters is odd, then the front path will collide 3770 | // with the reverse path. 3771 | front = ( delta % 2 !== 0 ); 3772 | 3773 | // Offsets for start and end of k loop. 3774 | // Prevents mapping of space beyond the grid. 3775 | k1start = 0; 3776 | k1end = 0; 3777 | k2start = 0; 3778 | k2end = 0; 3779 | for ( d = 0; d < maxD; d++ ) { 3780 | 3781 | // Bail out if deadline is reached. 3782 | if ( ( new Date() ).getTime() > deadline ) { 3783 | break; 3784 | } 3785 | 3786 | // Walk the front path one step. 3787 | for ( k1 = -d + k1start; k1 <= d - k1end; k1 += 2 ) { 3788 | k1Offset = vOffset + k1; 3789 | if ( k1 === -d || ( k1 !== d && v1[ k1Offset - 1 ] < v1[ k1Offset + 1 ] ) ) { 3790 | x1 = v1[ k1Offset + 1 ]; 3791 | } else { 3792 | x1 = v1[ k1Offset - 1 ] + 1; 3793 | } 3794 | y1 = x1 - k1; 3795 | while ( x1 < text1Length && y1 < text2Length && 3796 | text1.charAt( x1 ) === text2.charAt( y1 ) ) { 3797 | x1++; 3798 | y1++; 3799 | } 3800 | v1[ k1Offset ] = x1; 3801 | if ( x1 > text1Length ) { 3802 | 3803 | // Ran off the right of the graph. 3804 | k1end += 2; 3805 | } else if ( y1 > text2Length ) { 3806 | 3807 | // Ran off the bottom of the graph. 3808 | k1start += 2; 3809 | } else if ( front ) { 3810 | k2Offset = vOffset + delta - k1; 3811 | if ( k2Offset >= 0 && k2Offset < vLength && v2[ k2Offset ] !== -1 ) { 3812 | 3813 | // Mirror x2 onto top-left coordinate system. 3814 | x2 = text1Length - v2[ k2Offset ]; 3815 | if ( x1 >= x2 ) { 3816 | 3817 | // Overlap detected. 3818 | return this.diffBisectSplit( text1, text2, x1, y1, deadline ); 3819 | } 3820 | } 3821 | } 3822 | } 3823 | 3824 | // Walk the reverse path one step. 3825 | for ( k2 = -d + k2start; k2 <= d - k2end; k2 += 2 ) { 3826 | k2Offset = vOffset + k2; 3827 | if ( k2 === -d || ( k2 !== d && v2[ k2Offset - 1 ] < v2[ k2Offset + 1 ] ) ) { 3828 | x2 = v2[ k2Offset + 1 ]; 3829 | } else { 3830 | x2 = v2[ k2Offset - 1 ] + 1; 3831 | } 3832 | y2 = x2 - k2; 3833 | while ( x2 < text1Length && y2 < text2Length && 3834 | text1.charAt( text1Length - x2 - 1 ) === 3835 | text2.charAt( text2Length - y2 - 1 ) ) { 3836 | x2++; 3837 | y2++; 3838 | } 3839 | v2[ k2Offset ] = x2; 3840 | if ( x2 > text1Length ) { 3841 | 3842 | // Ran off the left of the graph. 3843 | k2end += 2; 3844 | } else if ( y2 > text2Length ) { 3845 | 3846 | // Ran off the top of the graph. 3847 | k2start += 2; 3848 | } else if ( !front ) { 3849 | k1Offset = vOffset + delta - k2; 3850 | if ( k1Offset >= 0 && k1Offset < vLength && v1[ k1Offset ] !== -1 ) { 3851 | x1 = v1[ k1Offset ]; 3852 | y1 = vOffset + x1 - k1Offset; 3853 | 3854 | // Mirror x2 onto top-left coordinate system. 3855 | x2 = text1Length - x2; 3856 | if ( x1 >= x2 ) { 3857 | 3858 | // Overlap detected. 3859 | return this.diffBisectSplit( text1, text2, x1, y1, deadline ); 3860 | } 3861 | } 3862 | } 3863 | } 3864 | } 3865 | 3866 | // Diff took too long and hit the deadline or 3867 | // number of diffs equals number of characters, no commonality at all. 3868 | return [ 3869 | [ DIFF_DELETE, text1 ], 3870 | [ DIFF_INSERT, text2 ] 3871 | ]; 3872 | }; 3873 | 3874 | /** 3875 | * Given the location of the 'middle snake', split the diff in two parts 3876 | * and recurse. 3877 | * @param {string} text1 Old string to be diffed. 3878 | * @param {string} text2 New string to be diffed. 3879 | * @param {number} x Index of split point in text1. 3880 | * @param {number} y Index of split point in text2. 3881 | * @param {number} deadline Time at which to bail if not yet complete. 3882 | * @return {!Array.} Array of diff tuples. 3883 | * @private 3884 | */ 3885 | DiffMatchPatch.prototype.diffBisectSplit = function( text1, text2, x, y, deadline ) { 3886 | var text1a, text1b, text2a, text2b, diffs, diffsb; 3887 | text1a = text1.substring( 0, x ); 3888 | text2a = text2.substring( 0, y ); 3889 | text1b = text1.substring( x ); 3890 | text2b = text2.substring( y ); 3891 | 3892 | // Compute both diffs serially. 3893 | diffs = this.DiffMain( text1a, text2a, false, deadline ); 3894 | diffsb = this.DiffMain( text1b, text2b, false, deadline ); 3895 | 3896 | return diffs.concat( diffsb ); 3897 | }; 3898 | 3899 | /** 3900 | * Reduce the number of edits by eliminating semantically trivial equalities. 3901 | * @param {!Array.} diffs Array of diff tuples. 3902 | */ 3903 | DiffMatchPatch.prototype.diffCleanupSemantic = function( diffs ) { 3904 | var changes, equalities, equalitiesLength, lastequality, 3905 | pointer, lengthInsertions2, lengthDeletions2, lengthInsertions1, 3906 | lengthDeletions1, deletion, insertion, overlapLength1, overlapLength2; 3907 | changes = false; 3908 | equalities = []; // Stack of indices where equalities are found. 3909 | equalitiesLength = 0; // Keeping our own length var is faster in JS. 3910 | /** @type {?string} */ 3911 | lastequality = null; 3912 | 3913 | // Always equal to diffs[equalities[equalitiesLength - 1]][1] 3914 | pointer = 0; // Index of current position. 3915 | 3916 | // Number of characters that changed prior to the equality. 3917 | lengthInsertions1 = 0; 3918 | lengthDeletions1 = 0; 3919 | 3920 | // Number of characters that changed after the equality. 3921 | lengthInsertions2 = 0; 3922 | lengthDeletions2 = 0; 3923 | while ( pointer < diffs.length ) { 3924 | if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { // Equality found. 3925 | equalities[ equalitiesLength++ ] = pointer; 3926 | lengthInsertions1 = lengthInsertions2; 3927 | lengthDeletions1 = lengthDeletions2; 3928 | lengthInsertions2 = 0; 3929 | lengthDeletions2 = 0; 3930 | lastequality = diffs[ pointer ][ 1 ]; 3931 | } else { // An insertion or deletion. 3932 | if ( diffs[ pointer ][ 0 ] === DIFF_INSERT ) { 3933 | lengthInsertions2 += diffs[ pointer ][ 1 ].length; 3934 | } else { 3935 | lengthDeletions2 += diffs[ pointer ][ 1 ].length; 3936 | } 3937 | 3938 | // Eliminate an equality that is smaller or equal to the edits on both 3939 | // sides of it. 3940 | if ( lastequality && ( lastequality.length <= 3941 | Math.max( lengthInsertions1, lengthDeletions1 ) ) && 3942 | ( lastequality.length <= Math.max( lengthInsertions2, 3943 | lengthDeletions2 ) ) ) { 3944 | 3945 | // Duplicate record. 3946 | diffs.splice( 3947 | equalities[ equalitiesLength - 1 ], 3948 | 0, 3949 | [ DIFF_DELETE, lastequality ] 3950 | ); 3951 | 3952 | // Change second copy to insert. 3953 | diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT; 3954 | 3955 | // Throw away the equality we just deleted. 3956 | equalitiesLength--; 3957 | 3958 | // Throw away the previous equality (it needs to be reevaluated). 3959 | equalitiesLength--; 3960 | pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1; 3961 | 3962 | // Reset the counters. 3963 | lengthInsertions1 = 0; 3964 | lengthDeletions1 = 0; 3965 | lengthInsertions2 = 0; 3966 | lengthDeletions2 = 0; 3967 | lastequality = null; 3968 | changes = true; 3969 | } 3970 | } 3971 | pointer++; 3972 | } 3973 | 3974 | // Normalize the diff. 3975 | if ( changes ) { 3976 | this.diffCleanupMerge( diffs ); 3977 | } 3978 | 3979 | // Find any overlaps between deletions and insertions. 3980 | // e.g: abcxxxxxxdef 3981 | // -> abcxxxdef 3982 | // e.g: xxxabcdefxxx 3983 | // -> defxxxabc 3984 | // Only extract an overlap if it is as big as the edit ahead or behind it. 3985 | pointer = 1; 3986 | while ( pointer < diffs.length ) { 3987 | if ( diffs[ pointer - 1 ][ 0 ] === DIFF_DELETE && 3988 | diffs[ pointer ][ 0 ] === DIFF_INSERT ) { 3989 | deletion = diffs[ pointer - 1 ][ 1 ]; 3990 | insertion = diffs[ pointer ][ 1 ]; 3991 | overlapLength1 = this.diffCommonOverlap( deletion, insertion ); 3992 | overlapLength2 = this.diffCommonOverlap( insertion, deletion ); 3993 | if ( overlapLength1 >= overlapLength2 ) { 3994 | if ( overlapLength1 >= deletion.length / 2 || 3995 | overlapLength1 >= insertion.length / 2 ) { 3996 | 3997 | // Overlap found. Insert an equality and trim the surrounding edits. 3998 | diffs.splice( 3999 | pointer, 4000 | 0, 4001 | [ DIFF_EQUAL, insertion.substring( 0, overlapLength1 ) ] 4002 | ); 4003 | diffs[ pointer - 1 ][ 1 ] = 4004 | deletion.substring( 0, deletion.length - overlapLength1 ); 4005 | diffs[ pointer + 1 ][ 1 ] = insertion.substring( overlapLength1 ); 4006 | pointer++; 4007 | } 4008 | } else { 4009 | if ( overlapLength2 >= deletion.length / 2 || 4010 | overlapLength2 >= insertion.length / 2 ) { 4011 | 4012 | // Reverse overlap found. 4013 | // Insert an equality and swap and trim the surrounding edits. 4014 | diffs.splice( 4015 | pointer, 4016 | 0, 4017 | [ DIFF_EQUAL, deletion.substring( 0, overlapLength2 ) ] 4018 | ); 4019 | 4020 | diffs[ pointer - 1 ][ 0 ] = DIFF_INSERT; 4021 | diffs[ pointer - 1 ][ 1 ] = 4022 | insertion.substring( 0, insertion.length - overlapLength2 ); 4023 | diffs[ pointer + 1 ][ 0 ] = DIFF_DELETE; 4024 | diffs[ pointer + 1 ][ 1 ] = 4025 | deletion.substring( overlapLength2 ); 4026 | pointer++; 4027 | } 4028 | } 4029 | pointer++; 4030 | } 4031 | pointer++; 4032 | } 4033 | }; 4034 | 4035 | /** 4036 | * Determine if the suffix of one string is the prefix of another. 4037 | * @param {string} text1 First string. 4038 | * @param {string} text2 Second string. 4039 | * @return {number} The number of characters common to the end of the first 4040 | * string and the start of the second string. 4041 | * @private 4042 | */ 4043 | DiffMatchPatch.prototype.diffCommonOverlap = function( text1, text2 ) { 4044 | var text1Length, text2Length, textLength, 4045 | best, length, pattern, found; 4046 | 4047 | // Cache the text lengths to prevent multiple calls. 4048 | text1Length = text1.length; 4049 | text2Length = text2.length; 4050 | 4051 | // Eliminate the null case. 4052 | if ( text1Length === 0 || text2Length === 0 ) { 4053 | return 0; 4054 | } 4055 | 4056 | // Truncate the longer string. 4057 | if ( text1Length > text2Length ) { 4058 | text1 = text1.substring( text1Length - text2Length ); 4059 | } else if ( text1Length < text2Length ) { 4060 | text2 = text2.substring( 0, text1Length ); 4061 | } 4062 | textLength = Math.min( text1Length, text2Length ); 4063 | 4064 | // Quick check for the worst case. 4065 | if ( text1 === text2 ) { 4066 | return textLength; 4067 | } 4068 | 4069 | // Start by looking for a single character match 4070 | // and increase length until no match is found. 4071 | // Performance analysis: https://neil.fraser.name/news/2010/11/04/ 4072 | best = 0; 4073 | length = 1; 4074 | while ( true ) { 4075 | pattern = text1.substring( textLength - length ); 4076 | found = text2.indexOf( pattern ); 4077 | if ( found === -1 ) { 4078 | return best; 4079 | } 4080 | length += found; 4081 | if ( found === 0 || text1.substring( textLength - length ) === 4082 | text2.substring( 0, length ) ) { 4083 | best = length; 4084 | length++; 4085 | } 4086 | } 4087 | }; 4088 | 4089 | /** 4090 | * Split two texts into an array of strings. Reduce the texts to a string of 4091 | * hashes where each Unicode character represents one line. 4092 | * @param {string} text1 First string. 4093 | * @param {string} text2 Second string. 4094 | * @return {{chars1: string, chars2: string, lineArray: !Array.}} 4095 | * An object containing the encoded text1, the encoded text2 and 4096 | * the array of unique strings. 4097 | * The zeroth element of the array of unique strings is intentionally blank. 4098 | * @private 4099 | */ 4100 | DiffMatchPatch.prototype.diffLinesToChars = function( text1, text2 ) { 4101 | var lineArray, lineHash, chars1, chars2; 4102 | lineArray = []; // E.g. lineArray[4] === 'Hello\n' 4103 | lineHash = {}; // E.g. lineHash['Hello\n'] === 4 4104 | 4105 | // '\x00' is a valid character, but various debuggers don't like it. 4106 | // So we'll insert a junk entry to avoid generating a null character. 4107 | lineArray[ 0 ] = ""; 4108 | 4109 | /** 4110 | * Split a text into an array of strings. Reduce the texts to a string of 4111 | * hashes where each Unicode character represents one line. 4112 | * Modifies linearray and linehash through being a closure. 4113 | * @param {string} text String to encode. 4114 | * @return {string} Encoded string. 4115 | * @private 4116 | */ 4117 | function diffLinesToCharsMunge( text ) { 4118 | var chars, lineStart, lineEnd, lineArrayLength, line; 4119 | chars = ""; 4120 | 4121 | // Walk the text, pulling out a substring for each line. 4122 | // text.split('\n') would would temporarily double our memory footprint. 4123 | // Modifying text would create many large strings to garbage collect. 4124 | lineStart = 0; 4125 | lineEnd = -1; 4126 | 4127 | // Keeping our own length variable is faster than looking it up. 4128 | lineArrayLength = lineArray.length; 4129 | while ( lineEnd < text.length - 1 ) { 4130 | lineEnd = text.indexOf( "\n", lineStart ); 4131 | if ( lineEnd === -1 ) { 4132 | lineEnd = text.length - 1; 4133 | } 4134 | line = text.substring( lineStart, lineEnd + 1 ); 4135 | lineStart = lineEnd + 1; 4136 | 4137 | if ( lineHash.hasOwnProperty ? lineHash.hasOwnProperty( line ) : 4138 | ( lineHash[ line ] !== undefined ) ) { 4139 | chars += String.fromCharCode( lineHash[ line ] ); 4140 | } else { 4141 | chars += String.fromCharCode( lineArrayLength ); 4142 | lineHash[ line ] = lineArrayLength; 4143 | lineArray[ lineArrayLength++ ] = line; 4144 | } 4145 | } 4146 | return chars; 4147 | } 4148 | 4149 | chars1 = diffLinesToCharsMunge( text1 ); 4150 | chars2 = diffLinesToCharsMunge( text2 ); 4151 | return { 4152 | chars1: chars1, 4153 | chars2: chars2, 4154 | lineArray: lineArray 4155 | }; 4156 | }; 4157 | 4158 | /** 4159 | * Rehydrate the text in a diff from a string of line hashes to real lines of 4160 | * text. 4161 | * @param {!Array.} diffs Array of diff tuples. 4162 | * @param {!Array.} lineArray Array of unique strings. 4163 | * @private 4164 | */ 4165 | DiffMatchPatch.prototype.diffCharsToLines = function( diffs, lineArray ) { 4166 | var x, chars, text, y; 4167 | for ( x = 0; x < diffs.length; x++ ) { 4168 | chars = diffs[ x ][ 1 ]; 4169 | text = []; 4170 | for ( y = 0; y < chars.length; y++ ) { 4171 | text[ y ] = lineArray[ chars.charCodeAt( y ) ]; 4172 | } 4173 | diffs[ x ][ 1 ] = text.join( "" ); 4174 | } 4175 | }; 4176 | 4177 | /** 4178 | * Reorder and merge like edit sections. Merge equalities. 4179 | * Any edit section can move as long as it doesn't cross an equality. 4180 | * @param {!Array.} diffs Array of diff tuples. 4181 | */ 4182 | DiffMatchPatch.prototype.diffCleanupMerge = function( diffs ) { 4183 | var pointer, countDelete, countInsert, textInsert, textDelete, 4184 | commonlength, changes, diffPointer, position; 4185 | diffs.push( [ DIFF_EQUAL, "" ] ); // Add a dummy entry at the end. 4186 | pointer = 0; 4187 | countDelete = 0; 4188 | countInsert = 0; 4189 | textDelete = ""; 4190 | textInsert = ""; 4191 | commonlength; 4192 | while ( pointer < diffs.length ) { 4193 | switch ( diffs[ pointer ][ 0 ] ) { 4194 | case DIFF_INSERT: 4195 | countInsert++; 4196 | textInsert += diffs[ pointer ][ 1 ]; 4197 | pointer++; 4198 | break; 4199 | case DIFF_DELETE: 4200 | countDelete++; 4201 | textDelete += diffs[ pointer ][ 1 ]; 4202 | pointer++; 4203 | break; 4204 | case DIFF_EQUAL: 4205 | 4206 | // Upon reaching an equality, check for prior redundancies. 4207 | if ( countDelete + countInsert > 1 ) { 4208 | if ( countDelete !== 0 && countInsert !== 0 ) { 4209 | 4210 | // Factor out any common prefixes. 4211 | commonlength = this.diffCommonPrefix( textInsert, textDelete ); 4212 | if ( commonlength !== 0 ) { 4213 | if ( ( pointer - countDelete - countInsert ) > 0 && 4214 | diffs[ pointer - countDelete - countInsert - 1 ][ 0 ] === 4215 | DIFF_EQUAL ) { 4216 | diffs[ pointer - countDelete - countInsert - 1 ][ 1 ] += 4217 | textInsert.substring( 0, commonlength ); 4218 | } else { 4219 | diffs.splice( 0, 0, [ DIFF_EQUAL, 4220 | textInsert.substring( 0, commonlength ) 4221 | ] ); 4222 | pointer++; 4223 | } 4224 | textInsert = textInsert.substring( commonlength ); 4225 | textDelete = textDelete.substring( commonlength ); 4226 | } 4227 | 4228 | // Factor out any common suffixies. 4229 | commonlength = this.diffCommonSuffix( textInsert, textDelete ); 4230 | if ( commonlength !== 0 ) { 4231 | diffs[ pointer ][ 1 ] = textInsert.substring( textInsert.length - 4232 | commonlength ) + diffs[ pointer ][ 1 ]; 4233 | textInsert = textInsert.substring( 0, textInsert.length - 4234 | commonlength ); 4235 | textDelete = textDelete.substring( 0, textDelete.length - 4236 | commonlength ); 4237 | } 4238 | } 4239 | 4240 | // Delete the offending records and add the merged ones. 4241 | if ( countDelete === 0 ) { 4242 | diffs.splice( pointer - countInsert, 4243 | countDelete + countInsert, [ DIFF_INSERT, textInsert ] ); 4244 | } else if ( countInsert === 0 ) { 4245 | diffs.splice( pointer - countDelete, 4246 | countDelete + countInsert, [ DIFF_DELETE, textDelete ] ); 4247 | } else { 4248 | diffs.splice( 4249 | pointer - countDelete - countInsert, 4250 | countDelete + countInsert, 4251 | [ DIFF_DELETE, textDelete ], [ DIFF_INSERT, textInsert ] 4252 | ); 4253 | } 4254 | pointer = pointer - countDelete - countInsert + 4255 | ( countDelete ? 1 : 0 ) + ( countInsert ? 1 : 0 ) + 1; 4256 | } else if ( pointer !== 0 && diffs[ pointer - 1 ][ 0 ] === DIFF_EQUAL ) { 4257 | 4258 | // Merge this equality with the previous one. 4259 | diffs[ pointer - 1 ][ 1 ] += diffs[ pointer ][ 1 ]; 4260 | diffs.splice( pointer, 1 ); 4261 | } else { 4262 | pointer++; 4263 | } 4264 | countInsert = 0; 4265 | countDelete = 0; 4266 | textDelete = ""; 4267 | textInsert = ""; 4268 | break; 4269 | } 4270 | } 4271 | if ( diffs[ diffs.length - 1 ][ 1 ] === "" ) { 4272 | diffs.pop(); // Remove the dummy entry at the end. 4273 | } 4274 | 4275 | // Second pass: look for single edits surrounded on both sides by equalities 4276 | // which can be shifted sideways to eliminate an equality. 4277 | // e.g: ABAC -> ABAC 4278 | changes = false; 4279 | pointer = 1; 4280 | 4281 | // Intentionally ignore the first and last element (don't need checking). 4282 | while ( pointer < diffs.length - 1 ) { 4283 | if ( diffs[ pointer - 1 ][ 0 ] === DIFF_EQUAL && 4284 | diffs[ pointer + 1 ][ 0 ] === DIFF_EQUAL ) { 4285 | 4286 | diffPointer = diffs[ pointer ][ 1 ]; 4287 | position = diffPointer.substring( 4288 | diffPointer.length - diffs[ pointer - 1 ][ 1 ].length 4289 | ); 4290 | 4291 | // This is a single edit surrounded by equalities. 4292 | if ( position === diffs[ pointer - 1 ][ 1 ] ) { 4293 | 4294 | // Shift the edit over the previous equality. 4295 | diffs[ pointer ][ 1 ] = diffs[ pointer - 1 ][ 1 ] + 4296 | diffs[ pointer ][ 1 ].substring( 0, diffs[ pointer ][ 1 ].length - 4297 | diffs[ pointer - 1 ][ 1 ].length ); 4298 | diffs[ pointer + 1 ][ 1 ] = 4299 | diffs[ pointer - 1 ][ 1 ] + diffs[ pointer + 1 ][ 1 ]; 4300 | diffs.splice( pointer - 1, 1 ); 4301 | changes = true; 4302 | } else if ( diffPointer.substring( 0, diffs[ pointer + 1 ][ 1 ].length ) === 4303 | diffs[ pointer + 1 ][ 1 ] ) { 4304 | 4305 | // Shift the edit over the next equality. 4306 | diffs[ pointer - 1 ][ 1 ] += diffs[ pointer + 1 ][ 1 ]; 4307 | diffs[ pointer ][ 1 ] = 4308 | diffs[ pointer ][ 1 ].substring( diffs[ pointer + 1 ][ 1 ].length ) + 4309 | diffs[ pointer + 1 ][ 1 ]; 4310 | diffs.splice( pointer + 1, 1 ); 4311 | changes = true; 4312 | } 4313 | } 4314 | pointer++; 4315 | } 4316 | 4317 | // If shifts were made, the diff needs reordering and another shift sweep. 4318 | if ( changes ) { 4319 | this.diffCleanupMerge( diffs ); 4320 | } 4321 | }; 4322 | 4323 | return function( o, n ) { 4324 | var diff, output, text; 4325 | diff = new DiffMatchPatch(); 4326 | output = diff.DiffMain( o, n ); 4327 | diff.diffCleanupEfficiency( output ); 4328 | text = diff.diffPrettyHtml( output ); 4329 | 4330 | return text; 4331 | }; 4332 | }() ); 4333 | 4334 | }() ); 4335 | -------------------------------------------------------------------------------- /unit_test/test/testData.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var Test = { 3 | add: function (a, b) { 4 | return a + b; 5 | }, 6 | 7 | al: function () { 8 | alert('hello'); 9 | return true; 10 | }, 11 | 12 | compareValue: function(a, b) { 13 | for (var key in a) { 14 | if (a[key] !== b[key]) { 15 | return; 16 | } 17 | } 18 | return true; 19 | }, 20 | 21 | obj: { 22 | a: 1, 23 | b: 2, 24 | c: 3 25 | }, 26 | 27 | arr: [1, 2, 3] 28 | 29 | }; 30 | 31 | window.Test = Test; 32 | })(); -------------------------------------------------------------------------------- /unit_test/test/van.each.test.js: -------------------------------------------------------------------------------- 1 | module('Van.each'); 2 | test('Object遍历', function () { 3 | var obj = { 4 | name: 'jiavan', 5 | age: 20 6 | }, 7 | res = { 8 | name: 'jiavan1', 9 | age: 21 10 | }; 11 | 12 | Van.each(obj, function (key, value) { 13 | obj[key] = value + 1; 14 | }); 15 | ok(Test.compareValue(obj, res), 'object passing'); 16 | }); 17 | 18 | test('Array遍历', function () { 19 | var dest = [1, 2, 3], 20 | src = [2, 3, 4]; 21 | Van.each(src, function (key, value) { 22 | src[key] = --value; 23 | }); 24 | ok(Test.compareValue(src, dest), 'array passing'); 25 | }); -------------------------------------------------------------------------------- /unit_test/test/van.extend.test.js: -------------------------------------------------------------------------------- 1 | module('Van.extend'); 2 | asyncTest('Van.extend直接挂载', function () { 3 | Van.extend({ 4 | add: Test.add, 5 | al: Test.al 6 | }); 7 | 8 | var src = {uid: "0x0001"}, 9 | result = Van.extend({}, {name: "Tom", age: 21}, 10 | {name: "Jerry", sex: "Boy"}), 11 | result2 = Van.extend(src, {name: "Tom", age: 21}, 12 | {name: "Jerry", sex: "Boy"}); 13 | 14 | equal(Van.add(1, 2), 3, '1+2等于3 passing'); 15 | equal(Test.compareValue(result, {name:"Jerry",age:21,sex:"Boy"}), 16 | true, "将两个对象合并为一个对象,不更改元对象 passing"); 17 | equal(Test.compareValue(result2, {uid: "0x0001",name:"Jerry",age:21,sex:"Boy"}), 18 | true, "将两个对象合并为一个对象,在原对象基础上扩展 passing"); 19 | start(); 20 | }); 21 | 22 | module('Van.fn.extend'); 23 | test('Van.prototype.extend原型挂载', function () { 24 | Van.prototype.extend({ 25 | add: Test.add, 26 | al: Test.al 27 | }); 28 | var src = {uid: "0x0001"}, 29 | result = Van.fn.extend({}, {name: "Tom", age: 21}, 30 | {name: "Jerry", sex: "Boy"}), 31 | result2 = Van.fn.extend(src, {name: "Tom", age: 21}, 32 | {name: "Jerry", sex: "Boy"}); 33 | 34 | equal(Van.add(1, 2), 3, '1+2等于3 passing'); 35 | equal(Test.compareValue(result, {name:"Jerry",age:21,sex:"Boy"}), 36 | true, "将两个对象合并为一个对象,不更改元对象 passing"); 37 | equal(Test.compareValue(result2, {uid: "0x0001",name:"Jerry",age:21,sex:"Boy"}), 38 | true, "将两个对象合并为一个对象,在原对象基础上扩展 passing"); 39 | }); 40 | -------------------------------------------------------------------------------- /unit_test/test/van.map.test.js: -------------------------------------------------------------------------------- 1 | module('Van.map'); 2 | test('object', function () { 3 | var src = Test.obj; 4 | var dest = { 5 | a: 2, 6 | b: 4, 7 | c: 6 8 | }; 9 | src = Van.map(function(item) { 10 | return item * 2; 11 | }); 12 | 13 | ok(Test.compareValue(src, dest), 'object passing'); 14 | }); 15 | 16 | test('array', function () { 17 | var src = Test.arr; 18 | var dest = [2, 4, 6]; 19 | src = Van.map(function(item) { 20 | return item * 2; 21 | }); 22 | 23 | ok(Test.compareValue(src, dest), 'array passing'); 24 | }); -------------------------------------------------------------------------------- /unit_test/test/van.test.js: -------------------------------------------------------------------------------- 1 | var Test = { 2 | add: function (a, b) { 3 | return a + b; 4 | }, 5 | 6 | al: function () { 7 | alert('hello'); 8 | return true; 9 | } 10 | 11 | }; 12 | 13 | module('扩展测试'); 14 | asyncTest('Van.extend直接挂载', function () { 15 | Van.extend({ 16 | add: Test.add, 17 | al: Test.al 18 | }); 19 | 20 | equal(Van.add(1, 2), 3, '1+2等于3'); 21 | //equal(Van.al(), true, 'alert hello'); 22 | // 注意异步测试时调用start以继续测试 23 | start(); 24 | }); 25 | 26 | test('Van.prototype.extend原型挂载', function () { 27 | Van.prototype.extend({ 28 | add: Test.add, 29 | al: Test.al 30 | }); 31 | equal(Van.prototype.add(1, 2), 3, '1+2等于3'); 32 | //equal(Van.prototype.al(), true, 'alert hello'); 33 | }); 34 | 35 | module('attribute相关测试'); 36 | test('获得attribute', function () { 37 | document.getElementById('qunit-fixture').setAttribute('title', 'jiavan'); 38 | var title = Van('#qunit-fixture').attr('title'); 39 | equal(title, 'jiavan', 'title的属性应该是jiavan'); 40 | }); 41 | 42 | test('设置attribute', function () { 43 | Van('#qunit-fixture').attr('title', 'jiavan'); 44 | var title = document.getElementById('qunit-fixture').getAttribute('title'); 45 | equal(title, 'jiavan', 'title属性被设置为jiavan'); 46 | }); --------------------------------------------------------------------------------