Sfoglia il codice sorgente

Merge branch 't/256'

Szymon Kupś 9 anni fa
parent
commit
077a23294e

+ 274 - 15
packages/ckeditor5-engine/src/treeview/element.js

@@ -63,6 +63,35 @@ export default class Element extends Node {
 		if ( children ) {
 			this.insertChildren( 0, children );
 		}
+
+		/**
+		 * Set of classes associated with element instance.
+		 *
+		 * @protected
+		 * @member {Set} core.treeView.Element#_classes
+		 */
+		this._classes = new Set();
+
+		if ( this._attrs.has( 'class' ) ) {
+			// Remove class attribute and handle it by class set.
+			const classString = this._attrs.get( 'class' );
+			parseClasses( this._classes, classString );
+			this._attrs.delete( 'class' );
+		}
+
+		/**
+		 * Map of styles.
+		 *
+		 * @protected
+		 * @member {Set} core.treeView.Element#_styles
+		 */
+		this._styles = new Map();
+
+		if ( this._attrs.has( 'style' ) ) {
+			// Remove style attribute and handle it by styles map.
+			parseInlineStyles( this._styles, this._attrs.get( 'style' ) );
+			this._attrs.delete( 'style' );
+		}
 	}
 
 	/**
@@ -81,7 +110,14 @@ export default class Element extends Node {
 			}
 		}
 
-		return new Element( this.name, this._attrs, childrenClone );
+		const cloned = new Element( this.name, this._attrs, childrenClone );
+
+		// Classes and styles are cloned separately - this solution is faster than adding them back to attributes and
+		// parse once again in constructor.
+		cloned._classes = new Set( this._classes );
+		cloned._styles = new Map( this._styles );
+
+		return cloned;
 	}
 
 	/**
@@ -137,20 +173,51 @@ export default class Element extends Node {
 
 	/**
 	 * Returns an iterator that contains the keys for attributes.
+	 * Order of inserting attributes is not preserved.
 	 *
 	 * @returns {Iterator.<String>} Keys for attributes.
 	 */
