text: /**
   * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
   * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
   */
  
  /* global document, Event, console */
  
  import ToolbarView from '../../src/toolbar/toolbarview';
  import ToolbarSeparatorView from '../../src/toolbar/toolbarseparatorview';
  import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
  import ComponentFactory from '../../src/componentfactory';
  import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
  import FocusCycler from '../../src/focuscycler';
  import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
  import ViewCollection from '../../src/viewcollection';
  import global from '@ckeditor/ckeditor5-utils/src/dom/global';
  import View from '../../src/view';
  import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  import { add as addTranslations, _clear as clearTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service';
  import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
  import Locale from '@ckeditor/ckeditor5-utils/src/locale';
  import ResizeObserver from '@ckeditor/ckeditor5-utils/src/dom/resizeobserver';
  
  describe( 'ToolbarView', () => {
  	let locale, view;
  
  	testUtils.createSinonSandbox();
  
  	before( () => {
  		addTranslations( 'pl', {
  			'Editor toolbar': 'Pasek narzędzi edytora'
  		} );
  		addTranslations( 'en', {
  			'Editor toolbar': 'Editor toolbar'
  		} );
  	} );
  
  	after( () => {
  		clearTranslations();
  	} );
  
  	beforeEach( () => {
  		locale = new Locale();
  		view = new ToolbarView( locale );
  		view.render();
  	} );
  
  	afterEach( () => {
  		sinon.restore();
  		view.destroy();
  	} );
  
  	describe( 'constructor()', () => {
  		it( 'should set view#locale', () => {
  			expect( view.locale ).to.equal( locale );
  		} );
  
  		it( 'should set view#isCompact', () => {
  			expect( view.isCompact ).to.be.false;
  		} );
  
  		describe( '#options', () => {
  			it( 'should be an empty object if none were passed', () => {
  				expect( view.options ).to.deep.equal( {} );
  			} );
  
  			it( 'should be an empty object if none were passed', () => {
  				const options = {
  					foo: 'bar'
  				};
  
  				const toolbar = new ToolbarView( locale, options );
  
  				expect( toolbar.options ).to.equal( options );
  
  				toolbar.destroy();
  			} );
  		} );
  
  		it( 'should create view#items collection', () => {
  			expect( view.items ).to.be.instanceOf( ViewCollection );
  		} );
  
  		it( 'creates #focusTracker instance', () => {
  			expect( view.focusTracker ).to.be.instanceOf( FocusTracker );
  		} );
  
  		it( 'creates #keystrokes instance', () => {
  			expect( view.keystrokes ).to.be.instanceOf( KeystrokeHandler );
  		} );
  
  		it( 'should create view#itemsView', () => {
  			expect( view.itemsView ).to.be.instanceOf( View );
  		} );
  
  		it( 'should create view#children collection', () => {
  			expect( view.children ).to.be.instanceOf( ViewCollection );
  		} );
  
  		it( 'creates #_focusCycler instance', () => {
  			expect( view._focusCycler ).to.be.instanceOf( FocusCycler );
  		} );
  
  		it( 'creates #_behavior', () => {
  			expect( view._behavior ).to.be.an( 'object' );
  		} );
  	} );
  
  	describe( 'template', () => {
  		it( 'should create element from template', () => {
  			expect( view.element.classList.contains( 'ck' ) ).to.true;
  			expect( view.element.classList.contains( 'ck-toolbar' ) ).to.true;
  		} );
  
  		it( 'should create #itemsView from template', () => {
  			expect( view.element.firstChild ).to.equal( view.itemsView.element );
  			expect( view.itemsView.element.classList.contains( 'ck' ) ).to.true;
  			expect( view.itemsView.element.classList.contains( 'ck-toolbar__items' ) ).to.true;
  		} );
  
  		describe( 'attributes', () => {
  			it( 'should be defined', () => {
  				expect( view.element.getAttribute( 'role' ) ).to.equal( 'toolbar' );
  				expect( view.element.getAttribute( 'aria-label' ) ).to.equal( 'Editor toolbar' );
  			} );
  
  			it( 'should allow a custom aria-label', () => {
  				const view = new ToolbarView( locale );
  
  				view.ariaLabel = 'Custom label';
  
  				view.render();
  
  				expect( view.element.getAttribute( 'aria-label' ) ).to.equal( 'Custom label' );
  
  				view.destroy();
  			} );
  
  			it( 'should allow the aria-label to be translated', () => {
  				const view = new ToolbarView( new Locale( { uiLanguage: 'pl' } ) );
  
  				view.render();
  
  				expect( view.element.getAttribute( 'aria-label' ) ).to.equal( 'Pasek narzędzi edytora' );
  
  				view.destroy();
  			} );
  		} );
  
  		describe( 'event listeners', () => {
  			it( 'prevent default on #mousedown', () => {
  				const evt = new Event( 'mousedown', { bubbles: true } );
  				const spy = sinon.spy( evt, 'preventDefault' );
  
  				view.element.dispatchEvent( evt );
  				sinon.assert.calledOnce( spy );
  			} );
  		} );
  	} );
  
  	describe( 'element bindings', () => {
  		describe( 'class', () => {
  			it( 'reacts on view#class', () => {
  				view.class = 'foo';
  				expect( view.element.classList.contains( 'foo' ) ).to.be.true;
  
  				view.class = 'bar';
  				expect( view.element.classList.contains( 'bar' ) ).to.be.true;
  
  				view.class = false;
  				expect( view.element.classList.contains( 'foo' ) ).to.be.false;
  				expect( view.element.classList.contains( 'bar' ) ).to.be.false;
  			} );
  
  			it( 'reacts on view#isCompact', () => {
  				view.isCompact = false;
  				expect( view.element.classList.contains( 'ck-toolbar_compact' ) ).to.be.false;
  
  				view.isCompact = true;
  				expect( view.element.classList.contains( 'ck-toolbar_compact' ) ).to.be.true;
  			} );
  		} );
  
  		describe( 'style', () => {
  			it( 'reacts on view#maxWidth', () => {
  				view.maxWidth = '100px';
  				expect( view.element.style.maxWidth ).to.equal( '100px' );
  
  				view.maxWidth = undefined;
  				expect( view.element.style.maxWidth ).to.equal( '' );
  
  				view.maxWidth = null;
  				expect( view.element.style.maxWidth ).to.equal( '' );
  
  				view.maxWidth = '200px';
  				expect( view.element.style.maxWidth ).to.equal( '200px' );
  			} );
  		} );
  	} );
  
  	describe( 'render()', () => {
  		it( 'registers #items in #focusTracker', () => {
  			const view = new ToolbarView( locale );
  			const spyAdd = sinon.spy( view.focusTracker, 'add' );
  			const spyRemove = sinon.spy( view.focusTracker, 'remove' );
  
  			view.items.add( focusable() );
  			view.items.add( focusable() );
  			sinon.assert.notCalled( spyAdd );
  
  			view.render();
  
  			sinon.assert.calledTwice( spyAdd );
  
  			view.items.remove( 1 );
  			sinon.assert.calledOnce( spyRemove );
  
  			view.destroy();
  		} );
  
  		it( 'starts listening for #keystrokes coming from #element', () => {
  			const view = new ToolbarView( new Locale() );
  			const spy = sinon.spy( view.keystrokes, 'listenTo' );
  
  			view.render();
  			sinon.assert.calledOnce( spy );
  			sinon.assert.calledWithExactly( spy, view.element );
  
  			view.destroy();
  		} );
  
  		describe( 'activates keyboard navigation for the toolbar', () => {
  			it( 'so "arrowup" focuses previous focusable item', () => {
  				const keyEvtData = getArrowKeyData( 'arrowup' );
  
  				// No children to focus.
  				view.keystrokes.press( keyEvtData );
  				sinon.assert.calledOnce( keyEvtData.preventDefault );
  				sinon.assert.calledOnce( keyEvtData.stopPropagation );
  
  				view.items.add( nonFocusable() );
  				view.items.add( nonFocusable() );
  
  				// No focusable children.
  				view.keystrokes.press( keyEvtData );
  				sinon.assert.calledTwice( keyEvtData.preventDefault );
  				sinon.assert.calledTwice( keyEvtData.stopPropagation );
  
  				view.items.add( focusable() );
  				view.items.add( nonFocusable() );
  				view.items.add( focusable() );
  
  				// Mock the last item is focused.
  				view.focusTracker.isFocused = true;
  				view.focusTracker.focusedElement = view.items.get( 4 ).element;
  
  				view.keystrokes.press( keyEvtData );
  
  				sinon.assert.calledThrice( keyEvtData.preventDefault );
  				sinon.assert.calledThrice( keyEvtData.stopPropagation );
  				sinon.assert.calledOnce( view.items.get( 2 ).focus );
  			} );
  
  			it( 'so "arrowleft" focuses previous focusable item', () => {
  				const keyEvtData = getArrowKeyData( 'arrowleft' );
  
  				view.items.add( focusable() );
  				view.items.add( nonFocusable() );
  				view.items.add( focusable() );
  
  				// Mock the last item is focused.
  				view.focusTracker.isFocused = true;
  				view.focusTracker.focusedElement = view.items.get( 2 ).element;
  
  				view.keystrokes.press( keyEvtData );
  				sinon.assert.calledOnce( view.items.get( 0 ).focus );
  			} );
  
  			it( 'so "arrowdown" focuses next focusable item', () => {
  				const keyEvtData = getArrowKeyData( 'arrowdown' );
  
  				// No children to focus.
  				view.keystrokes.press( keyEvtData );
  				sinon.assert.calledOnce( keyEvtData.preventDefault );
  				sinon.assert.calledOnce( keyEvtData.stopPropagation );
  
  				view.items.add( nonFocusable() );
  				view.items.add( nonFocusable() );
  
  				// No focusable children.
  				view.keystrokes.press( keyEvtData );
  				sinon.assert.calledTwice( keyEvtData.preventDefault );
  				sinon.assert.calledTwice( keyEvtData.stopPropagation );
  
  				view.items.add( focusable() );
  				view.items.add( nonFocusable() );
  				view.items.add( focusable() );
  
  				// Mock the last item is focused.
  				view.focusTracker.isFocused = true;
  				view.focusTracker.focusedElement = view.items.get( 4 ).element;
  
  				view.keystrokes.press( keyEvtData );
  
  				sinon.assert.calledThrice( keyEvtData.preventDefault );
  				sinon.assert.calledThrice( keyEvtData.stopPropagation );
  				sinon.assert.calledOnce( view.items.get( 2 ).focus );
  			} );
  
  			it( 'so "arrowright" focuses next focusable item', () => {
  				const keyEvtData = getArrowKeyData( 'arrowright' );
  
  				view.items.add( focusable() );
  				view.items.add( nonFocusable() );
  				view.items.add( focusable() );
  
  				// Mock the last item is focused.
  				view.focusTracker.isFocused = true;
  				view.focusTracker.focusedElement = view.items.get( 0 ).element;
  
  				view.keystrokes.press( keyEvtData );
  				sinon.assert.calledOnce( view.items.get( 2 ).focus );
  			} );
  		} );
  
  		it( 'calls _behavior#render()', () => {
  			const view = new ToolbarView( locale );
  			sinon.spy( view._behavior, 'render' );
  
  			view.render();
  			sinon.assert.calledOnce( view._behavior.render );
  			sinon.assert.calledWithExactly( view._behavior.render, view );
  
  			view.destroy();
  		} );
  	} );
  
  	describe( 'destroy()', () => {
  		it( 'destroys the feature', () => {
  			sinon.spy( view._behavior, 'destroy' );
  
  			view.destroy();
  
  			sinon.assert.calledOnce( view._behavior.destroy );
  		} );
  
  		it( 'calls _behavior#destroy()', () => {
  			sinon.spy( view._behavior, 'destroy' );
  
  			view.destroy();
  			sinon.assert.calledOnce( view._behavior.destroy );
  		} );
  	} );
  
  	describe( 'focus()', () => {
  		it( 'focuses the first focusable of #items in DOM', () => {
  			// No children to focus.
  			view.focus();
  
  			// The second child is focusable.
  			view.items.add( nonFocusable() );
  			view.items.add( focusable() );
  			view.items.add( nonFocusable() );
  
  			view.focus();
  
  			sinon.assert.calledOnce( view.items.get( 1 ).focus );
  		} );
  	} );
  
  	describe( 'focusLast()', () => {
  		it( 'focuses the last focusable of #items in DOM', () => {
  			// No children to focus.
  			view.focusLast();
  
  			// The second child is focusable.
  			view.items.add( nonFocusable() );
  			view.items.add( focusable() );
  			view.items.add( focusable() );
  			view.items.add( focusable() );
  			view.items.add( nonFocusable() );
  
  			view.focusLast();
  
  			sinon.assert.calledOnce( view.items.get( 3 ).focus );
  		} );
  	} );
  
  	describe( 'fillFromConfig()', () => {
  		let factory;
  
  		beforeEach( () => {
  			factory = new ComponentFactory( {} );
  
  			factory.add( 'foo', namedFactory( 'foo' ) );
  			factory.add( 'bar', namedFactory( 'bar' ) );
  		} );
  
  		it( 'expands the config into collection', () => {
  			view.fillFromConfig( [ 'foo', 'bar', '|', 'foo' ], factory );
  
  			const items = view.items;
  
  			expect( items ).to.have.length( 4 );
  			expect( items.get( 0 ).name ).to.equal( 'foo' );
  			expect( items.get( 1 ).name ).to.equal( 'bar' );
  			expect( items.get( 2 ) ).to.be.instanceOf( ToolbarSeparatorView );
  			expect( items.get( 3 ).name ).to.equal( 'foo' );
  		} );
  
  		it( 'warns if there is no such component in the factory', () => {
  			const items = view.items;
  			const consoleWarnStub = sinon.stub( console, 'warn' );
  
  			view.fillFromConfig( [ 'foo', 'bar', 'baz' ], factory );
  
  			expect( items ).to.have.length( 2 );
  			expect( items.get( 0 ).name ).to.equal( 'foo' );
  			expect( items.get( 1 ).name ).to.equal( 'bar' );
  
  			sinon.assert.calledOnce( consoleWarnStub );
  			sinon.assert.calledWithExactly( consoleWarnStub,
  				sinon.match( /^toolbarview-item-unavailable/ ),
  				{ name: 'baz' }
  			);
  		} );
  	} );
  
  	describe( 'toolbar with static items', () => {
  		describe( 'constructor()', () => {
  			it( 'should set view#isVertical', () => {
  				expect( view.isVertical ).to.be.false;
  			} );
  
  			it( 'binds itemsView#children to #items', () => {
  				const itemA = focusable();
  				const itemB = focusable();
  				const itemC = focusable();
  
  				view.items.add( itemA );
  				view.items.add( itemB );
  				view.items.add( itemC );
  
  				expect( view.itemsView.children.map( i => i ) ).to.have.ordered.members( [ itemA, itemB, itemC ] );
  			} );
  
  			it( 'binds #focusables to #items', () => {
  				const itemA = focusable();
  				const itemB = focusable();
  				const itemC = focusable();
  
  				view.items.add( itemA );
  				view.items.add( itemB );
  				view.items.add( itemC );
  
  				expect( view.focusables.map( i => i ) ).to.have.ordered.members( [ itemA, itemB, itemC ] );
  			} );
  		} );
  
  		describe( 'element bindings', () => {
  			describe( 'class', () => {
  				it( 'reacts on view#isVertical', () => {
  					view.isVertical = false;
  					expect( view.element.classList.contains( 'ck-toolbar_vertical' ) ).to.be.false;
  
  					view.isVertical = true;
  					expect( view.element.classList.contains( 'ck-toolbar_vertical' ) ).to.be.true;
  				} );
  			} );
  		} );
  	} );
  
  	describe( 'toolbar with a dynamic item grouping', () => {
  		let locale, view, groupedItems, ungroupedItems, groupedItemsDropdown;
  		let resizeCallback, observeSpy, unobserveSpy;
  
  		beforeEach( () => {
  			observeSpy = sinon.spy();
  			unobserveSpy = sinon.spy();
  
  			// Make sure other tests of the editor do not affect tests that follow.
  			// Without it, if an instance of ResizeObserver already exists somewhere undestroyed
  			// in DOM, the following DOM mock will have no effect.
  			ResizeObserver._observerInstance = null;
  
  			testUtils.sinon.stub( global.window, 'ResizeObserver' ).callsFake( callback => {
  				resizeCallback = callback;
  
  				return {
  					observe: observeSpy,
  					unobserve: unobserveSpy
  				};
  			} );
  
  			locale = new Locale();
  			view = new ToolbarView( locale, {
  				shouldGroupWhenFull: true
  			} );
  
  			view.render();
  			view.element.style.width = '200px';
  			document.body.appendChild( view.element );
  
  			groupedItems = view._behavior.groupedItems;
  			ungroupedItems = view._behavior.ungroupedItems;
  			groupedItemsDropdown = view._behavior.groupedItemsDropdown;
  		} );
  
  		afterEach( () => {
  			view.element.remove();
  			view.destroy();
  		} );
  
  		describe( 'constructor()', () => {
  			it( 'extends the template with the CSS class', () => {
  				expect( view.element.classList.contains( 'ck-toolbar_grouping' ) ).to.be.true;
  			} );
  
  			it( 'updates the UI as new #items are added', () => {
  				sinon.spy( view._behavior, '_updateGrouping' );
  
  				const itemA = focusable();
  				const itemB = focusable();
  				const itemC = focusable();
  				const itemD = focusable();
  
  				view.element.style.width = '200px';
  
  				view.items.add( itemA );
  				view.items.add( itemB );
  
  				sinon.assert.calledTwice( view._behavior._updateGrouping );
  
  				expect( ungroupedItems ).to.have.length( 2 );
  				expect( groupedItems ).to.have.length( 0 );
  
  				view.items.add( itemC );
  
  				// The dropdown took some extra space.
  				expect( ungroupedItems ).to.have.length( 1 );
  				expect( groupedItems ).to.have.length( 2 );
  
  				view.items.add( itemD, 2 );
  
  				expect( ungroupedItems ).to.have.length( 1 );
  				expect( groupedItems ).to.have.length( 3 );
  
  				expect( ungroupedItems.map( i => i ) ).to.have.ordered.members( [ itemA ] );
  				expect( groupedItems.map( i => i ) ).to.have.ordered.members( [ itemB, itemD, itemC ] );
  			} );
  
  			it( 'updates the UI as #items are removed', () => {
  				const itemA = focusable();
  				const itemB = focusable();
  				const itemC = focusable();
  				const itemD = focusable();
  
  				view.element.style.width = '200px';
  
  				view.items.add( itemA );
  				view.items.add( itemB );
  				view.items.add( itemC );
  				view.items.add( itemD );
  
  				sinon.spy( view._behavior, '_updateGrouping' );
  				view.items.remove( 2 );
  
  				expect( ungroupedItems.map( i => i ) ).to.have.ordered.members( [ itemA ] );
  				expect( groupedItems.map( i => i ) ).to.have.ordered.members( [ itemB, itemD ] );
  
  				sinon.assert.calledOnce( view._behavior._updateGrouping );
  
  				view.items.remove( 0 );
  				sinon.assert.calledTwice( view._behavior._updateGrouping );
  
  				expect( ungroupedItems.map( i => i ) ).to.have.ordered.members( [ itemB, itemD ] );
  			} );
  
  			it( 'doesn\'t throw when removing the first of grouped items', () => { // (#7655)
  				const items = [ focusable(), focusable(), focusable(), focusable() ];
  				view.element.style.width = '200px';
  				view.items.addMany( items );
  
  				view.items.remove( 1 );
  
  				expect( ungroupedItems.map( i => i ) ).to.have.ordered.members( [ items[ 0 ] ] );
  				expect( groupedItems.map( i => i ) ).to.have.ordered.members( [ items[ 2 ], items[ 3 ] ] );
  			} );
  		} );
  
  		it( 'groups items that overflow into the dropdown', () => {
  			const itemA = focusable();
  			const itemB = focusable();
  			const itemC = focusable();
  			const itemD = focusable();
  
  			view.items.add( itemA );
  			view.items.add( itemB );
  			view.items.add( itemC );
  			view.items.add( itemD );
  
  			expect( ungroupedItems.map( i => i ) ).to.have.ordered.members( [ itemA ] );
  			expect( groupedItems.map( i => i ) ).to.have.ordered.members( [ itemB, itemC, itemD ] );
  			expect( view.children ).to.have.length( 3 );
  			expect( view.children.get( 0 ) ).to.equal( view.itemsView );
  			expect( view.children.get( 1 ) ).to.be.instanceOf( ToolbarSeparatorView );
  			expect( view.children.get( 2 ) ).to.equal( groupedItemsDropdown );
  		} );
  
  		it( 'ungroups items if there is enough space to display them (all)', () => {
  			const itemA = focusable();
  			const itemB = focusable();
  			const itemC = focusable();
  			const itemD = focusable();
  
  			view.items.add( itemA );
  			view.items.add( itemB );
  			view.items.add( itemC );
  			view.items.add( itemD );
  
  			expect( ungroupedItems.map( i => i ) ).to.have.ordered.members( [ itemA ] );
  			expect( groupedItems.map( i => i ) ).to.have.ordered.members( [ itemB, itemC, itemD ] );
  
  			view.element.style.width = '350px';
  
  			// Some grouped items cannot be ungrouped because there is not enough space and they will
  			// land back in #_behavior.groupedItems after an attempt was made.
  			view._behavior._updateGrouping();
  			expect( ungroupedItems.map( i => i ) ).to.have.ordered.members( [ itemA, itemB, itemC ] );
  			expect( groupedItems.map( i => i ) ).to.have.ordered.members( [ itemD ] );
  		} );
  
  		it( 'ungroups items if there is enough space to display them (some)', () => {
  			const itemA = focusable();
  			const itemB = focusable();
  			const itemC = focusable();
  
  			view.items.add( itemA );
  			view.items.add( itemB );
  			view.items.add( itemC );
  
  			expect( ungroupedItems.map( i => i ) ).to.have.ordered.members( [ itemA ] );
  			expect( groupedItems.map( i => i ) ).to.have.ordered.members( [ itemB, itemC ] );
  
  			view.element.style.width = '350px';
  
  			// All grouped items will be ungrouped because they fit just alright in the main space.
  			view._behavior._updateGrouping();
  			expect( ungroupedItems.map( i => i ) ).to.have.ordered.members( [ itemA, itemB, itemC ] );
  			expect( groupedItems ).to.have.length( 0 );
  			expect( view.children ).to.have.length( 1 );
  			expect( view.children.get( 0 ) ).to.equal( view.itemsView );
  		} );
  
  		describe( 'render()', () => {
  			let view, groupedItems, ungroupedItems;
  
  			beforeEach( () => {
  				view = new ToolbarView( locale, {
  					shouldGroupWhenFull: true
  				} );
  
  				observeSpy.resetHistory();
  				unobserveSpy.resetHistory();
  
  				view.render();
  
  				groupedItems = view._behavior.groupedItems;
  				ungroupedItems = view._behavior.ungroupedItems;
  
  				document.body.appendChild( view.element );
  			} );
  
  			afterEach( () => {
  				view.element.remove();
  				view.destroy();
  			} );
  
  			it( 'starts observing toolbar resize immediatelly after render', () => {
  				sinon.assert.calledOnce( observeSpy );
  				sinon.assert.calledWithExactly( observeSpy, view.element );
  			} );
  
  			it( 'updates the UI when the toolbar is being resized (expanding)', () => {
  				view.element.style.width = '200px';
  
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  
  				resizeCallback( [ {
  					target: view.element,
  					contentRect: new Rect( view.element )
  				} ] );
  				expect( ungroupedItems ).to.have.length( 1 );
  				expect( groupedItems ).to.have.length( 4 );
  
  				view.element.style.width = '500px';
  				resizeCallback( [ {
  					target: view.element,
  					contentRect: new Rect( view.element )
  				} ] );
  
  				expect( ungroupedItems ).to.have.length( 5 );
  				expect( groupedItems ).to.have.length( 0 );
  			} );
  
  			it( 'updates the UI when the toolbar is being resized (narrowing)', () => {
  				view.element.style.width = '500px';
  
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  
  				resizeCallback( [ {
  					target: view.element,
  					contentRect: new Rect( view.element )
  				} ] );
  				expect( ungroupedItems ).to.have.length( 5 );
  				expect( groupedItems ).to.have.length( 0 );
  
  				view.element.style.width = '200px';
  				resizeCallback( [ {
  					target: view.element,
  					contentRect: new Rect( view.element )
  				} ] );
  
  				expect( ungroupedItems ).to.have.length( 1 );
  				expect( groupedItems ).to.have.length( 4 );
  			} );
  
  			it( 'does not react to changes in height', () => {
  				view.element.style.width = '500px';
  				view.element.style.height = '200px';
  
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  
  				sinon.spy( view._behavior, '_updateGrouping' );
  				view.element.style.width = '500px';
  				resizeCallback( [ {
  					target: view.element,
  					contentRect: new Rect( view.element )
  				} ] );
  
  				sinon.assert.calledOnce( view._behavior._updateGrouping );
  				view.element.style.height = '500px';
  				resizeCallback( [ {
  					target: view.element,
  					contentRect: new Rect( view.element )
  				} ] );
  
  				sinon.assert.calledOnce( view._behavior._updateGrouping );
  			} );
  
  			it( 'updates the state of grouped items upon resize', () => {
  				testUtils.sinon.spy( view._behavior, '_updateGrouping' );
  				sinon.assert.notCalled( view._behavior._updateGrouping );
  
  				resizeCallback( [ {
  					target: view.element,
  					contentRect: new Rect( view.element )
  				} ] );
  
  				sinon.assert.calledOnce( view._behavior._updateGrouping );
  			} );
  
  			it( 'does not update the state of grouped items if invisible', () => {
  				view.element.style.width = '500px';
  				view.element.style.height = '200px';
  
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  
  				expect( ungroupedItems ).to.have.length( 5 );
  				expect( groupedItems ).to.have.length( 0 );
  
  				view.element.style.display = 'none';
  				view.maxWidth = '100px';
  
  				expect( ungroupedItems ).to.have.length( 5 );
  				expect( groupedItems ).to.have.length( 0 );
  			} );
  
  			it( 'should queue the update of the grouped items state when invisible (and execute it when visible again)', () => {
  				view.maxWidth = '200px';
  
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  
  				expect( ungroupedItems ).to.have.length( 1 );
  				expect( groupedItems ).to.have.length( 4 );
  
  				view.element.style.display = 'none';
  
  				resizeCallback( [ {
  					target: view.element,
  					contentRect: new Rect( view.element )
  				} ] );
  
  				// Response to this change will be queued.
  				view.maxWidth = '500px';
  
  				expect( ungroupedItems ).to.have.length( 1 );
  				expect( groupedItems ).to.have.length( 4 );
  
  				// The queued items state should happen after that.
  				view.element.style.display = 'flex';
  
  				resizeCallback( [ {
  					target: view.element,
  					contentRect: new Rect( view.element )
  				} ] );
  
  				expect( ungroupedItems ).to.have.length( 5 );
  				expect( groupedItems ).to.have.length( 0 );
  			} );
  
  			it( 'should fire the "groupedItemsUpdate" event on the toolbar when some item is grouped or ungrouped', () => {
  				const updateSpy = sinon.spy();
  
  				view.on( 'groupedItemsUpdate', updateSpy );
  
  				view.element.style.width = '200px';
  
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  
  				resizeCallback( [ {
  					target: view.element,
  					contentRect: new Rect( view.element )
  				} ] );
  
  				sinon.assert.calledOnce( updateSpy );
  
  				// This 10px is not enough to ungroup an item.
  				view.element.style.width = '210px';
  
  				resizeCallback( [ {
  					target: view.element,
  					contentRect: new Rect( view.element )
  				} ] );
  
  				sinon.assert.calledOnce( updateSpy );
  
  				// But this is not enough to ungroup some items.
  				view.element.style.width = '300px';
  
  				resizeCallback( [ {
  					target: view.element,
  					contentRect: new Rect( view.element )
  				} ] );
  
  				sinon.assert.calledTwice( updateSpy );
  			} );
  		} );
  
  		describe( 'destroy()', () => {
  			it( 'destroys the #groupedItemsDropdown', () => {
  				view.element.style.width = '200px';
  
  				const itemA = focusable();
  				const itemB = focusable();
  				const itemC = focusable();
  				const itemD = focusable();
  
  				view.items.add( itemA );
  				view.items.add( itemB );
  				view.items.add( itemC );
  				view.items.add( itemD );
  
  				sinon.spy( groupedItemsDropdown, 'destroy' );
  
  				view.element.style.width = '500px';
  
  				// The dropdown hides; it does not belong to any collection but it still exist.
  				view._behavior._updateGrouping();
  
  				view.destroy();
  				sinon.assert.calledOnce( groupedItemsDropdown.destroy );
  			} );
  
  			it( 'should destroy the #resizeObserver', () => {
  				view.element.style.width = '200px';
  
  				const itemA = focusable();
  				const itemB = focusable();
  				const itemC = focusable();
  				const itemD = focusable();
  
  				view.items.add( itemA );
  				view.items.add( itemB );
  				view.items.add( itemC );
  				view.items.add( itemD );
  
  				sinon.spy( view._behavior.resizeObserver, 'destroy' );
  
  				view.destroy();
  				sinon.assert.calledOnce( view._behavior.resizeObserver.destroy );
  			} );
  		} );
  
  		describe( 'dropdown with grouped items', () => {
  			it( 'has proper DOM structure', () => {
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  
  				expect( view.children.has( groupedItemsDropdown ) ).to.be.true;
  				expect( groupedItemsDropdown.element.classList.contains( 'ck-toolbar__grouped-dropdown' ) );
  				expect( groupedItemsDropdown.buttonView.label ).to.equal( 'Show more items' );
  			} );
  
  			it( 'shares its toolbarView#items with grouped items', () => {
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  
  				expect( groupedItemsDropdown.toolbarView.items.map( i => i ) )
  					.to.have.ordered.members( groupedItems.map( i => i ) );
  			} );
  
  			// https://github.com/ckeditor/ckeditor5/issues/5608
  			it( 'has the proper position depending on the UI language direction (LTR UI)', () => {
  				const locale = new Locale( { uiLanguage: 'en' } );
  				const view = new ToolbarView( locale, { shouldGroupWhenFull: true } );
  				view.render();
  
  				expect( view._behavior.groupedItemsDropdown.panelPosition ).to.equal( 'sw' );
  
  				view.destroy();
  			} );
  
  			// https://github.com/ckeditor/ckeditor5/issues/5608
  			it( 'has the proper position depending on the UI language direction (RTL UI)', () => {
  				const locale = new Locale( { uiLanguage: 'ar' } );
  				const view = new ToolbarView( locale, { shouldGroupWhenFull: true } );
  				view.render();
  
  				expect( view._behavior.groupedItemsDropdown.panelPosition ).to.equal( 'se' );
  
  				view.destroy();
  			} );
  		} );
  
  		describe( 'item overflow checking logic', () => {
  			it( 'considers the right padding of the toolbar (LTR UI)', () => {
  				view.class = 'ck-reset_all';
  				view.element.style.width = '210px';
  				view.element.style.paddingLeft = '0px';
  				view.element.style.paddingRight = '20px';
  
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  
  				expect( view._behavior.groupedItems ).to.have.length( 1 );
  			} );
  
  			it( 'considers the left padding of the toolbar (RTL UI)', () => {
  				const locale = new Locale( { uiLanguage: 'ar' } );
  				const view = new ToolbarView( locale, {
  					shouldGroupWhenFull: true
  				} );
  
  				view.extendTemplate( {
  					attributes: {
  						dir: locale.uiLanguageDirection
  					}
  				} );
  
  				view.render();
  				document.body.appendChild( view.element );
  
  				view.class = 'ck-reset_all';
  				view.element.style.width = '210px';
  				view.element.style.paddingLeft = '20px';
  				view.element.style.paddingRight = '0px';
  
  				view.items.add( focusable() );
  				view.items.add( focusable() );
  
  				expect( view._behavior.groupedItems ).to.have.length( 1 );
  
  				view.destroy();
  				view.element.remove();
  			} );
  		} );
  
  		describe( 'focus management', () => {
  			it( '#focus() focuses the dropdown when it is the only focusable', () => {
  				sinon.spy( groupedItemsDropdown, 'focus' );
  				view.element.style.width = '10px';
  
  				const itemA = focusable();
  				const itemB = focusable();
  
  				view.items.add( itemA );
  				view.items.add( itemB );
  
  				expect( view.focusables.map( i => i ) ).to.have.ordered.members( [ groupedItemsDropdown ] );
  
  				view.focus();
  				sinon.assert.calledOnce( groupedItemsDropdown.focus );
  			} );
  
  			it( '#focusLast() focuses the dropdown when present', () => {
  				sinon.spy( groupedItemsDropdown, 'focus' );
  				view.element.style.width = '200px';
  
  				const itemA = focusable();
  				const itemB = focusable();
  				const itemC = focusable();
  
  				view.items.add( itemA );
  				view.items.add( itemB );
  				view.items.add( itemC );
  
  				expect( view.focusables.map( i => i ) ).to.have.ordered.members( [ itemA, groupedItemsDropdown ] );
  
  				view.focusLast();
  
  				sinon.assert.calledOnce( groupedItemsDropdown.focus );
  
  				view.element.remove();
  			} );
  		} );
  	} );
  } );
  
  function focusable() {
  	const view = nonFocusable();
  
  	view.label = 'focusable';
  	view.focus = sinon.stub().callsFake( () => {
  		view.element.focus();
  	} );
  
  	view.extendTemplate( {
  		attributes: {
  			tabindex: -1
  		}
  	} );
  
  	return view;
  }
  
  function nonFocusable() {
  	const view = new View();
  
  	view.set( 'label', 'non-focusable' );
  
  	const bind = view.bindTemplate;
  
  	view.setTemplate( {
  		tag: 'div',
  		attributes: {
  			style: {
  				padding: '0',
  				margin: '0',
  				width: '100px',
  				height: '100px',
  				background: 'rgba(255,0,0,.3)'
  			}
  		},
  		children: [
  			{
  				text: bind.to( 'label' )
  			}
  		]
  	} );
  
  	return view;
  }
  
  function namedFactory( name ) {
  	return locale => {
  		const view = new View( locale );
  
  		view.name = name;
  		view.element = document.createElement( 'a' );
  
  		return view;
  	};
  }
  
  function getArrowKeyData( arrow ) {
  	return {
  		keyCode: keyCodes[ arrow ],
  		preventDefault: sinon.spy(),
  		stopPropagation: sinon.spy()
  	};
  }
  