%PDF- %PDF-
| Direktori : /www/varak.net/wiki.varak.net/extensions/VisualEditor/lib/ve/tests/ |
| Current File : //www/varak.net/wiki.varak.net/extensions/VisualEditor/lib/ve/tests/ve.test.js |
/*!
* VisualEditor Base method tests.
*
* @copyright 2011-2016 VisualEditor Team and others; see http://ve.mit-license.org
*/
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', 1, function ( assert ) {
var i, 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
}
];
QUnit.expect( cases.length );
for ( i = 0; i < cases.length; i++ ) {
assert.strictEqual( ve.compareClassLists.apply( ve, cases[ i ].args ), cases[ i ].expected );
}
} );
QUnit.test( 'isInstanceOfAny', 7, function ( assert ) {
function Foo() {}
OO.initClass( Foo );
function Bar() {}
OO.initClass( Bar );
function SpecialFoo() {}
OO.inheritClass( SpecialFoo, Foo );
function VerySpecialFoo() {}
OO.inheritClass( VerySpecialFoo, SpecialFoo );
assert.strictEqual(
ve.isInstanceOfAny( new Foo(), [ Foo ] ),
true,
'Foo is an instance of Foo'
);
assert.strictEqual(
ve.isInstanceOfAny( new SpecialFoo(), [ Foo ] ),
true,
'SpecialFoo is an instance of Foo'
);
assert.strictEqual(
ve.isInstanceOfAny( new SpecialFoo(), [ Bar ] ),
false,
'SpecialFoo is not an instance of Bar'
);
assert.strictEqual(
ve.isInstanceOfAny( new SpecialFoo(), [ Bar, Foo ] ),
true,
'SpecialFoo is an instance of Bar or Foo'
);
assert.strictEqual(
ve.isInstanceOfAny( new VerySpecialFoo(), [ Bar, Foo ] ),
true,
'VerySpecialFoo is an instance of Bar or Foo'
);
assert.strictEqual(
ve.isInstanceOfAny( new VerySpecialFoo(), [ Foo, SpecialFoo ] ),
true,
'VerySpecialFoo is an instance of Foo or SpecialFoo'
);
assert.strictEqual(
ve.isInstanceOfAny( new VerySpecialFoo(), [] ),
false,
'VerySpecialFoo is not an instance of nothing'
);
} );
QUnit.test( 'getDomAttributes', 1, function ( assert ) {
assert.deepEqual(
ve.getDomAttributes( $.parseHTML( '<div string="foo" empty number="0"></div>' )[ 0 ] ),
{ string: 'foo', empty: '', number: '0' },
'getDomAttributes() returns object with correct attributes'
);
} );
QUnit.test( 'setDomAttributes', 7, function ( assert ) {
var target,
sample = $.parseHTML( '<div foo="one" bar="two" baz="three"></div>' )[ 0 ];
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.ok( !target.hasAttribute( 'onclick' ), 'whitelist affects creating attributes' );
assert.deepEqual(
ve.getDomAttributes( target ),
{ foo: 'update', bar: 'two', baz: 'three', add: 'whee' },
'whitelist 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' },
'whitelist is case-insensitive'
);
target = sample.cloneNode();
ve.setDomAttributes( target, { foo: 'update', bar: null }, [ 'bar', 'baz' ] );
assert.propEqual(
ve.getDomAttributes( target ),
{ foo: 'one', baz: 'three' },
'whitelist affects removal/updating of attributes'
);
} );
QUnit.test( 'getHtmlAttributes', 7, function ( assert ) {
assert.deepEqual(
ve.getHtmlAttributes(),
'',
'no attributes argument'
);
assert.deepEqual(
ve.getHtmlAttributes( NaN + 'px' ),
'',
'invalid attributes argument'
);
assert.deepEqual(
ve.getHtmlAttributes( {} ),
'',
'empty attributes argument'
);
assert.deepEqual(
ve.getHtmlAttributes( { src: 'foo' } ),
'src="foo"',
'one attribute'
);
assert.deepEqual(
ve.getHtmlAttributes( { href: 'foo', rel: 'bar' } ),
'href="foo" rel="bar"',
'two attributes'
);
assert.deepEqual(
ve.getHtmlAttributes( { selected: true, blah: false, value: 3 } ),
'selected="selected" value="3"',
'handling of booleans and numbers'
);
assert.deepEqual(
ve.getHtmlAttributes( { placeholder: '<foo>&"bar"&\'baz\'' } ),
'placeholder="<foo>&"bar"&'baz'"',
'escaping of attribute values'
);
} );
QUnit.test( 'getOpeningHtmlTag', 3, function ( assert ) {
assert.deepEqual(
ve.getOpeningHtmlTag( 'code', {} ),
'<code>',
'opening tag without attributes'
);
assert.deepEqual(
ve.getOpeningHtmlTag( 'img', { src: 'foo' } ),
'<img src="foo">',
'opening tag with one attribute'
);
assert.deepEqual(
ve.getOpeningHtmlTag( 'a', { href: 'foo', rel: 'bar' } ),
'<a href="foo" rel="bar">',
'tag with two attributes'
);
} );
QUnit.test( 'batchSplice', function ( assert ) {
var spliceWasSupported = ve.supportsSplice;
function assertBatchSplice() {
var actualRet, expectedRet, msg, i,
actual = [ 'a', 'b', 'c', 'd', 'e' ],
expected = actual.slice( 0 ),
bigArr = [];
msg = ve.supportsSplice ? 'Array#splice native' : 'Array#splice polyfill';
actualRet = ve.batchSplice( actual, 1, 1, [] );
expectedRet = expected.splice( 1, 1 );
assert.deepEqual( expectedRet, actualRet, msg + ': removing 1 element (return value)' );
assert.deepEqual( expected, actual, msg + ': removing 1 element (array)' );
actualRet = ve.batchSplice( actual, 3, 2, [ 'w', 'x', 'y', 'z' ] );
expectedRet = expected.splice( 3, 2, 'w', 'x', 'y', 'z' );
assert.deepEqual( expectedRet, actualRet, msg + ': replacing 2 elements with 4 elements (return value)' );
assert.deepEqual( expected, actual, msg + ': replacing 2 elements with 4 elements (array)' );
actualRet = ve.batchSplice( actual, 0, 0, [ 'f', 'o', 'o' ] );
expectedRet = expected.splice( 0, 0, 'f', 'o', 'o' );
assert.deepEqual( expectedRet, actualRet, msg + ': inserting 3 elements (return value)' );
assert.deepEqual( expected, actual, msg + ': inserting 3 elements (array)' );
for ( i = 0; i < 2100; i++ ) {
bigArr[ i ] = i;
}
actualRet = ve.batchSplice( actual, 2, 3, bigArr );
expectedRet = expected.splice.apply( expected, [ 2, 3 ].concat( bigArr.slice( 0, 1050 ) ) );
expected.splice.apply( expected, [ 1052, 0 ].concat( bigArr.slice( 1050 ) ) );
assert.deepEqual( expectedRet, actualRet, msg + ': replacing 3 elements with 2100 elements (return value)' );
assert.deepEqual( expected, actual, msg + ': replacing 3 elements with 2100 elements (array)' );
}
QUnit.expect( 8 * ( spliceWasSupported ? 2 : 1 ) );
assertBatchSplice();
// If the current browser supported native splice,
// test again without the native splice.
if ( spliceWasSupported ) {
ve.supportsSplice = false;
assertBatchSplice();
ve.supportsSplice = true;
}
} );
QUnit.test( 'insertIntoArray', 3, function ( assert ) {
var target;
target = [ 'a', 'b', 'c' ];
ve.insertIntoArray( target, 0, [ 'x', 'y' ] );
assert.deepEqual( target, [ 'x', 'y', 'a', 'b', 'c' ], 'insert at start' );
target = [ 'a', 'b', 'c' ];
ve.insertIntoArray( target, 2, [ 'x', 'y' ] );
assert.deepEqual( target, [ 'a', 'b', 'x', 'y', 'c' ], 'insert into the middle' );
target = [ 'a', 'b', 'c' ];
ve.insertIntoArray( target, 10, [ 'x', 'y' ] );
assert.deepEqual( target, [ 'a', 'b', 'c', 'x', 'y' ], 'insert beyond end' );
} );
QUnit.test( 'escapeHtml', 1, function ( assert ) {
assert.strictEqual( ve.escapeHtml( ' "script\' <foo & bar> ' ), ' "script' <foo & bar> ' );
} );
QUnit.test( 'createDocumentFromHtml', function ( assert ) {
var doc, expectedHead, expectedBody,
supportsDomParser = !!ve.createDocumentFromHtmlUsingDomParser( '' ),
supportsIframe = !!ve.createDocumentFromHtmlUsingIframe( '' ),
cases = [
{
msg: 'simple document with doctype, head and body',
html: '<!doctype html><html lang="en"><head><title>Foo</title></head><body><p>Bar</p></body></html>',
head: '<title>Foo</title>',
body: '<p>Bar</p>',
htmlAttributes: {
lang: 'en'
}
},
{
msg: 'simple document without doctype',
html: '<html lang="en"><head><title>Foo</title></head><body><p>Bar</p></body></html>',
head: '<title>Foo</title>',
body: '<p>Bar</p>',
htmlAttributes: {
lang: 'en'
}
},
{
msg: 'document with missing closing tags and missing <html> tag',
html: '<!doctype html><head><title>Foo</title><base href="yay"><body><p>Bar<b>Baz',
head: '<title>Foo</title><base href="yay" />',
body: '<p>Bar<b>Baz</b></p>',
htmlAttributes: {}
},
{
msg: 'empty string results in empty document',
html: '',
head: '',
body: '',
htmlAttributes: {}
}
];
QUnit.expect( cases.length * 3 * ( 2 + ( supportsDomParser ? 1 : 0 ) + ( supportsIframe ? 1 : 0 ) ) );
function assertCreateDocument( createDocument, msg ) {
var i, key, attributes, attributesObject;
for ( key in cases ) {
doc = createDocument( cases[ key ].html );
attributes = $( 'html', doc ).get( 0 ).attributes;
attributesObject = {};
for ( i = 0; i < attributes.length; i++ ) {
attributesObject[ attributes[ i ].name ] = attributes[ i ].value;
}
expectedHead = $( '<head>' ).html( cases[ key ].head ).get( 0 );
expectedBody = $( '<body>' ).html( cases[ key ].body ).get( 0 );
assert.equalDomElement( $( 'head', doc ).get( 0 ), expectedHead, msg + ': ' + cases[ key ].msg + ' (head)' );
assert.equalDomElement( $( 'body', doc ).get( 0 ), expectedBody, msg + ': ' + cases[ key ].msg + ' (body)' );
assert.deepEqual( attributesObject, cases[ key ].htmlAttributes, msg + ': ' + cases[ key ].msg + ' (html attributes)' );
}
}
if ( supportsDomParser ) {
assertCreateDocument( ve.createDocumentFromHtmlUsingDomParser, 'DOMParser' );
}
if ( supportsIframe ) {
assertCreateDocument( ve.createDocumentFromHtmlUsingIframe, 'IFrame' );
}
assertCreateDocument( ve.createDocumentFromHtmlUsingInnerHtml, 'innerHTML' );
assertCreateDocument( ve.createDocumentFromHtml, 'wrapper' );
} );
QUnit.test( 'resolveUrl', function ( assert ) {
var i, doc,
cases = [
{
base: 'http://example.com',
href: 'foo',
resolved: 'http://example.com/foo',
msg: 'Simple href with domain as base'
},
{
base: 'http://example.com/bar',
href: 'foo',
resolved: 'http://example.com/foo',
msg: 'Simple href with page as base'
},
{
base: 'http://example.com/bar/',
href: 'foo',
resolved: 'http://example.com/bar/foo',
msg: 'Simple href with directory as base'
},
{
base: 'http://example.com/bar/',
href: './foo',
resolved: 'http://example.com/bar/foo',
msg: './ in href'
},
{
base: 'http://example.com/bar/',
href: '../foo',
resolved: 'http://example.com/foo',
msg: '../ in href'
},
{
base: 'http://example.com/bar/',
href: '/foo',
resolved: 'http://example.com/foo',
msg: 'href starting with /'
},
{
base: 'http://example.com/bar/',
href: '//example.org/foo',
resolved: 'http://example.org/foo',
msg: 'protocol-relative href'
},
{
base: 'http://example.com/bar/',
href: 'https://example.org/foo',
resolved: 'https://example.org/foo',
msg: 'href with protocol'
}
];
QUnit.expect( cases.length );
for ( i = 0; i < cases.length; i++ ) {
doc = ve.createDocumentFromHtml( '' );
doc.head.appendChild( $( '<base>', doc ).attr( 'href', cases[ i ].base )[ 0 ] );
assert.strictEqual( ve.resolveUrl( cases[ i ].href, doc ), cases[ i ].resolved, cases[ i ].msg );
}
} );
QUnit.test( 'resolveAttributes', function ( assert ) {
var i, doc, $html,
cases = [
{
base: 'http://example.com',
html: '<div><a href="foo">foo</a></div><a href="bar">bar</a><img src="baz">',
resolved: '<div><a href="http://example.com/foo">foo</a></div><a href="http://example.com/bar">bar</a><img src="http://example.com/baz">',
msg: 'href and src resolved'
}
];
QUnit.expect( cases.length );
for ( i = 0; i < cases.length; i++ ) {
doc = ve.createDocumentFromHtml( '' );
doc.head.appendChild( $( '<base>', doc ).attr( 'href', cases[ i ].base )[ 0 ] );
$html = $( '<div>' ).append( cases[ i ].html );
ve.resolveAttributes( $html, doc, ve.dm.Converter.static.computedAttributes );
assert.strictEqual(
$html.html(),
cases[ i ].resolved,
cases[ i ].msg
);
}
} );
QUnit.test( 'fixBase', function ( assert ) {
var i, targetDoc, sourceDoc, expectedBase,
cases = [
{
targetBase: '//example.org/foo',
sourceBase: 'https://example.com',
fixedBase: 'https://example.org/foo',
msg: 'Protocol-relative base is made absolute'
},
{
targetBase: 'http://example.org/foo',
sourceBase: 'https://example.com',
fixedBase: 'http://example.org/foo',
msg: 'Fully specified base is left alone'
},
{
// No targetBase
sourceBase: 'https://example.com',
fallbackBase: 'https://example.org/foo',
fixedBase: 'https://example.org/foo',
msg: 'When base is missing, fallback base is used'
}
];
QUnit.expect( cases.length );
for ( i = 0; i < cases.length; i++ ) {
targetDoc = ve.createDocumentFromHtml( '' );
sourceDoc = ve.createDocumentFromHtml( '' );
expectedBase = cases[ i ].fixedBase;
if ( cases[ i ].targetBase ) {
targetDoc.head.appendChild( $( '<base>', targetDoc ).attr( 'href', cases[ i ].targetBase )[ 0 ] );
if ( targetDoc.baseURI ) {
// baseURI is valid, so we expect it to be untouched
expectedBase = targetDoc.baseURI;
}
}
if ( cases[ i ].sourceBase ) {
sourceDoc.head.appendChild( $( '<base>', sourceDoc ).attr( 'href', cases[ i ].sourceBase )[ 0 ] );
}
ve.fixBase( targetDoc, sourceDoc, cases[ i ].fallbackBase );
assert.strictEqual( targetDoc.baseURI, expectedBase, cases[ i ].msg );
}
} );
QUnit.test( 'isBlockElement/isVoidElement', 10, function ( assert ) {
assert.strictEqual( ve.isBlockElement( 'div' ), true, '"div" is a block element' );
assert.strictEqual( ve.isBlockElement( 'SPAN' ), false, '"SPAN" is not a block element' );
assert.strictEqual( ve.isBlockElement( 'a' ), false, '"a" is not a block element' );
assert.strictEqual( ve.isBlockElement( document.createElement( 'div' ) ), true, '<div> is a block element' );
assert.strictEqual( ve.isBlockElement( document.createElement( 'span' ) ), false, '<span> is not a block element' );
assert.strictEqual( ve.isVoidElement( 'img' ), true, '"img" is a void element' );
assert.strictEqual( ve.isVoidElement( 'DIV' ), false, '"DIV" is not a void element' );
assert.strictEqual( ve.isVoidElement( 'span' ), false, '"span" is not a void element' );
assert.strictEqual( ve.isVoidElement( document.createElement( 'img' ) ), true, '<img> is a void element' );
assert.strictEqual( ve.isVoidElement( document.createElement( 'div' ) ), false, '<div> is not a void element' );
} );
// TODO: ve.isUnattachedCombiningMark
// TODO: ve.getByteOffset
// TODO: ve.getClusterOffset
QUnit.test( 'graphemeSafeSubstring', function ( assert ) {
var i,
text = '12\ud860\udee245\ud860\udee2789\ud860\udee2bc',
cases = [
{
msg: 'start and end inside multibyte',
start: 3,
end: 12,
expected: [ '\ud860\udee245\ud860\udee2789\ud860\udee2', '45\ud860\udee2789' ]
},
{
msg: 'start and end next to multibyte',
start: 4,
end: 11,
expected: [ '45\ud860\udee2789', '45\ud860\udee2789' ]
},
{
msg: 'complete string',
start: 0,
end: text.length,
expected: [ text, text ]
},
{
msg: 'collapsed selection inside multibyte',
start: 3,
end: 3,
expected: [ '\ud860\udee2', '' ]
}
];
QUnit.expect( cases.length * 2 );
for ( i = 0; i < cases.length; i++ ) {
assert.strictEqual(
ve.graphemeSafeSubstring( text, cases[ i ].start, cases[ i ].end, true ),
cases[ i ].expected[ 0 ],
cases[ i ].msg + ' (outer)'
);
assert.strictEqual(
ve.graphemeSafeSubstring( text, cases[ i ].start, cases[ i ].end, false ),
cases[ i ].expected[ 1 ],
cases[ i ].msg + ' (inner)'
);
}
} );
QUnit.test( 'transformStyleAttributes', function ( assert ) {
var i, wasStyleAttributeBroken, oldNormalizeAttributeValue,
normalizeColor = function ( name, value ) {
if ( name === 'style' && value === 'color:#ffd' ) {
return 'color: rgb(255, 255, 221);';
}
return value;
},
normalizeBgcolor = function ( name, value ) {
if ( name === 'bgcolor' ) {
return value && value.toLowerCase();
}
return value;
},
cases = [
{
msg: 'Empty tags are not changed self-closing tags',
before: '<html><head></head><body>Hello <a href="foo"></a> world</body></html>'
},
{
msg: 'HTML string with doctype is parsed correctly',
before: '<!DOCTYPE html><html><head><title>Foo</title></head><body>Hello</body></html>'
},
{
msg: 'Style attributes are masked then unmasked',
before: '<body><div style="color:#ffd">Hello</div></body>',
masked: '<body><div style="color:#ffd" data-ve-style="color:#ffd">Hello</div></body>'
},
{
msg: 'Style attributes that differ but normalize the same are overwritten when unmasked',
masked: '<body><div style="color: rgb(255, 255, 221);" data-ve-style="color:#ffd">Hello</div></body>',
after: '<body><div style="color:#ffd">Hello</div></body>',
normalize: normalizeColor
},
{
msg: 'Style attributes that do not normalize the same are not overwritten when unmasked',
masked: '<body><div style="color: rgb(0, 0, 0);" data-ve-style="color:#ffd">Hello</div></body>',
after: '<body><div style="color: rgb(0, 0, 0);">Hello</div></body>',
normalize: normalizeColor
},
{
msg: 'bgcolor attributes are masked then unmasked',
before: '<body><table><tr bgcolor="#FFDEAD"></tr></table></body>',
masked: '<body><table><tr bgcolor="#FFDEAD" data-ve-bgcolor="#FFDEAD"></tr></table></body>'
},
{
msg: 'bgcolor attributes that differ but normalize the same are overwritten when unmasked',
masked: '<body><table><tr bgcolor="#ffdead" data-ve-bgcolor="#FFDEAD"></tr></table></body>',
after: '<body><table><tr bgcolor="#FFDEAD"></tr></table></body>',
normalize: normalizeBgcolor
},
{
msg: 'bgcolor attributes that do not normalize the same are not overwritten when unmasked',
masked: '<body><table><tr bgcolor="#fffffa" data-ve-bgcolor="#FFDEAD"></tr></table></body>',
after: '<body><table><tr bgcolor="#fffffa"></tr></table></body>',
normalize: normalizeBgcolor
}
];
QUnit.expect( 2 * cases.length );
// Force transformStyleAttributes to think that we're in a broken browser
wasStyleAttributeBroken = ve.isStyleAttributeBroken;
ve.isStyleAttributeBroken = true;
for ( i = 0; i < cases.length; i++ ) {
if ( cases[ i ].normalize ) {
oldNormalizeAttributeValue = ve.normalizeAttributeValue;
ve.normalizeAttributeValue = cases[ i ].normalize;
}
if ( cases[ i ].before ) {
assert.strictEqual(
ve.transformStyleAttributes( cases[ i ].before, false )
// Firefox adds linebreaks after <!DOCTYPE>s
.replace( '<!DOCTYPE html>\n', '<!DOCTYPE html>' ),
cases[ i ].masked || cases[ i ].before,
cases[ i ].msg + ' (masking)'
);
} else {
assert.ok( true, cases[ i ].msg + ' (no masking test)' );
}
assert.strictEqual(
ve.transformStyleAttributes( cases[ i ].masked || cases[ i ].before, true )
// Firefox adds a linebreak after <!DOCTYPE>s
.replace( '<!DOCTYPE html>\n', '<!DOCTYPE html>' ),
cases[ i ].after || cases[ i ].before,
cases[ i ].msg + ' (unmasking)'
);
if ( cases[ i ].normalize ) {
ve.normalizeAttributeValue = oldNormalizeAttributeValue;
}
}
} );
QUnit.test( 'normalizeNode', function ( assert ) {
var i, actual, expected, wasNormalizeBroken,
cases = [
{
msg: 'Merge two adjacent text nodes',
before: {
type: 'p',
children: [
{ type: '#text', text: 'Foo' },
{ type: '#text', text: 'Bar' }
]
},
after: {
type: 'p',
children: [
{ type: '#text', text: 'FooBar' }
]
}
},
{
msg: 'Merge three adjacent text nodes',
before: {
type: 'p',
children: [
{ type: '#text', text: 'Foo' },
{ type: '#text', text: 'Bar' },
{ type: '#text', text: 'Baz' }
]
},
after: {
type: 'p',
children: [
{ type: '#text', text: 'FooBarBaz' }
]
}
},
{
msg: 'Drop empty text node after single text node',
before: {
type: 'p',
children: [
{ type: '#text', text: 'Foo' },
{ type: '#text', text: '' }
]
},
after: {
type: 'p',
children: [
{ type: '#text', text: 'Foo' }
]
}
},
{
msg: 'Drop empty text node after two text nodes',
before: {
type: 'p',
children: [
{ type: '#text', text: 'Foo' },
{ type: '#text', text: 'Bar' },
{ type: '#text', text: '' }
]
},
after: {
type: 'p',
children: [
{ type: '#text', text: 'FooBar' }
]
}
},
{
msg: 'Normalize recursively',
before: {
type: 'div',
children: [
{ type: '#text', text: '' },
{
type: 'p',
children: [
{ type: '#text', text: 'Foo' },
{ type: '#text', text: 'Bar' }
]
},
{
type: 'p',
children: [
{ type: '#text', text: 'Baz' },
{ type: '#text', text: 'Quux' }
]
},
{ type: '#text', text: 'Whee' }
]
},
after: {
type: 'div',
children: [
{
type: 'p',
children: [
{ type: '#text', text: 'FooBar' }
]
},
{
type: 'p',
children: [
{ type: '#text', text: 'BazQuux' }
]
},
{ type: '#text', text: 'Whee' }
]
}
}
];
QUnit.expect( 2 * cases.length );
// Force normalizeNode to think native normalization is broken so it uses the manual
// normalization code
wasNormalizeBroken = ve.isNormalizeBroken;
ve.isNormalizeBroken = true;
for ( i = 0; i < cases.length; i++ ) {
actual = ve.test.utils.buildDom( cases[ i ].before );
expected = ve.test.utils.buildDom( cases[ i ].after );
ve.normalizeNode( actual );
assert.equalDomElement( actual, expected, cases[ i ].msg );
assert.ok( actual.isEqualNode( expected ), cases[ i ].msg + ' (isEqualNode)' );
}
ve.isNormalizeBroken = wasNormalizeBroken;
} );
QUnit.test( 'getCommonAncestor', function ( assert ) {
var doc, nodes, tests, i, len, test, testNodes, ancestorNode;
doc = ve.createDocumentFromHtml( '<html><div><p>AA<i><b>BB<img src="#"></b></i>CC</p>DD</div>EE' );
tests = [
{ nodes: 'b b', ancestor: 'b' },
{ nodes: 'b i', ancestor: 'i' },
{ nodes: 'textB img', ancestor: 'b' },
{ nodes: 'p textD', ancestor: 'div' },
{ nodes: 'textC img', ancestor: 'p' },
{ nodes: 'textC b', ancestor: 'p' },
{ nodes: 'textC textD', ancestor: 'div' },
{ nodes: 'textA textB', ancestor: 'p' },
{ nodes: 'textA img', ancestor: 'p' },
{ nodes: 'img textE', ancestor: 'body' },
{ nodes: 'textA textB textC textD', ancestor: 'div' },
{ nodes: 'textA i b textC', ancestor: 'p' },
{ nodes: 'body div head p', ancestor: 'html' },
{ nodes: 'b null', ancestor: 'null' },
{ nodes: 'null b', ancestor: 'null' },
{ nodes: 'b i null', ancestor: 'null' },
{ nodes: 'b null i', ancestor: 'null' },
{ nodes: 'b unattached', ancestor: 'null' },
{ nodes: 'unattached b', ancestor: 'null' }
];
nodes = {};
nodes.html = doc.documentElement;
nodes.head = doc.head;
nodes.body = doc.body;
nodes.div = doc.getElementsByTagName( 'div' )[ 0 ];
nodes.p = doc.getElementsByTagName( 'p' )[ 0 ];
nodes.b = doc.getElementsByTagName( 'b' )[ 0 ];
nodes.i = doc.getElementsByTagName( 'i' )[ 0 ];
nodes.img = doc.getElementsByTagName( 'img' )[ 0 ];
nodes.textA = nodes.p.childNodes[ 0 ];
nodes.textB = nodes.b.childNodes[ 0 ];
nodes.textC = nodes.p.childNodes[ 2 ];
nodes.textD = nodes.div.childNodes[ 1 ];
nodes.textE = nodes.body.childNodes[ 1 ];
nodes.null = null;
nodes.unattached = doc.createElement( 'div' ).appendChild( doc.createElement( 'span' ) );
function getNode( name ) {
return nodes[ name ];
}
QUnit.expect( tests.length );
for ( i = 0, len = tests.length; i < len; i++ ) {
test = tests[ i ];
testNodes = test.nodes.split( /\s+/ ).map( getNode );
ancestorNode = nodes[ test.ancestor ];
assert.equal(
ve.getCommonAncestor.apply( null, testNodes ),
ancestorNode,
test.nodes + ' -> ' + test.ancestor
);
}
} );
QUnit.test( 'getCommonStartSequenceLength', function ( assert ) {
var i, len, tests, test;
tests = [
{
sequences: [ [ 0, 1, 2 ], [ 0, 1, 2 ], [ '0', 1, 2 ] ],
commonLength: 0,
title: 'No common start sequence'
},
{
sequences: [ [ 1, 2, 3 ], [] ],
commonLength: 0,
title: 'Empty sequence'
},
{
sequences: [ [ 'five', 6 ], [ 'five' ] ],
commonLength: 1,
title: 'Differing lengths'
},
{
sequences: [ [ 1, 2 ] ],
commonLength: 2,
title: 'Single sequence'
},
{
sequences: [ 'Cymru', 'Cymry', 'Cymraes', 'Cymro', 'Cymraeg' ],
commonLength: 4,
title: 'String sequences'
}
];
QUnit.expect( tests.length );
for ( i = 0, len = tests.length; i < len; i++ ) {
test = tests[ i ];
assert.strictEqual(
ve.getCommonStartSequenceLength( test.sequences ),
test.commonLength,
test.title
);
}
} );
QUnit.test( 'adjacentDomPosition', function ( assert ) {
var tests, direction, i, len, test, offsetPaths, position, div;
// In the following tests, the html is put inside the top-level div as innerHTML. Then
// ve.adjacentDomPosition is called with the position just inside the div (i.e.
// { node: div, offset: 0 } for forward direction tests, and
// { node: div, offset: div.childNodes.length } for reverse direction tests). The result
// of the first call is passed into the function again, and so on iteratively until the
// function returns null. The 'path' properties are a list of descent offsets to find a
// particular position node from the top-level div. E.g. a path of [ 5, 7 ] refers to the
// node div.childNodes[ 5 ].childNodes[ 7 ] .
tests = [
{
title: 'Simple p node',
html: '<p>x</p>',
options: { stop: function () { return true; } },
expectedOffsetPaths: [
[ 0 ],
[ 0, 0 ],
[ 0, 0, 0 ],
[ 0, 0, 1 ],
[ 0, 1 ],
[ 1 ]
]
},
{
title: 'Filtered descent',
html: '<div class="x">foo</div><div class="y">bar</div>',
options: { stop: function () { return true; }, noDescend: '.x' },
expectedOffsetPaths: [
[ 0 ],
[ 1 ],
[ 1, 0 ],
[ 1, 0, 0 ],
[ 1, 0, 1 ],
[ 1, 0, 2 ],
[ 1, 0, 3 ],
[ 1, 1 ],
[ 2 ]
]
},
{
title: 'Empty tags and heavy nesting',
html: '<div><br/><p>foo <b>bar <i>baz</i></b></p></div>',
options: { stop: function () { return true; } },
expectedOffsetPaths: [
[ 0 ],
[ 0, 0 ],
// The <br/> tag is void, so should get skipped
[ 0, 1 ],
[ 0, 1, 0 ],
[ 0, 1, 0, 0 ],
[ 0, 1, 0, 1 ],
[ 0, 1, 0, 2 ],
[ 0, 1, 0, 3 ],
[ 0, 1, 0, 4 ],
[ 0, 1, 1 ],
[ 0, 1, 1, 0 ],
[ 0, 1, 1, 0, 0 ],
[ 0, 1, 1, 0, 1 ],
[ 0, 1, 1, 0, 2 ],
[ 0, 1, 1, 0, 3 ],
[ 0, 1, 1, 0, 4 ],
[ 0, 1, 1, 1 ],
[ 0, 1, 1, 1, 0 ],
[ 0, 1, 1, 1, 0, 0 ],
[ 0, 1, 1, 1, 0, 1 ],
[ 0, 1, 1, 1, 0, 2 ],
[ 0, 1, 1, 1, 0, 3 ],
[ 0, 1, 1, 1, 1 ],
[ 0, 1, 1, 2 ],
[ 0, 1, 2 ],
[ 0, 2 ],
[ 1 ]
]
}
];
QUnit.expect( 2 * tests.length );
div = document.createElement( 'div' );
div.contentEditable = 'true';
for ( direction in { forward: undefined, backward: undefined } ) {
for ( i = 0, len = tests.length; i < len; i++ ) {
test = tests[ i ];
div.innerHTML = test.html;
offsetPaths = [];
position = {
node: div,
offset: direction === 'backward' ? div.childNodes.length : 0
};
while ( position.node !== null ) {
offsetPaths.push(
ve.getOffsetPath( div, position.node, position.offset )
);
position = ve.adjacentDomPosition(
position,
direction === 'backward' ? -1 : 1,
test.options
);
}
assert.deepEqual(
offsetPaths,
(
direction === 'backward' ?
test.expectedOffsetPaths.slice().reverse() :
test.expectedOffsetPaths
),
test.title + ' (' + direction + ')'
);
}
}
} );