-	getAttributeKeys() {
-		return this._attrs.keys();
+	*getAttributeKeys() {
+		if ( this._classes.size > 0 ) {
+			yield 'class';
+		}
+
+		if ( this._styles.size > 0 ) {
+			yield 'style';
+		}
+
+		yield* this._attrs.keys();
 	}
 
 	/**
-	 * Gets attribute by key.
+	 * Gets attribute by key. If attribute is not present - returns undefined.
 	 *
 	 * @param {String} key Attribute key.
-	 * @returns {String} Attribute value.
+	 * @returns {String|undefined} Attribute value.
 	 */
 	getAttribute( key ) {
+		if ( key == 'class' ) {
+			if ( this._classes.size > 0 ) {
+				return [ ...this._classes ].join( ' ' );
+			}
+
+			return undefined;
+		}
+
+		if ( key == 'style' ) {
+			if ( this._styles.size > 0 ) {
+				let styleString = '';
+
+				for ( let [ property, value ] of this._styles ) {
+					styleString += `${ property }:${ value };`;
+				}
+
+				return styleString;
+			}
+
+			return undefined;
+		}
+
 		return this._attrs.get( key );
 	}
 
@@ -161,6 +228,14 @@ export default class Element extends Node {
 	 * @returns {Boolean} `true` if attribute with the specified key exists in the element, false otherwise.
 	 */
 	hasAttribute( key ) {
+		if ( key == 'class' ) {
+			return this._classes.size  > 0;
+		}
+
+		if ( key == 'style' ) {
+			return this._styles.size > 0;
+		}
+
 		return this._attrs.has( key );
 	}
 
@@ -174,7 +249,13 @@ export default class Element extends Node {
 	setAttribute( key, value ) {
 		this._fireChange( 'ATTRIBUTES', this );
 
-		this._attrs.set( key, value );
+		if ( key == 'class' ) {
+			parseClasses( this._classes, value );
+		} else if ( key == 'style' ) {
+			parseInlineStyles( this._styles, value );
+		} else {
+			this._attrs.set( key, value );
+		}
 	}
 
 	/**
@@ -215,6 +296,29 @@ export default class Element extends Node {
 	removeAttribute( key ) {
 		this._fireChange( 'ATTRIBUTES', this );
 
+		// Remove class attribute.
+		if ( key == 'class' ) {
+			if ( this._classes.size > 0 ) {
+				this._classes.clear();
+
+				return true;
+			}
+
+			return false;
+		}
+
+		// Remove style attribute.
+		if ( key == 'style' ) {
+			if ( this._styles.size > 0 ) {
+				this._styles.clear();
+
+				return true;
+			}
+
+			return false;
+		}
+
+		// Remove other attributes.
 		return this._attrs.delete( key );
 	}
 
@@ -254,27 +358,182 @@ export default class Element extends Node {
 			return true;
 		}
 
-		// Check name and attributes.
+		// Check element name.
 		if ( this.name != otherElement.name ) {
 			return false;
 		}
 
-		const thisNodeAttrKeys = this.getAttributeKeys();
-		const otherNodeAttrKeys = otherElement.getAttributeKeys();
-		let count = 0;
+		// Check number of attributes, classes and styles.
+		if ( this._attrs.size !== otherElement._attrs.size || this._classes.size !== otherElement._classes.size ||
+			this._styles.size !== otherElement._styles.size ) {
+			return false;
+		}
 
-		for ( let key of thisNodeAttrKeys ) {
-			if ( this.getAttribute( key ) !== otherElement.getAttribute( key ) ) {
+		// Check if attributes are the same.
+		for ( let [ key, value ] of this._attrs ) {
+			if ( !otherElement._attrs.has( key ) || otherElement._attrs.get( key ) !== value ) {
 				return false;
 			}
+		}
 
-			count++;
+		// Check if classes are the same.
+		for ( let className of this._classes ) {
+			if ( !otherElement._classes.has( className ) ) {
+				return false;
+			}
 		}
 
-		if ( count != utils.count( otherNodeAttrKeys ) ) {
-			return false;
+		// Check if styles are the same.
+		for ( let [ property, value ] of this._styles ) {
+			if ( !otherElement._styles.has( property ) || otherElement._styles.get( property ) !== value ) {
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+	/**
+	 * Adds specified class.
+	 *
+	 *		element.addClass( 'foo' ); // Adds 'foo' class.
+	 *		element.addClass( 'foo', 'bar' ); // Adds 'foo' and 'bar' classes.
+	 *
+	 * @param {...String} className
+	 * @fires core.treeView.Node#change
+	 */
+	addClass( ...className ) {
+		this._fireChange( 'ATTRIBUTES', this );
+		className.forEach( name => this._classes.add( name ) );
+	}
+
+	/**
+	 * Removes specified class.
+	 *
+ 	 *		element.removeClass( 'foo' );  // Removes 'foo' class.
+	 *		element.removeClass( 'foo', 'bar' ); // Removes both 'foo' and 'bar' classes.
+	 *
+	 * @param {...String} className
+	 * @fires core.treeView.Node#change
+	 */
+	removeClass( ...className ) {
+		this._fireChange( 'ATTRIBUTES', this );
+		className.forEach( name => this._classes.delete( name ) );
+	}
+
+	/**
+	 * Returns true if class is present.
+	 * If more then one class is provided - returns true only when all classes are present.
+	 *
+	 *		element.hasClass( 'foo' ); // Returns true if 'foo' class is present.
+	 *		element.hasClass( 'foo', 'bar' ); // Returns true if 'foo' and 'bar' classes are both present.
+	 *
+	 * @param {...String} className
+	 */
+	hasClass( ...className ) {
+		for ( let name of className ) {
+			if ( !this._classes.has( name ) ) {
+				return false;
+			}
 		}
 
 		return true;
 	}
+
+	/**
+	 * Adds style to the element.
+	 *
+	 *		element.setStyle( 'color', 'red' );
+	 *		element.setStyle( {
+	 *			color: 'red',
+	 *			position: 'fixed'
+	 *		} );
+	 *
+	 * @param {String|Object} property Property name or object with key - value pairs.
+	 * @param {String} [value] Value to set. This parameter is ignored if object is provided as the first parameter.
+	 * @fires core.treeView.Node#change
+	 */
+	setStyle( property, value ) {
+		this._fireChange( 'ATTRIBUTES', this );
+
+		if ( isPlainObject( property ) ) {
+			const keys = Object.keys( property );
+
+			for ( let key of keys ) {
+				this._styles.set( key, property[ key ] );
+			}
+		} else {
+			this._styles.set( property, value );
+		}
+	}
+
+	/**
+	 * Returns style value for given property.
+	 * Undefined is returned if style does not exist.
+	 *
+	 * @param {String} property
+	 * @returns {String|undefined}
+	 */
+	getStyle( property ) {
+		return this._styles.get( property );
+	}
+
+	/**
+	 * Returns true if style keys are present.
+	 * If more then one style property is provided - returns true only when all properties are present.
+	 *
+	 *		element.hasStyle( 'color' ); // Returns true if 'border-top' style is present.
+	 *		element.hasStyle( 'color', 'border-top' ); // Returns true if 'color' and 'border-top' styles are both present.
+	 *
+	 * @param {...String} property
+	 */
+	hasStyle( ...property ) {
+		for ( let name of property ) {
+			if ( !this._styles.has( name ) ) {
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+	/**
+	 * Removes specified style.
+	 *
+	 *		element.removeStyle( 'color' );  // Removes 'color' style.
+	 *		element.removeStyle( 'color', 'border-top' ); // Removes both 'color' and 'border-top' styles.
+	 *
+	 * @param {...String} property
+	 * @fires core.treeView.Node#change
+	 */
+	removeStyle( ...property ) {
+		this._fireChange( 'ATTRIBUTES', this );
+		property.forEach( name => this._styles.delete( name ) );
+	}
 }
+
+// Parses inline styles and puts property - value pairs into styles map.
+// Styles map is cleared before insertion.
+//
+// @param {Map.<String, String>} stylesMap Map to insert parsed properties and values.
+// @param {String} stylesString Styles to parse.
+function parseInlineStyles( stylesMap, stylesString ) {
+	const regex = /\s*([^:;\s]+)\s*:\s*([^;]+)\s*(?=;|$)/g;
+	let matchStyle;
+	stylesMap.clear();
+
+	while ( ( matchStyle = regex.exec( stylesString ) ) !== null ) {
+		stylesMap.set( matchStyle[ 1 ], matchStyle[ 2 ].trim() );
+	}
+}
+
+// Parses class attribute and puts all classes into classes set.
+// Classes set s cleared before insertion.
+//
+// @param {Set.<String>} classesSet Set to insert parsed classes.
+// @param {String} classesString String with classes to parse.
+function parseClasses( classesSet, classesString ) {
+	const classArray = classesString.split( /\s+/ );
+	classesSet.clear();
+	classArray.forEach( name => classesSet.add( name ) );
+}

+ 423 - 0
packages/ckeditor5-engine/tests/treeview/element.js

@@ -49,6 +49,29 @@ describe( 'Element', () => {
 			expect( parent.getChildCount() ).to.equal( 1 );
 			expect( parent.getChild( 0 ) ).to.have.property( 'name' ).that.equals( 'p' );
 		} );
+
+		it( 'should move class attribute to class set ', () => {
+			const el = new ViewElement( 'p', { id: 'test', class: 'one two three' } );
+
+			expect( el._attrs.has( 'class' ) ).to.be.false;
+			expect( el._attrs.has( 'id' ) ).to.be.true;
+			expect( el._classes.has( 'one' ) ).to.be.true;
+			expect( el._classes.has( 'two' ) ).to.be.true;
+			expect( el._classes.has( 'three' ) ).to.be.true;
+		} );
+
+		it( 'should move style attribute to style map', () => {
+			const el = new ViewElement( 'p', { id: 'test', style: 'one: style1; two:style2 ; three : url(http://ckeditor.com)' } );
+
+			expect( el._attrs.has( 'style' ) ).to.be.false;
+			expect( el._attrs.has( 'id' ) ).to.be.true;
+			expect( el._styles.has( 'one' ) ).to.be.true;
+			expect( el._styles.get( 'one' ) ).to.equal( 'style1' );
+			expect( el._styles.has( 'two' ) ).to.be.true;
+			expect( el._styles.get( 'two' ) ).to.equal( 'style2' );
+			expect( el._styles.has( 'three' ) ).to.be.true;
+			expect( el._styles.get( 'three' ) ).to.equal( 'url(http://ckeditor.com)' );
+		} );
 	} );
 
 	describe( 'clone', () => {
@@ -99,6 +122,29 @@ describe( 'Element', () => {
 			expect( clone.getAttribute( 'attr2' ) ).to.equal( 'bar' );
 			expect( clone.getChildCount() ).to.equal( 0 );
 		} );
+
+		it( 'should clone class attribute', () => {
+			const el = new ViewElement( 'p', { foo: 'bar' } );
+			el.addClass( 'baz', 'qux' );
+			const clone = el.clone( false );
+
+			expect( clone ).to.not.equal( el );
+			expect( clone.name ).to.equal( el.name );
+			expect( clone.getAttribute( 'foo' ) ).to.equal( 'bar' );
+			expect( clone.getAttribute( 'class' ) ).to.equal( 'baz qux' );
+		} );
+
+		it( 'should clone style attribute', () => {
+			const el = new ViewElement( 'p', { style: 'color: red; font-size: 12px;' } );
+			const clone = el.clone( false );
+
+			expect( clone ).to.not.equal( el );
+			expect( clone.name ).to.equal( el.name );
+			expect( clone._styles.has( 'color' ) ).to.be.true;
+			expect( clone._styles.get( 'color' ) ).to.equal( 'red' );
+			expect( clone._styles.has( 'font-size' ) ).to.be.true;
+			expect( clone._styles.get( 'font-size' ) ).to.equal( '12px' );
+		} );
 	} );
 
 	describe( 'isSimilar', () => {
@@ -135,6 +181,42 @@ describe( 'Element', () => {
 			expect( el.isSimilar( other2 ) ).to.be.false;
 			expect( el.isSimilar( other3 ) ).to.be.false;
 		} );
+
+		it( 'should compare class attribute', () => {
+			const el1 = new ViewElement( 'p' );
+			const el2 = new ViewElement( 'p' );
+			const el3 = new ViewElement( 'p' );
+			const el4 = new ViewElement( 'p' );
+
+			el1.addClass( 'foo', 'bar' );
+			el2.addClass( 'bar', 'foo' );
+			el3.addClass( 'baz' );
+			el4.addClass( 'baz', 'bar' );
+
+			expect( el1.isSimilar( el2 ) ).to.be.true;
+			expect( el1.isSimilar( el3 ) ).to.be.false;
+			expect( el1.isSimilar( el4 ) ).to.be.false;
+		} );
+
+		it( 'should compare styles attribute', () => {
+			const el1 = new ViewElement( 'p' );
+			const el2 = new ViewElement( 'p' );
+			const el3 = new ViewElement( 'p' );
+			const el4 = new ViewElement( 'p' );
+
+			el1.setStyle( 'color', 'red' );
+			el1.setStyle( 'top', '10px' );
+			el2.setStyle( 'top', '20px' );
+			el3.setStyle( 'top', '10px' );
+			el3.setStyle( 'color', 'red' );
+			el4.setStyle( 'color', 'blue' );
+			el4.setStyle( 'top', '10px' );
+
+			expect( el1.isSimilar( el2 ) ).to.be.false;
+			expect( el1.isSimilar( el3 ) ).to.be.true;
+			expect( el2.isSimilar( el3 ) ).to.be.false;
+			expect( el3.isSimilar( el4 ) ).to.be.false;
+		} );
 	} );
 
 	describe( 'children manipulation methods', () => {
@@ -250,6 +332,55 @@ describe( 'Element', () => {
 			el = new ViewElement( 'p' );
 		} );
 
+		describe( 'setAttribute', () => {
+			it( 'should set attribute', () => {
+				el.setAttribute( 'foo', 'bar' );
+
+				expect( el._attrs.has( 'foo' ) ).to.be.true;
+				expect( el._attrs.get( 'foo' ) ).to.equal( 'bar' );
+			} );
+
+			it( 'should fire change event with ATTRIBUTES type', ( done ) => {
+				el.once( 'change', ( eventInfo, type ) => {
+					expect( eventInfo.name ).to.equal( 'change' );
+					expect( eventInfo.source ).to.equal( el );
+					expect( type ).to.equal( 'ATTRIBUTES' );
+					done();
+				} );
+
+				el.setAttribute( 'foo', 'bar' );
+			} );
+
+			it( 'should set class', () => {
+				el.setAttribute( 'class', 'foo bar' );
+
+				expect( el._attrs.has( 'class' ) ).to.be.false;
+				expect( el._classes.has( 'foo' )  ).to.be.true;
+				expect( el._classes.has( 'bar' )  ).to.be.true;
+			} );
+
+			it( 'should replace all existing classes', () => {
+				el.setAttribute( 'class', 'foo bar baz' );
+				el.setAttribute( 'class', 'qux' );
+
+				expect( el._classes.has( 'foo' )  ).to.be.false;
+				expect( el._classes.has( 'bar' )  ).to.be.false;
+				expect( el._classes.has( 'baz' )  ).to.be.false;
+				expect( el._classes.has( 'qux' )  ).to.be.true;
+			} );
+
+			it( 'should replace all styles', () => {
+				el.setStyle( 'color', 'red' );
+				el.setStyle( 'top', '10px' );
+				el.setAttribute( 'style', 'border:none' );
+
+				expect( el.hasStyle( 'color' ) ).to.be.false;
+				expect( el.hasStyle( 'top' ) ).to.be.false;
+				expect( el.hasStyle( 'border' ) ).to.be.true;
+				expect( el.getStyle( 'border' ) ).to.equal( 'none' );
+			} );
+		} );
+
 		describe( 'getAttribute', () => {
 			it( 'should return attribute', () => {
 				el.setAttribute( 'foo', 'bar' );
@@ -257,6 +388,27 @@ describe( 'Element', () => {
 				expect( el.getAttribute( 'foo' ) ).to.equal( 'bar' );
 				expect( el.getAttribute( 'bom' ) ).to.not.be.ok;
 			} );
+
+			it( 'should return class attribute', () => {
+				el.addClass( 'foo', 'bar' );
+
+				expect( el.getAttribute( 'class' ) ).to.equal( 'foo bar' );
+			} );
+
+			it( 'should return undefined if no class attribute', () => {
+				expect( el.getAttribute( 'class' ) ).to.be.undefined;
+			} );
+
+			it( 'should return style attribute', () => {
+				el.setStyle( 'color', 'red' );
+				el.setStyle( 'top', '10px' );
+
+				expect( el.getAttribute( 'style' ) ).to.equal( 'color:red;top:10px;' );
+			} );
+
+			it( 'should return undefined if no style attribute', () => {
+				expect( el.getAttribute( 'style' ) ).to.be.undefined;
+			} );
 		} );
 
 		describe( 'hasAttribute', () => {
@@ -266,6 +418,18 @@ describe( 'Element', () => {
 				expect( el.hasAttribute( 'foo' ) ).to.be.true;
 				expect( el.hasAttribute( 'bom' ) ).to.be.false;
 			} );
+
+			it( 'should return true if element has class attribute', () => {
+				expect( el.hasAttribute( 'class' ) ).to.be.false;
+				el.addClass( 'foo' );
+				expect( el.hasAttribute( 'class' ) ).to.be.true;
+			} );
+
+			it( 'should return true if element has style attribute', () => {
+				expect( el.hasAttribute( 'style' ) ).to.be.false;
+				el.setStyle( 'border', '1px solid red' );
+				expect( el.hasAttribute( 'style' ) ).to.be.true;
+			} );
 		} );
 
 		describe( 'getAttributeKeys', () => {
@@ -283,6 +447,30 @@ describe( 'Element', () => {
 
 				expect( i ).to.equal( 2 );
 			} );
+
+			it( 'should return class key', () => {
+				el.addClass( 'foo' );
+				el.setAttribute( 'bar', true );
+				const expected = [ 'class', 'bar' ];
+				let i = 0;
+
+				for ( let key of el.getAttributeKeys() ) {
+					expect( key ).to.equal( expected[ i ] );
+					i++;
+				}
+			} );
+
+			it( 'should return style key', () => {
+				el.setStyle( 'color', 'black' );
+				el.setAttribute( 'bar', true );
+				const expected = [ 'style', 'bar' ];
+				let i = 0;
+
+				for ( let key of el.getAttributeKeys() ) {
+					expect( key ).to.equal( expected[ i ] );
+					i++;
+				}
+			} );
 		} );
 
 		describe( 'removeAttribute', () => {
@@ -297,6 +485,241 @@ describe( 'Element', () => {
 
 				expect( utils.count( el.getAttributeKeys() ) ).to.equal( 0 );
 			} );
+
+			it( 'should fire change event with ATTRIBUTES type', ( done ) => {
+				el.setAttribute( 'foo', 'bar' );
+				el.once( 'change', ( eventInfo, type ) => {
+					expect( eventInfo.name ).to.equal( 'change' );
+					expect( eventInfo.source ).to.equal( el );
+					expect( type ).to.equal( 'ATTRIBUTES' );
+					done();
+				} );
+
+				el.removeAttribute( 'foo' );
+			} );
+
+			it( 'should remove class attribute', () => {
+				el.addClass( 'foo', 'bar' );
+				const el2 = new ViewElement( 'p' );
+				const removed1 = el.removeAttribute( 'class' );
+				const removed2 = el2.removeAttribute( 'class' );
+
+				expect( el.hasAttribute( 'class' ) ).to.be.false;
+				expect( el.hasClass( 'foo' ) ).to.be.false;
+				expect( el.hasClass( 'bar' ) ).to.be.false;
+				expect( removed1 ).to.be.true;
+				expect( removed2 ).to.be.false;
+			} );
+
+			it( 'should remove style attribute', () => {
+				el.setStyle( 'color', 'red' );
+				el.setStyle( 'position', 'fixed' );
+				const el2 = new ViewElement( 'p' );
+				const removed1 = el.removeAttribute( 'style' );
+				const removed2 = el2.removeAttribute( 'style' );
+
+				expect( el.hasAttribute( 'style' ) ).to.be.false;
+				expect( el.hasStyle( 'color' ) ).to.be.false;
+				expect( el.hasStyle( 'position' ) ).to.be.false;
+				expect( removed1 ).to.be.true;
+				expect( removed2 ).to.be.false;
+			} );
+		} );
+	} );
+
+	describe( 'classes manipulation methods', () => {
+		let el;
+
+		beforeEach( () => {
+			el = new ViewElement( 'p' );
+		} );
+
+		describe( 'addClass', () => {
+			it( 'should add single class', () => {
+				el.addClass( 'one' );
+
+				expect( el._classes.has( 'one' ) ).to.be.true;
+			} );
+
+			it( 'should fire change event with ATTRIBUTES type', ( done ) => {
+				el.once( 'change', ( eventInfo, type ) => {
+					expect( eventInfo.name ).to.equal( 'change' );
+					expect( eventInfo.source ).to.equal( el );
+					expect( type ).to.equal( 'ATTRIBUTES' );
+					done();
+				} );
+
+				el.addClass( 'one' );
+			} );
+
+			it( 'should add multiple classes', () => {
+				el.addClass( 'one', 'two', 'three' );
+
+				expect( el._classes.has( 'one' ) ).to.be.true;
+				expect( el._classes.has( 'two' ) ).to.be.true;
+				expect( el._classes.has( 'three' ) ).to.be.true;
+			} );
+		} );
+
+		describe( 'removeClass', () => {
+			it( 'should remove single class', () => {
+				el.addClass( 'one', 'two', 'three' );
+
+				el.removeClass( 'one' );
+
+				expect( el._classes.has( 'one' ) ).to.be.false;
+				expect( el._classes.has( 'two' ) ).to.be.true;
+				expect( el._classes.has( 'three' ) ).to.be.true;
+			} );
+
+			it( 'should fire change event with ATTRIBUTES type', ( done ) => {
+				el.addClass( 'one' );
+				el.once( 'change', ( eventInfo, type ) => {
+					expect( eventInfo.name ).to.equal( 'change' );
+					expect( eventInfo.source ).to.equal( el );
+					expect( type ).to.equal( 'ATTRIBUTES' );
+					done();
+				} );
+
+				el.removeClass( 'one' );
+			} );
+
+			it( 'should remove multiple classes', () => {
+				el.addClass( 'one', 'two', 'three', 'four' );
+				el.removeClass( 'one', 'two', 'three' );
+
+				expect( el._classes.has( 'one' ) ).to.be.false;
+				expect( el._classes.has( 'two' ) ).to.be.false;
+				expect( el._classes.has( 'three' ) ).to.be.false;
+				expect( el._classes.has( 'four' ) ).to.be.true;
+			} );
+		} );
+
+		describe( 'hasClass', () => {
+			it( 'should check if element has a class', () => {
+				el.addClass( 'one', 'two', 'three' );
+
+				expect( el.hasClass( 'one' ) ).to.be.true;
+				expect( el.hasClass( 'two' ) ).to.be.true;
+				expect( el.hasClass( 'three' ) ).to.be.true;
+				expect( el.hasClass( 'four' ) ).to.be.false;
+			} );
+
+			it( 'should check if element has multiple classes', () => {
+				el.addClass( 'one', 'two', 'three' );
+
+				expect( el.hasClass( 'one', 'two' ) ).to.be.true;
+				expect( el.hasClass( 'three', 'two' ) ).to.be.true;
+				expect( el.hasClass( 'three', 'one', 'two' ) ).to.be.true;
+				expect( el.hasClass( 'three', 'one', 'two', 'zero' ) ).to.be.false;
+			} );
+		} );
+	} );
+
+	describe( 'styles manipulation methods', () => {
+		let el;
+
+		beforeEach( () => {
+			el = new ViewElement( 'p' );
+		} );
+
+		describe( 'setStyle', () => {
+			it( 'should set element style', () => {
+				el.setStyle( 'color', 'red' );
+
+				expect( el._styles.has( 'color' ) ).to.be.true;
+				expect( el._styles.get( 'color' ) ).to.equal( 'red' );
+			} );
+
+			it( 'should fire change event with ATTRIBUTES type', ( done ) => {
+				el.once( 'change', ( eventInfo, type ) => {
+					expect( eventInfo.name ).to.equal( 'change' );
+					expect( eventInfo.source ).to.equal( el );
+					expect( type ).to.equal( 'ATTRIBUTES' );
+					done();
+				} );
+
+				el.setStyle( 'color', 'red' );
+			} );
+
+			it( 'should set multiple styles by providing an object', () => {
+				el.setStyle( {
+					color: 'red',
+					position: 'fixed'
+				} );
+
+				expect( el._styles.has( 'color' ) ).to.be.true;
+				expect( el._styles.has( 'position' ) ).to.be.true;
+				expect( el._styles.get( 'color' ) ).to.equal( 'red' );
+				expect( el._styles.get( 'position' ) ).to.equal( 'fixed' );
+			} );
+		} );
+
+		describe( 'getStyle', () => {
+			it( 'should get style', () => {
+				el.setStyle( {
+					color: 'red',
+					border: '1px solid red'
+				} );
+
+				expect( el.getStyle( 'color' ) ).to.equal( 'red' );
+				expect( el.getStyle( 'border' ) ).to.equal( '1px solid red' );
+			} );
+		} );
+
+		describe( 'hasStyle', () => {
+			it( 'should check if element has a style', () => {
+				el.setStyle( 'padding-top', '10px' );
+
+				expect( el.hasStyle( 'padding-top' ) ).to.be.true;
+				expect( el.hasStyle( 'padding-left' ) ).to.be.false;
+			} );
+
+			it( 'should check if element has multiple styles', () => {
+				el.setStyle( {
+					'padding-top': '10px',
+					'margin-left': '10px',
+					'color': '10px;'
+				} );
+
+				expect( el.hasStyle( 'padding-top', 'margin-left' ) ).to.be.true;
+				expect( el.hasStyle( 'padding-top', 'margin-left', 'color' ) ).to.be.true;
+				expect( el.hasStyle( 'padding-top', 'padding-left' ) ).to.be.false;
+			} );
+		} );
+
+		describe( 'removeStyle', () => {
+			it( 'should remove style', () => {
+				el.setStyle( 'padding-top', '10px' );
+				el.removeStyle( 'padding-top' );
+
+				expect( el.hasStyle( 'padding-top' ) ).to.be.false;
+			} );
+
+			it( 'should fire change event with ATTRIBUTES type', ( done ) => {
+				el.setStyle( 'color', 'red' );
+				el.once( 'change', ( eventInfo, type ) => {
+					expect( eventInfo.name ).to.equal( 'change' );
+					expect( eventInfo.source ).to.equal( el );
+					expect( type ).to.equal( 'ATTRIBUTES' );
+					done();
+				} );
+
+				el.removeStyle( 'color' );
+			} );
+
+			it( 'should remove multiple styles', () => {
+				el.setStyle( {
+					'padding-top': '10px',
+					'margin-top': '10px',
+					'color': 'red'
+				} );
+				el.removeStyle( 'padding-top', 'margin-top' );
+
+				expect( el.hasStyle( 'padding-top' ) ).to.be.false;
+				expect( el.hasStyle( 'margin-top' ) ).to.be.false;
+				expect( el.hasStyle( 'color' ) ).to.be.true;
+			} );
 		} );
 	} );
 } );