/*! * VisualEditor Base method tests. * * @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt * @license The MIT License (MIT); see LICENSE.txt */ QUnit.module( 've' ); /* Tests */ // ve.getObjectKeys: Untested (TODO) QUnit.test( 'createObject', 4, function ( assert ) { var foo, bar, fooKeys, barKeys; foo = { a: 'a of foo', b: 'b of foo' }; bar = ve.createObject( foo ); // Add an own property, hiding the inherited one. bar.b = 'b of bar'; // Add an own property, hiding an inherited property // that will be added later bar.c = 'c of bar'; // Add more properties to the origin object, // should be visible in the inheriting object. foo.c = 'c of foo'; foo.d = 'd of foo'; // Different property that only one of each has foo.foo = true; bar.bar = true; assert.deepEqual( foo, { a: 'a of foo', b: 'b of foo', c: 'c of foo', d: 'd of foo', foo: true }, 'Foo has expected properties' ); assert.deepEqual( bar, { a: 'a of foo', b: 'b of bar', c: 'c of bar', d: 'd of foo', foo: true, bar: true }, 'Bar has expected properties' ); fooKeys = ve.getObjectKeys( foo ); barKeys = ve.getObjectKeys( bar ); assert.deepEqual( fooKeys, ['a', 'b', 'c', 'd', 'foo'], 'Own properties of foo' ); assert.deepEqual( barKeys, ['b', 'c', 'bar'], 'Own properties of bar' ); } ); QUnit.test( 'inheritClass', 18, function ( assert ) { var foo, bar; function Foo() { this.constructedFoo = true; } Foo.a = 'prop of Foo'; Foo.b = 'prop of Foo'; Foo.prototype.b = 'proto of Foo'; Foo.prototype.c = 'proto of Foo'; Foo.prototype.bFn = function () { return 'proto of Foo'; }; Foo.prototype.cFn = function () { return 'proto of Foo'; }; foo = new Foo(); function Bar() { this.constructedBar = true; } ve.inheritClass( Bar, Foo ); assert.deepEqual( Foo.static, {}, 'A "static" property (empty object) is automatically created if absent' ); Foo.static.a = 'static of Foo'; Foo.static.b = 'static of Foo'; assert.notStrictEqual( Foo.static, Bar.static, 'Static property is not copied, but inheriting' ); assert.equal( Bar.static.a, 'static of Foo', 'Foo.static inherits from Bar.static' ); Bar.static.b = 'static of Bar'; assert.equal( Foo.static.b, 'static of Foo', 'Change to Bar.static does not affect Foo.static' ); Bar.a = 'prop of Bar'; Bar.prototype.b = 'proto of Bar'; Bar.prototype.bFn = function () { return 'proto of Bar'; }; assert.throws( function () { ve.inheritClass( Bar, Foo ); }, 'Throw if target already inherits from source (from an earlier call)' ); assert.throws( function () { ve.inheritClass( Bar, Object ); }, 'Throw if target already inherits from source (naturally, Object)' ); bar = new Bar(); assert.strictEqual( Bar.b, undefined, 'Constructor properties are not inherited' ); assert.strictEqual( foo instanceof Foo, true, 'foo instance of Foo' ); assert.strictEqual( foo instanceof Bar, false, 'foo not instance of Bar' ); assert.strictEqual( bar instanceof Foo, true, 'bar instance of Foo' ); assert.strictEqual( bar instanceof Bar, true, 'bar instance of Bar' ); assert.equal( bar.constructor, Bar, 'constructor property is restored' ); assert.equal( bar.b, 'proto of Bar', 'own methods go first' ); assert.equal( bar.bFn(), 'proto of Bar', 'own properties go first' ); assert.equal( bar.c, 'proto of Foo', 'prototype properties are inherited' ); assert.equal( bar.cFn(), 'proto of Foo', 'prototype methods are inherited' ); Bar.prototype.dFn = function () { return 'proto of Bar'; }; Foo.prototype.dFn = function () { return 'proto of Foo'; }; Foo.prototype.eFn = function () { return 'proto of Foo'; }; assert.equal( bar.dFn(), 'proto of Bar', 'inheritance is live (overwriting an inherited method)' ); assert.equal( bar.eFn(), 'proto of Foo', 'inheritance is live (adding a new method deeper in the chain)' ); } ); QUnit.test( 'mixinClass', 4, function ( assert ) { var quux; function Foo() {} Foo.prototype.aFn = function () { return 'proto of Foo'; }; function Bar() {} // ve.inheritClass makes the 'constructor' // property an own property when it restores it. ve.inheritClass( Bar, Foo ); Bar.prototype.bFn = function () { return 'mixin of Bar'; }; function Quux() {} ve.mixinClass( Quux, Bar ); assert.strictEqual( Quux.prototype.aFn, undefined, 'mixin inheritance is not copied over' ); assert.strictEqual( Quux.prototype.constructor, Quux, 'constructor property skipped' ); assert.strictEqual( Quux.prototype.hasOwnProperty( 'bFn' ), true, 'mixin properties are now own properties, not inherited' ); quux = new Quux(); assert.equal( quux.bFn(), 'mixin of Bar', 'mixin method works as expected' ); } ); QUnit.test( 'isMixedIn', 11, function ( assert ) { function Foo () {} function Bar () {} function Quux () {} ve.inheritClass( Quux, Foo ); ve.mixinClass( Quux, Bar ); var b = new Bar(), q = new Quux(); assert.strictEqual( ve.isMixedIn( Foo, Function ), false, 'Direct native inheritance is not considered' ); assert.strictEqual( ve.isMixedIn( Foo, Object ), false, 'Indirect native inheritance is not considered' ); assert.strictEqual( ve.isMixedIn( Quux, Foo ), false, 've.inheritClass does not affect mixin status' ); assert.strictEqual( ve.isMixedIn( Foo, Foo ), false, 'Foo does not mixin Foo' ); assert.strictEqual( ve.isMixedIn( Bar, Foo ), false, 'Bar does not mixin Foo' ); assert.strictEqual( ve.isMixedIn( Quux, Bar ), true, 'Quux has Bar mixed in' ); assert.strictEqual( ve.isMixedIn( Bar, Quux ), false, 'Bar does not mixin Quux' ); assert.strictEqual( ve.isMixedIn( q, Foo ), false, 've.inheritClass does not affect mixin status' ); assert.strictEqual( ve.isMixedIn( b, Foo ), false, 'b does not mixin Foo' ); assert.strictEqual( ve.isMixedIn( q, Bar ), true, 'q has Bar mixed in' ); assert.strictEqual( ve.isMixedIn( b, Quux ), false, 'b does not mixin Quux' ); } ); QUnit.test( 'cloneObject', 4, function ( assert ) { var myfoo, myfooClone, expected; function Foo( x ) { this.x = x; } Foo.prototype.x = 'default'; Foo.prototype.aFn = function () { return 'proto of Foo'; }; myfoo = new Foo( 10 ); myfooClone = ve.cloneObject( myfoo ); assert.notStrictEqual( myfoo, myfooClone, 'clone is not equal when compared by reference' ); assert.deepEqual( myfoo, myfooClone, 'clone is equal when recursively compared by value' ); expected = { x: 10, aFn: 'proto of Foo', constructor: Foo, instanceOf: true, own: { x: true, aFn: false, constructor: false } }; assert.deepEqual( { x: myfoo.x, aFn: myfoo.aFn(), constructor: myfoo.constructor, instanceOf: myfoo instanceof Foo, own: { x: myfoo.hasOwnProperty( 'x' ), aFn: myfoo.hasOwnProperty( 'aFn' ), constructor: myfoo.hasOwnProperty( 'constructor' ) } }, expected, 'original looks as expected' ); assert.deepEqual( { x: myfooClone.x, aFn: myfooClone.aFn(), constructor: myfooClone.constructor, instanceOf: myfooClone instanceof Foo, own: { x: myfooClone.hasOwnProperty( 'x' ), aFn: myfooClone.hasOwnProperty( 'aFn' ), constructor: myfoo.hasOwnProperty( 'constructor' ) } }, expected, 'clone looks as expected' ); } ); // ve.isPlainObject: Tested upstream (jQuery) // ve.isEmptyObject: Tested upstream (jQuery) // ve.isArray: Tested upstream (jQuery) // ve.bind: Tested upstream (jQuery) // ve.indexOf: Tested upstream (jQuery) // ve.extendObject: Tested upstream (jQuery) QUnit.test( 'getHash: Basic usage', 7, function ( assert ) { var tmp, cases = {}, hash = '{"a":1,"b":1,"c":1}', customHash = '{"first":1,"last":1}'; cases['a-z literal'] = { object: { a: 1, b: 1, c: 1 }, hash: hash }; cases['z-a literal'] = { object: { c: 1, b: 1, a: 1 }, hash: hash }; tmp = {}; cases['a-z augmented'] = { object: tmp, hash: hash }; tmp.a = 1; tmp.b = 1; tmp.c = 1; tmp = {}; cases['z-a augmented'] = { object: tmp, hash: hash }; tmp.c = 1; tmp.b = 1; tmp.a = 1; cases['custom hash'] = { object: { getHashObject: function () { return { 'first': 1, 'last': 1 }; } }, hash: customHash }; cases['custom hash reversed'] = { object: { getHashObject: function () { return { 'last': 1, 'first': 1 }; } }, hash: customHash }; $.each( cases, function ( key, val ) { assert.equal( ve.getHash( val.object ), val.hash, key + ': object has expected hash, regardless of "property order"' ); } ); // .. and that something completely different is in face different // (just incase getHash is broken and always returns the same) assert.notEqual( ve.getHash( { a: 2, b: 2 } ), hash, 'A different object has a different hash' ); } ); QUnit.test( 'getHash: Complex usage', 3, function ( assert ) { var obj, hash, frame; obj = { a: 1, b: 1, c: 1, // Nested array d: ['x', 'y', 'z'], e: { a: 2, b: 2, c: 2 } }; assert.equal( ve.getHash( obj ), '{"a":1,"b":1,"c":1,"d":["x","y","z"],"e":{"a":2,"b":2,"c":2}}', 'Object with nested array and nested object' ); // Include a circular reference /* * PhantomJS hangs when calling JSON.stringify with an object containing a * circular reference (https://github.com/ariya/phantomjs/issues/11206). * We know latest Chrome/Firefox and IE8+ support this. So, for the sake of * having qunit/phantomjs work, lets disable this for now. obj.f = obj; assert.throws( function () { ve.getHash( obj ); }, 'Throw exceptions for objects with cirular refences ' ); */ function Foo() { this.a = 1; this.c = 3; this.b = 2; } hash = '{"a":1,"b":2,"c":3}'; assert.equal( ve.getHash( new Foo() ), hash, // This was previously broken when we used .constructor === Object // ve.getHash.keySortReplacer, because although instances of Foo // do inherit from Object (( new Foo() ) instanceof Object === true), // direct comparison would return false. 'Treat objects constructed by a function as well' ); frame = document.createElement( 'frame' ); frame.src = 'about:blank'; $( '#qunit-fixture' ).append( frame ); obj = new frame.contentWindow.Object(); obj.c = 3; obj.b = 2; obj.a = 1; assert.equal( ve.getHash( obj ), hash, // This was previously broken when we used comparison with "Object" in // ve.getHash.keySortReplacer, because they are an instance of the other // window's "Object". 'Treat objects constructed by a another window as well' ); } ); QUnit.test( 'getObjectValues', 6, function ( assert ) { var tmp; assert.deepEqual( ve.getObjectValues( { a: 1, b: 2, c: 3, foo: 'bar' } ), [ 1, 2, 3, 'bar' ], 'Simple object with numbers and strings as values' ); assert.deepEqual( ve.getObjectValues( [ 1, 2, 3, 'bar' ] ), [ 1, 2, 3, 'bar' ], 'Simple array with numbers and strings as values' ); tmp = function () { this.isTest = true; return this; }; tmp.a = 'foo'; tmp.b = 'bar'; assert.deepEqual( ve.getObjectValues( tmp ), ['foo', 'bar'], 'Function with properties' ); assert.throws( function () { ve.getObjectValues( 'hello' ); }, TypeError, 'Throw exception for non-object (string)' ); assert.throws( function () { ve.getObjectValues( 123 ); }, TypeError, 'Throw exception for non-object (number)' ); assert.throws( function () { ve.getObjectValues( null ); }, TypeError, 'Throw exception for non-object (null)' ); } ); QUnit.test( 'copyArray', 7, function ( assert ) { var simpleArray = [ 'foo', 3, true, false ], withObj = [ { 'bar': 'baz', 'quux': 3 }, 5, null ], nestedArray = [ [ 'a', 'b' ], [ 1, 3, 4 ] ], sparseArray = [ 'a', undefined, undefined, 'b' ], withSparseArray = [ [ 'a', undefined, undefined, 'b' ] ], withFunction = [ function () { return true; } ], Cloneable = function ( p ) { this.p = p; }; Cloneable.prototype.clone = function () { return new Cloneable( this.p + '-clone' ); }; assert.deepEqual( ve.copyArray( simpleArray ), simpleArray, 'Simple array' ); assert.deepEqual( ve.copyArray( withObj ), withObj, 'Array containing object' ); assert.deepEqual( ve.copyArray( [ new Cloneable( 'bar' ) ] ), [ new Cloneable( 'bar-clone' ) ], 'Use the .clone() method if available' ); assert.deepEqual( ve.copyArray( nestedArray ), nestedArray, 'Nested array' ); assert.deepEqual( ve.copyArray( sparseArray ), sparseArray, 'Sparse array' ); assert.deepEqual( ve.copyArray( withSparseArray ), withSparseArray, 'Nested sparse array' ); assert.deepEqual( ve.copyArray( withFunction ), withFunction, 'Array containing function' ); } ); QUnit.test( 'copyObject', 7, function ( assert ) { var simpleObj = { 'foo': 'bar', 'baz': 3, 'quux': null, 'truth': true, 'falsehood': false }, nestedObj = { 'foo': { 'bar': 'baz', 'quux': 3 }, 'whee': 5 }, withArray = { 'foo': [ 'a', 'b' ], 'bar': [ 1, 3, 4 ] }, withSparseArray = { 'foo': [ 'a', undefined, undefined, 'b' ] }, withFunction = { 'func': function () { return true; } }, Cloneable = function ( p ) { this.p = p; }; Cloneable.prototype.clone = function () { return new Cloneable( this.p + '-clone' ); }; assert.deepEqual( ve.copyObject( simpleObj ), simpleObj, 'Simple object' ); assert.deepEqual( ve.copyObject( nestedObj ), nestedObj, 'Nested object' ); assert.deepEqual( ve.copyObject( new Cloneable( 'foo' ) ), new Cloneable( 'foo-clone' ), 'Cloneable object' ); assert.deepEqual( ve.copyObject( { 'foo': new Cloneable( 'bar' ) } ), { 'foo': new Cloneable( 'bar-clone' ) }, 'Object containing object' ); assert.deepEqual( ve.copyObject( withArray ), withArray, 'Object with array' ); assert.deepEqual( ve.copyObject( withSparseArray ), withSparseArray, 'Object with sparse array' ); assert.deepEqual( ve.copyObject( withFunction ), withFunction, 'Object with function' ); } ); QUnit.test( 'getDomAttributes', 1, function ( assert ) { assert.deepEqual( ve.getDomAttributes( $( '
').get( 0 ) ), { 'foo': 'bar', 'baz': '', 'quux': '3' }, 'getDomAttributes() returns object with correct attributes' ); } ); QUnit.test( 'setDomAttributes', 3, function ( assert ) { var element = document.createElement( 'div' ); ve.setDomAttributes( element, { 'foo': 'bar', 'baz': '', 'quux': 3 } ); assert.deepEqual( ve.getDomAttributes( element ), { 'foo': 'bar', 'baz': '', 'quux': '3' }, 'setDomAttributes() sets attributes correctly' ); ve.setDomAttributes( element, { 'foo': null, 'bar': 1, 'baz': undefined, 'quux': 5, 'whee': 'yay' } ); assert.deepEqual( ve.getDomAttributes( element ), { 'bar': '1', 'quux': '5', 'whee': 'yay' }, 'setDomAttributes() overwrites attributes, removes attributes, and sets new attributes' ); ve.setDomAttributes( element, { 'onclick': 'alert(1);' }, ['foo', 'bar', 'baz', 'quux', 'whee'] ); assert.ok( !element.hasAttribute( 'onclick' ), 'event attributes are blocked when sanitizing' ); } ); QUnit.test( 'getOpeningHtmlTag', 5, function ( assert ) { assert.deepEqual( ve.getOpeningHtmlTag( 'code', {} ), '', 'opening tag without attributes' ); assert.deepEqual( ve.getOpeningHtmlTag( 'img', { 'src': 'foo' } ), '', 'opening tag with one attribute' ); assert.deepEqual( ve.getOpeningHtmlTag( 'a', { 'href': 'foo', 'rel': 'bar' } ), '', 'tag with two attributes' ); assert.deepEqual( ve.getOpeningHtmlTag( 'option', { 'selected': true, 'blah': false, 'value': 3 } ), '