diff --git a/src/editor/editorui.js b/src/editor/editorui.js index 55dabdc3..0b594aae 100644 --- a/src/editor/editorui.js +++ b/src/editor/editorui.js @@ -80,6 +80,7 @@ export default class EditorUI { destroy() { this.stopListening(); this.view.destroy(); + this.focusTracker.destroy(); } /** diff --git a/tests/_utils/memory.js b/tests/_utils/memory.js new file mode 100644 index 00000000..77b75f6d --- /dev/null +++ b/tests/_utils/memory.js @@ -0,0 +1,164 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* global window, document, setTimeout */ + +const TEST_TIMEOUT = 6000; +const GARBAGE_COLLECTOR_TIMEOUT = 500; + +/** + * Memory tests suite definition that: + * - skips tests when garbage collector is not available. + * - creates/destroys editor element (id = 'mem-editor'). + * + * This method should be used with dedicated memory usage test case functions: + * + * describe( 'editor', () => { + * // Other tests. + * + * describeMemoryUsage( () => { + * testMemoryUsage( 'and editor', () => { + * return ClassicEditor.create( document.querySelector( '#mem-editor' ) ); + * } ); + * } ); + * } ); + * + * @param {Function} callback Callback with test suit body + */ +export function describeMemoryUsage( callback ) { + describe( 'memory usage', () => { + skipIfNoGarbageCollector(); + + beforeEach( createEditorElement ); + + afterEach( destroyEditorElement ); + + callback(); + } ); +} + +/** + * Single test case for memory usage test. This method will handle memory usage test procedure: + * - creating editor instance + * - recording its memory usage (after garbage collector) + * - create and destroy editor 10 times + * - record memory usage after final editor destroy (after garbage collector) + * - tests if memory grew + * + * See {@link describeMemoryUsage} function for usage details. + * + * @param {String} testName Name of a test case. + * @param {Function} editorCreator Callback which creates editor and returns it's `.create()` promise. + */ +export function testMemoryUsage( testName, editorCreator ) { + it( testName, function() { + this.timeout( TEST_TIMEOUT ); + + return runTest( editorCreator ); + } ); +} + +// Runs a single test case. This method will properly setup memory-leak test: +// - create editor +// - run garbage collector +// - record memory allocations +// - destroy the editor +// - create & destroy editor multiple times (9) - after each editor creation the test runner will be paused for ~200ms +function runTest( editorCreator ) { + const createEditor = createAndDestroy( editorCreator ); + + let memoryAfterFirstStart; + + return Promise.resolve() // Promise.resolve just to align below code. + // First editor creation needed to load all editor code,css into the memory (it is reported as used heap memory) + .then( createEditor ) + .then( editor => { + return collectMemoryStats().then( mem => { + memoryAfterFirstStart = mem; + + return editor; + } ); + } ) + .then( destroy ) + // Run create-wait-destroy multiple times. Multiple runs to grow memory significantly even on smaller leaks. + .then( testAndDestroy ) // #2 + .then( testAndDestroy ) // #3 + .then( testAndDestroy ) // #4 + .then( testAndDestroy ) // #5 + .then( testAndDestroy ) // #6 + .then( testAndDestroy ) // #7 + .then( testAndDestroy ) // #8 + .then( testAndDestroy ) // #9 + .then( testAndDestroy ) // #10 + .then( () => { + return new Promise( resolve => { + collectMemoryStats().then( memory => { + const memoryDifference = memory.usedJSHeapSize - memoryAfterFirstStart.usedJSHeapSize; + + expect( memoryDifference, 'used heap size should not grow' ).to.be.at.most( 0 ); + resolve(); + } ); + } ); + } ); + + function testAndDestroy() { + return Promise.resolve() + .then( createEditor ) + .then( destroy ); + } +} + +function createEditorElement() { + const editorElement = document.createElement( 'div' ); + editorElement.id = 'mem-editor'; + + editorElement.innerHTML = + '

Editor 1

\n' + + '

This is an editor instance. And there\'s some link.

'; + + document.body.appendChild( editorElement ); +} + +function destroyEditorElement() { + document.getElementById( 'mem-editor' ).remove(); +} + +function createAndDestroy( editorCreator ) { + return () => editorCreator(); +} + +function destroy( editor ) { + return editor.destroy(); +} + +function collectMemoryStats() { + return new Promise( resolve => { + // Enforce garbage collection before recording memory stats. + window.gc(); + + setTimeout( () => { + const memeInfo = window.performance.memory; + + resolve( { + totalJSHeapSize: memeInfo.totalJSHeapSize, + usedJSHeapSize: memeInfo.usedJSHeapSize, + jsHeapSizeLimit: memeInfo.jsHeapSizeLimit + } ); + }, GARBAGE_COLLECTOR_TIMEOUT ); + } ); +} + +// Will skip test suite if not in compatible browser. +// Currently on Google Chrome supports this method and must be run with proper flags: +// +// google-chrome -js-flags="--expose-gc" +// +function skipIfNoGarbageCollector() { + before( function() { + if ( !window.gc ) { + this.skip(); + } + } ); +}