/*! * VisualEditor Base method tests. * * @copyright See AUTHORS.txt */ QUnit.module( 've' ); /* Tests */ // ve.getProp: Tested upstream (OOjs) // ve.setProp: Tested upstream (OOjs) // ve.cloneObject: Tested upstream (OOjs) // ve.getObjectValues: Tested upstream (OOjs) // ve.compare: Tested upstream (OOjs) // ve.copy: Tested upstream (OOjs) // ve.isPlainObject: Tested upstream (jQuery) // ve.isEmptyObject: Tested upstream (jQuery) // ve.extendObject: Tested upstream (jQuery) QUnit.test( 'compareClassLists', ( assert ) => { const cases = [ { args: [ '', '' ], expected: true }, { args: [ '', [] ], expected: true }, { args: [ [], [] ], expected: true }, { args: [ '', [ '' ] ], expected: true }, { args: [ [], [ '' ] ], expected: true }, { args: [ 'foo', '' ], expected: false }, { args: [ 'foo', 'foo' ], expected: true }, { args: [ 'foo', 'bar' ], expected: false }, { args: [ 'foo', 'foo bar' ], expected: false }, { args: [ 'foo', [ 'foo' ] ], expected: true }, { args: [ [ 'foo' ], 'bar' ], expected: false }, { args: [ 'foo', [ 'foo', 'bar' ] ], expected: false }, { args: [ 'foo', [ 'foo', 'foo' ] ], expected: true }, { args: [ [ 'foo' ], 'foo foo' ], expected: true }, { args: [ 'foo bar foo', 'foo foo' ], expected: false } ]; cases.forEach( ( caseItem ) => { assert.strictEqual( ve.compareClassLists( ...caseItem.args ), caseItem.expected ); } ); } ); QUnit.test( 'isInstanceOfAny', ( assert ) => { function Foo() {} OO.initClass( Foo ); function Bar() {} OO.initClass( Bar ); function SpecialFoo() {} OO.inheritClass( SpecialFoo, Foo ); function VerySpecialFoo() {} OO.inheritClass( VerySpecialFoo, SpecialFoo ); assert.true( ve.isInstanceOfAny( new Foo(), [ Foo ] ), 'Foo is an instance of Foo' ); assert.true( ve.isInstanceOfAny( new SpecialFoo(), [ Foo ] ), 'SpecialFoo is an instance of Foo' ); assert.false( ve.isInstanceOfAny( new SpecialFoo(), [ Bar ] ), 'SpecialFoo is not an instance of Bar' ); assert.true( ve.isInstanceOfAny( new SpecialFoo(), [ Bar, Foo ] ), 'SpecialFoo is an instance of Bar or Foo' ); assert.true( ve.isInstanceOfAny( new VerySpecialFoo(), [ Bar, Foo ] ), 'VerySpecialFoo is an instance of Bar or Foo' ); assert.true( ve.isInstanceOfAny( new VerySpecialFoo(), [ Foo, SpecialFoo ] ), 'VerySpecialFoo is an instance of Foo or SpecialFoo' ); assert.false( ve.isInstanceOfAny( new VerySpecialFoo(), [] ), 'VerySpecialFoo is not an instance of nothing' ); } ); QUnit.test( 'getDomAttributes', ( assert ) => { assert.deepEqual( ve.getDomAttributes( $.parseHTML( '
' )[ 0 ] ), { string: 'foo', empty: '', number: '0' }, 'getDomAttributes() returns object with correct attributes' ); } ); QUnit.test( 'setDomAttributes', ( assert ) => { const sample = $.parseHTML( '' )[ 0 ]; let target = {}; ve.setDomAttributes( target, { add: 'foo' } ); assert.deepEqual( target, {}, 'ignore incompatible target object' ); target = document.createElement( 'div' ); ve.setDomAttributes( target, { string: 'foo', empty: '', number: 0 } ); assert.deepEqual( ve.getDomAttributes( target ), { string: 'foo', empty: '', number: '0' }, 'add attributes' ); target = sample.cloneNode(); ve.setDomAttributes( target, { foo: null, bar: 'update', baz: undefined, add: 'yay' } ); assert.deepEqual( ve.getDomAttributes( target ), { bar: 'update', add: 'yay' }, 'add, update, and remove attributes' ); target = sample.cloneNode(); ve.setDomAttributes( target, { onclick: 'alert(1);', foo: 'update', add: 'whee' }, [ 'foo', 'add' ] ); assert.false( target.hasAttribute( 'onclick' ), 'allow list affects creating attributes' ); assert.deepEqual( ve.getDomAttributes( target ), { foo: 'update', bar: 'two', baz: 'three', add: 'whee' }, 'allow list does not affect pre-existing attributes' ); target = document.createElement( 'div' ); ve.setDomAttributes( target, { Foo: 'add', Bar: 'add' }, [ 'bar' ] ); assert.deepEqual( ve.getDomAttributes( target ), { bar: 'add' }, 'allow list is case-insensitive' ); target = sample.cloneNode(); ve.setDomAttributes( target, { foo: 'update', bar: null }, [ 'bar', 'baz' ] ); assert.propEqual( ve.getDomAttributes( target ), { foo: 'one', baz: 'three' }, 'allow list affects removal/updating of attributes' ); } ); QUnit.test( 'sparseSplice', ( assert ) => { // Convert a sparse array of primitives to an array of strings, with '' for holes. // This is needed because QUnit.equiv treats holes as equivalent to undefined. function mapToString( flatArray ) { const strings = []; for ( let j = 0, jLen = flatArray.length; j < jLen; j++ ) { strings.push( Object.prototype.hasOwnProperty.call( flatArray, j ) ? String( flatArray[ j ] ) : '' ); } return strings; } function runTest( arr, offset, remove, data, expectedReturn, expectedArray, msg ) { const testArr = arr.slice(); const observedReturn = ve.sparseSplice( testArr, offset, remove, data ); assert.deepEqual( mapToString( observedReturn ), mapToString( expectedReturn ), msg + ': return' ); assert.deepEqual( mapToString( testArr ), mapToString( expectedArray ), msg + ': modification' ); } /* eslint-disable no-sparse-arrays */ const scratch = [ 4, , 5, , 6 ]; const cases = [ // arr, offset, remove, data, expectedReturn, expectedArray, msg [ [], 0, 0, [ , 3 ], [], [ , 3 ], 'insert empty, leading hole' ], [ [], 0, 0, [ 1, , 3 ], [], [ 1, , 3 ], 'insert empty, middle hole' ], // Note: the first trailing comma does not create a hole [ [], 0, 0, [ 1, , ], [], [ 1, , ], 'insert empty, trailing hole' ], [ [ 4, , 5 ], 0, 0, [ 1, , 3 ], [], [ 1, , 3, 4, , 5 ], 'insert start' ], [ [ 0, , 4 ], 1, 0, [ 1, , 3 ], [], [ 0, 1, , 3, , 4 ], 'insert mid' ], [ [ 0, , 4 ], 3, 0, [ 1, , 3 ], [], [ 0, , 4, 1, , 3 ], 'insert end' ], [ [ 4, , 5, , 6 ], 0, 4, [ 1, , 3 ], [ 4, , 5, , ], [ 1, , 3, 6 ], 'diff<0 start' ], [ [ 4, , , 5, , 6 ], 1, 4, [ 1, , 3 ], [ , , 5, , ], [ 4, 1, , 3, 6 ], 'diff<0 mid' ], [ [ 4, , 5, , 6 ], 1, 4, [ 1, , 3 ], [ , 5, , 6 ], [ 4, 1, , 3 ], 'diff<0 end' ], [ [ 4, , 5, , 6 ], 0, 2, [ 1, , 3 ], [ 4, , ], [ 1, , 3, 5, , 6 ], 'diff>0 start' ], [ [ 4, , 5, , 6 ], 1, 2, [ 1, , 3 ], [ , 5 ], [ 4, 1, , 3, , 6 ], 'diff>0 mid' ], [ [ 4, , 5, , 6 ], 3, 2, [ 1, , 3 ], [ , 6 ], [ 4, , 5, 1, , 3 ], 'diff>0 end' ], [ [ 4, , 5, , 6 ], 0, 3, [ 1, , 3 ], [ 4, , 5 ], [ 1, , 3, , 6 ], 'diff=0 start' ], [ [ 4, , 5, , 6 ], 1, 3, [ 1, , 3 ], [ , 5, , ], [ 4, 1, , 3, 6 ], 'diff=0 mid' ], [ [ 4, , 5, , 6 ], 2, 3, [ 1, , 3 ], [ 5, , 6 ], [ 4, , 1, , 3 ], 'diff=0 end' ], [ scratch, 0, 0, scratch, [], [ 4, , 5, , 6, 4, , 5, , 6 ], 'reference-identical arr and data' ] ]; /* eslint-enable no-sparse-arrays */ assert.notDeepEqual( // eslint-disable-next-line no-sparse-arrays mapToString( [ 1, , ] ), mapToString( [ 1, undefined ] ), 'holes look different to undefined' ); cases.forEach( ( caseItem ) => { runTest( ...caseItem ); } ); } ); QUnit.test( 'batchSplice', ( assert ) => { const actual = [ ...'abcde' ], expected = actual.slice( 0 ); let actualRet = ve.batchSplice( actual, 1, 1, [] ); let expectedRet = expected.splice( 1, 1 ); assert.deepEqual( expectedRet, actualRet, 'Removing 1 element (return value)' ); assert.deepEqual( expected, actual, 'Removing 1 element (array)' ); actualRet = ve.batchSplice( actual, 3, 2, [ ...'wxyz' ] ); expectedRet = expected.splice( 3, 2, ...'wxyz' ); assert.deepEqual( expectedRet, actualRet, 'Replacing 2 elements with 4 elements (return value)' ); assert.deepEqual( expected, actual, 'Replacing 2 elements with 4 elements (array)' ); actualRet = ve.batchSplice( actual, 0, 0, [ ...'Foo' ] ); expectedRet = expected.splice( 0, 0, ...'Foo' ); assert.deepEqual( expectedRet, actualRet, 'Inserting 3 elements (return value)' ); assert.deepEqual( expected, actual, 'Inserting 3 elements (array)' ); const bigArr = []; for ( let i = 0; i < 2100; i++ ) { bigArr[ i ] = i; } actualRet = ve.batchSplice( actual, 2, 3, bigArr ); expectedRet = expected.splice( 2, 3, ...bigArr.slice( 0, 1050 ) ); expected.splice( 1052, 0, ...bigArr.slice( 1050 ) ); assert.deepEqual( expectedRet, actualRet, 'Replacing 3 elements with 2100 elements (return value)' ); assert.deepEqual( expected, actual, 'Replacing 3 elements with 2100 elements (array)' ); } ); QUnit.test( 'batchPush', ( assert ) => { let actual = []; let actualRet = ve.batchPush( actual, [ 1, 2, 3 ] ); assert.strictEqual( actualRet, 3, 'Adding to an empty array: return' ); assert.deepEqual( actual, [ 1, 2, 3 ], 'Adding to an empty array: value' ); actual = [ 1 ]; actualRet = ve.batchPush( actual, [ 1, 2, 3 ] ); assert.strictEqual( actualRet, 4, 'Adding to a non-empty array: return' ); assert.deepEqual( actual, [ 1, 1, 2, 3 ], 'Adding to a non-empty array: value' ); // batchPush takes a separate codepath for really long arrays, make sure it's behaving similarly: const bigArr = []; for ( let i = 0; i < 2100; i++ ) { bigArr[ i ] = i; } actual = [ 'a' ]; actualRet = ve.batchPush( actual, bigArr ); assert.strictEqual( actualRet, 2101, 'Adding a huge array: return' ); assert.strictEqual( actual[ 0 ], 'a', 'Adding a huge array: first value' ); assert.strictEqual( actual[ actual.length - 1 ], 2099, 'Adding a huge array: last value' ); } ); QUnit.test( 'insertIntoArray', ( assert ) => { let target = [ ...'abc' ]; ve.insertIntoArray( target, 0, [ ...'xy' ] ); assert.deepEqual( target, [ ...'xyabc' ], 'insert at start' ); target = [ ...'abc' ]; ve.insertIntoArray( target, 2, [ ...'xy' ] ); assert.deepEqual( target, [ ...'abxyc' ], 'insert into the middle' ); target = [ ...'abc' ]; ve.insertIntoArray( target, 10, [ ...'xy' ] ); assert.deepEqual( target, [ ...'abcxy' ], 'insert beyond end' ); } ); QUnit.test( 'escapeHtml', ( assert ) => { assert.strictEqual( ve.escapeHtml( ' "script\'foo
', expected: 'foo
' }, { msg: 'body only', html: 'foo
', expected: 'foo
' }, { msg: 'head & body', html: 'foo
', expected: 'foo
' }, { msg: 'html & body', html: 'foo
', expected: 'foo
' }, { msg: 'html, head & body', html: 'foo
', expected: 'foo
' } ]; cases.forEach( ( caseItem ) => { assert.strictEqual( ve.addHeadTag( caseItem.html, '' ), caseItem.expected, caseItem.msg ); } ); } ); QUnit.test( 'createDocumentFromHtml', ( assert ) => { const cases = [ { msg: 'simple document with doctype, head and body', html: 'Bar
', head: 'Bar
', htmlAttributes: { lang: 'en' } }, { msg: 'simple document without doctype', html: 'Bar
', head: 'Bar
', htmlAttributes: { lang: 'en' } }, { msg: 'document with missing closing tags and missing tag', html: 'BarBaz',
head: '
BarBaz
', htmlAttributes: {} }, { msg: 'empty string results in empty document', html: '', head: '', body: '', htmlAttributes: {} }, { msg: 'meta tag stays in body', html: 'foo
', head: '', body: 'foo
', htmlAttributes: {} }, { msg: 'body wrapping not broken by custom tag', html: 'foo
foo
AABBCC
x
', options: { stop: () => true }, expectedOffsetPaths: [ [ 0 ], [ 0, 0 ], [ 0, 0, 0 ], [ 0, 0, 1 ], [ 0, 1 ], [ 1 ] ] }, { title: 'Filtered descent', html: 'foo bar baz