-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix for: Errors thrown when Editor is detached and destroyed during initialization #3128
Conversation
@alinaciuysal the issue has been added to the 4.12.0 milestone (see #3115) so if everything goes smooth it will be available since the 4.12.0 release. Bear in mind that the time may be delayed if we will struggle with some unexpected problems, but hopefully we would be able to close it with 4.12.0. Please, subscribe #3115 for updates. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rebased to the latest major
.
I'm missing a lot of test coverage which makes reviewing hard as it's a bit unknown why some changes have been introduced.
Also, you refactored a lot of tests introducing editors.element
stub object like:
editor.element = {
isDetached: function() {},
getAttribute: function() {}
};
It hides a real issue where according to our documentation, editor's element
is optional:
https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#method-constructor
but your changes makes it obligatory.
@@ -109,6 +109,27 @@ | |||
assert.isNull( editor.container.getCustomData( 'x' ), 'Custom data purged' ); | |||
} ); | |||
} ); | |||
}, | |||
|
|||
'test destroy when editor.container is absent': function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing tag reference. Please, update the rest of the tests also.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
☝️ I have same errors on major
branch. If you check on createConcurrentEditorTest helper you will see that it is intentional to test this warning.
core/creators/themedui.js
Outdated
@@ -239,6 +239,9 @@ CKEDITOR.replaceClass = 'ckeditor'; | |||
|
|||
// Delay to avoid race conditions (setMode inside setMode). | |||
setTimeout( function() { | |||
if ( editor.status === 'destroyed' || editor.container.isDetached() ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are using this condition multiple times, so it seems like a good candidate for method extraction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing unit tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually this case is supposed to be covered by
test editor set mode when editor is detached'
and
test editor set mode when editor is destroyed'
But these tests are false positive, because they use fictional mode :sadfrog:.
core/editor.js
Outdated
@@ -189,7 +189,7 @@ | |||
|
|||
// Return the editor instance immediately to enable early stage event registrations. | |||
CKEDITOR.tools.setTimeout( function() { | |||
if ( this.status !== 'destroyed' ) { | |||
if ( this.status !== 'destroyed' && !this.element.isDetached() ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change doesn't seem to be covered by unit tests.
core/editor.js
Outdated
return; | ||
} | ||
|
||
if ( editor.container && editor.container.isDetached() ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No unit test coverage for this condition.
plugins/clipboard/plugin.js
Outdated
@@ -1203,8 +1203,10 @@ | |||
} | |||
|
|||
function setToolbarStates() { | |||
if ( editor.mode != 'wysiwyg' ) | |||
if ( editor.mode != 'wysiwyg' || editor.status === 'destroyed' || editor.container.isDetached() ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No test coverage. Also, why this change has been introduced?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It produced an error for me, but I see after other changes it doesn't occur anymore, so probably now it can be reverted.
plugins/wysiwygarea/plugin.js
Outdated
@@ -89,6 +89,11 @@ | |||
|
|||
function onLoad( evt ) { | |||
evt && evt.removeListener(); | |||
|
|||
if ( editor.status === 'destroyed' || editor.container.isDetached() ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No tests coverage.
plugins/wysiwygarea/plugin.js
Outdated
iframe.remove(); | ||
if ( iframe.getParent() ) { | ||
iframe.remove(); | ||
} | ||
} else { | ||
CKEDITOR.warn( 'editor-destroy-iframe' ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This warning probably doesn't seem sense anymore after changes.
var editor = bot.editor, | ||
container = editor.container; | ||
|
||
delete editor.container; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm missing verification when the container is alive but in detached mode.
The general problem with unit test is that editor has to be detached at the right moment, so that given change in code is executed, if it's too early than it might be not reached. With that amount of async code it's quite tricky. I'll try to add some more cases, but I can't guarantee to cover them all. |
With fine amount of dirty hack replacing methods and stealing provided arguments I managed to cover all the cases with tests. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Failing test at /core/editor/destroy
.
I can see that there is some memory increase when destroying editors between snapshots:
Also, allocation timeline shows some increase, around 150-200 KB for each create/destroy pair. However, I'm getting similar results with the editor without your changes (just skipping issue with container removed before destroy), so it seems more like the general issue than introduced by your changes. Nevertheless, I'm wondering if this test could be simplified. Memory leaks testing requires a bit of patience and depends highly on the browser dev tools, i.e. in the manual test you assumed that the tester uses Chrome, but it's available also for Safari and Edge. Tools also often change.
Also, there is no IE test coverage (and Edge is covered partially). I know that the tests may be hard to write for this case, but we shouldn't leave core change untested correctly.
If it won't be possible to add unit tests for these changes, we should have at least some really well written manual tests.
core/editor.js
Outdated
* @private | ||
* @return {boolean} | ||
*/ | ||
_shouldPreventInit: function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method name doesn't tell much. Maybe something like isDestroyed
? Wondering if we can assume that missing/detached editor's container means that it's destroyed 🤔
Also, as the function is used among multiple modules, it should be rather a public API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isDestroyed
would be very misleading. Even when editor is detached it still should be destroyed for proper cleanup, such name would suggest that if editor isDestroyed
you might not need to call editor.destroy
anymore.
Sometimes I have this dilemma which name is better:
- what it does
- what's the purpose
In this case second option feels better for me. Eventually condition might be updated, but with such a name it wouldn't be a problem. If we went for first option it'd be named like isDestroyedOrDetached
, and in that case updating condition might be not possible without changing a name. Unless we want to have outdated name which doesn't tell what it does any more.
return true; | ||
} | ||
|
||
return this.status === 'destroyed'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe simplify a bit into oneliner?
return this.status === 'destroyed' || this.container && this.container.isDetached()
plugins/wysiwygarea/plugin.js
Outdated
iframe.remove(); | ||
} else { | ||
CKEDITOR.warn( 'editor-destroy-iframe' ); | ||
if ( iframe.getParent() ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't be used isDetached
method here?
tests/core/skin/skin.js
Outdated
@@ -55,6 +55,12 @@ bender.test( { | |||
var skin = CKEDITOR.skin; | |||
|
|||
var editor = new CKEDITOR.editor(); | |||
|
|||
editor.element = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like you already fixed the issue with missing editor element. If so, do we still require this dirty stub among unit tests?
When initially working on memory leaks #2969 we decided to test only Chrome. We reduced amount of leaks there, so we can compare if given branch produces any new leaks or not. For other browsers we didn't test or fix leaks, doing so is out of scope of this ticket. Other browser have memory profiling tools they aren't nearly as good as Chromes. Even if they allow to see how much memory is leaking they aren't really helpful to find out what is causing leaks. New changes
|
Rebased into the latest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implementation starts to be a bit complicated.
- please, update version tags into
4.13.0
- some tests are failing, see CI
- manual test gives no feedback when clicking
create editor
button. Looks like the test is broken - the only manual test is still written for Chrome (however, not ignored for other browsers this time). The most visible issue reproduction is error thrown in dev console. Can't it determine the test result for other browsers without more advanced dev tools?
I'm not sure about your latest changes. Does the whole expandoNumber
workaround is created only for upstream Edge issue? If so, isn't better to report an upstream issue than creating workarounds? Silencing errors isn't the prettiest way to hide the issue. Especially, that at some point MS may decide to fix the issue (when moving to chromium ) and we will end up with dead code. Are you able to verify which accessor throws an error?
Of course, if the editor blows because of this issue, a workaround may make sense.
core/editable.js
Outdated
this.removeClass( 'cke_editable' ); | ||
catchEdgePermissionDenied( function() { | ||
this.removeClass( 'cke_editable' ); | ||
}.bind( this ) ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use CKEDITOR.tools.bind
instead.
core/editable.js
Outdated
@@ -1264,6 +1270,17 @@ | |||
} | |||
} ); | |||
|
|||
function catchEdgePermissionDenied( callback ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using a try/catch clause for an upstream issue is the last resort. Does this issue make editor unusable/unstable? It seems a more obvious choice to report upstream issue than providing such workaround.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replied here: #3128 (comment)
core/editable.js
Outdated
try { | ||
callback(); | ||
} catch ( error ) { | ||
if ( !CKEDITOR.env.edge || error.number !== -2146828218 ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I bet error code may change during Edge update. Do you think it's safe to use it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that it's possible but unlikely that error code will change. Yet it is safe, worst thing that could happen is that same error with different code will be thrown. However without using this check we might silent different error.
Previously we did it this way:
https://github.com/ckeditor/ckeditor-dev/blob/c4d250ea5fa0da09781a3ee84c99ca03f09c144b/core/editable.js#L100-L104
But IMO it's not optimal, as it might silent other errors. If error code changes tho, we might repeat logic from above.
core/editor.js
Outdated
* @return {boolean} | ||
*/ | ||
shouldPreventInitialization: function() { | ||
return this.status === 'destroyed' || this.container && this.container.isDetached(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It still says nothing about function purpose. You need to read implementation to understand what's going on. Even isDetachedOrDestroyed
seems like a better name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The purpose is to tell when editor initialization should be prevented, so this name says everything you need to know. Condition under we prevent initialization might change in the future, so binding function name to the implementation doesn't feel right to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function has too low-level implementation for such high abstraction name. But let's leave it for now and back later during final refactoring.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a big mismatch between API docs for this method and its name. isDetachedOrDestroyed
seems a better choice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, especially that docs says the same:
Returns boolean whether editor is destroyed or detached.
👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've come with an idea to replace this method with simple isDetached()
and implement or
statement in places where we actually checking if editor is detached or destroyed.
This removes this additional abstraction here and remains much cleaner API.
<li><input type="radio" name="destroy" value="1"><label>async with 0 timeout</label></li> | ||
<li><input type="radio" name="destroy" value="2"><label>before 'loaded' event</label></li> | ||
<li><input type="radio" name="destroy" value="3"><label>before 'scriptLoader.load' callback called</label></li> | ||
<li><input type="radio" name="destroy" value="4"><label>before iframe#onload</label></li> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this button is disabled all the time despite using different options. Couldn't spot a way to enable it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is enabled only when browser is Firefox or IE (not Edge), and editor type is classic
. Reason for that is only in this case iframe#onload
is used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you see it readable in a current shape? It looks like there is something broken when testing not listed browsers, not like it has been disabled on purpose. I see 2 options:
- enable it despite browser sniffing, so it will prevent against regressions as browser sniffing should be treated as an internal mechanism
- write some note at this radio so it's known that it should only work for FF and IE
I'm rather for unifying test case among browsers (enabling it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll add an message in test. This condition is the same as one deciding if iframe#onload
should be used. So it doesn't matter if browser sniffing is working or not. This case is enabled only in environment which uses this listener for creating editor. Also this case could break test in other browsers, so I'm for having it disabled when it shouldn't be tested.
The problem with Edge permission denied is that it's occurring randomly, I couldn't reproduce it outside of CKEditor. I doubt Edges team would confirm issue without easy reproduction steps. By fixing it with try/catch on our side, we are not just silenting it, but also allowing execution of code that otherwise wouldn't be reached because of error. This allows to finish the cleanup, otherwise some data of removed elements won't be ever released. What's more that error is very visible in our framework integrations.
When Chromium based MS Edge will be released we will have to investigate carefully how whole editor works on it. We have so many conditional code for Edge, that it all should be revised. I don't think that we should worry as much about how browsers might work in the future, instead we should focus on providing Editor that works right now. Even if it means sometimes we need to fix browser issue on our side.
Test is written for all of the browsers. It has EXTRA steps for Chrome to make sure no memory leaks are introduced with changes. When initially working on memory leaks we decided to work only with Chrome. There is no reason to put our effort to test memory leaks for every browser now if we didn't do it in dedicated ticket. |
…variations. Improve checks in algorithm.
Taking over this review from @Comandeer. Rebased onto latest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I extracted those failing tests to separate issue #3426. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 👍 🎉
Good job @engineering-this, @jacekbogdanski, @Comandeer and @msamsel 😄
Unit tests
- Chrome (CI) ✅
- Firefox (CI) ✅
- Safari ✅
- IE8 ✅
- IE11 ✅
- Edge ✅
Taken over review of this PR.
What is the purpose of this pull request?
A bit of fix a bit of feature
Does your PR contain necessary tests?
All patches which change the editor code must include tests. You can always read more
on PR testing,
how to set the testing environment and
how to create tests
in the official CKEditor documentation.
This PR contains
Proposed changelog entry
What changes did you make?
Fix
Within various places in code I've added checks if editor is destroyed or detached then execution of following code is prevented. When possible
editor.container
is used to check, otherwiseeditor.element
.Edge && IE
MS browsers sometimes throws
Permission Denied
, however browser won't break on such error, so it's nearly impossible to fix it. I've spend some time checkingtimeouts
andonload
listeners with no success.Note: This PR shouldn't break anything for Edge or IE, because when editor is detached than it will throws errors regardless of having this fix or not.
Other stuff
https://github.com/ckeditor/ckeditor-dev/compare/t/3115?expand=1#diff-162a1b36d2efd62081522822a026e853R195
☝️ I think about removing above message, as now editor can be destroyed without any issues.
this PR introduces new method
CKEDITOR.dom.element.isDetached
(element.isDetached
check if element is detached from DOM #3124)Closes #718.
Closes #2257.
Closes #3115.
Closes #3124.