Skip to content
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

Fixed issue with textwatcher throttle invalid selection #2376

Closed
wants to merge 17 commits into from

Conversation

jacekbogdanski
Copy link
Member

What is the purpose of this pull request?

Bug fix

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

  • Unit tests
  • Manual tests

What changes did you make?

Throttling textwatcher#check function sometimes results with out of sync selection. I moved selection getter into a throttled function.

Closes #2373

@mlewand mlewand self-requested a review September 7, 2018 09:39
@mlewand mlewand self-assigned this Sep 7, 2018
@mlewand mlewand self-assigned this Sep 10, 2018
Copy link
Contributor

@mlewand mlewand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It fixes the error, yes, but it takes away optimization that we want our devs to have - that we make sure that callback is not called by check too often.

See the comments for more info.

on: {
instanceReady: function() {
new CKEDITOR.plugins.textWatcher( editor, function( selectionRange ) {
var isSelCorrect = editor.getSelection().getRanges()[0].startContainer.equals( selectionRange.startContainer );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code style, missing spaces in [0].


bot.setHtmlWithSelection( '<b>xxx</b><i>[yyy]</i>' );

setTimeout( function() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know why this test is asynchronous and has a timeout? The assertion could be placed straight inside callback passed to attachTextWatcher making test synchronous and allowing us to drop this timeout.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I understand you correctly. The issue occurs when the selection changes between throttled inputs and the late input is fired with an invalid selection.

Also, the issue is only reproducible with enabled throttling.

How could we test this case with the synchronous path? There will be no time to broke selection between textwatcher.check and textwatcher._buffer.input execution.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What this test is doing is essentially an equivalent of the following:

		// (#2373)
		'test throttle synchronization': function() {
			var editor = this.editor,
				bot = this.editorBot,
				textWatcher = attachTextWatcher( editor, function( selectionRange ) {
					var range = editor.getSelection().getRanges()[ 0 ];

					if ( editor.getSelection().getSelectedText() === 'yyy' ) {
						resume( function() {
							assert.isTrue( range.startContainer.equals( selectionRange.startContainer ) );
						} );
					}

					return {
						text: 'test'
					};
				}, 100 );

			bot.setHtmlWithSelection( '<b>[xxx]</b><i>yyy</i>' );

			textWatcher.check( {} );
			textWatcher.check( {} );

			bot.setHtmlWithSelection( '<b>xxx</b><i>[yyy]</i>' );

			wait();
		},

Note that there is no timeout. Same can be used for the next test case.

this._listeners.push( editable.attachListener( editable, 'keyup', check, this ) );
}

// CKEditor's event system has a limitation that one function (in this case this.check)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's nice that we were able to drop this quirk code 👍

@@ -253,7 +232,18 @@
return;
}

this._buffer.input( selectionRange );
var matched = this.callback( selectionRange );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By inlining this code we're breaking the feature of this API, that check's are throttled out of the box, and end user does not need to worry about it.

With this proposition you're changing how the API behaves, and it will always call the TextWatcher.callback() which might be expensive.

What if we change thinking a bit? What if the actual fetch of selection and range happens inside the throttled function? It would make sense to put it in both places, I mean: leave it where it is now on master and put inside throttled function.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pushed solution based on above in the t/2373b branch. It also results with smaller LoC added compared to the original code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that there's one issue - in case if during thorttled call there's no selection (edge case, but we need to consider it) there's an early return.

https://github.com/ckeditor/ckeditor-dev/blob/b5e9817/plugins/textwatcher/plugin.js#L177-L179

It would be much better to notify unmatch, because we're no longer matching anything, right? It would be nice to add unit test coverage for this edge case.

@jacekbogdanski
Copy link
Member Author

You are right that I accidentally removed the throttling feature from check function. I merged branches and added required changes.

@mlewand mlewand closed this Sep 12, 2018
@Comandeer Comandeer changed the base branch from next to master September 12, 2018 10:07
@Comandeer Comandeer reopened this Sep 12, 2018
@mlewand mlewand self-assigned this Sep 13, 2018
Copy link
Contributor

@mlewand mlewand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please simplify the tests a little bit, ignore the test for mobile and I think we're good to go.

// There are cases where the selection needs to be refreshed after debounce. (#2373)
var selectionRange = this._getRange( this.editor );

if ( !selectionRange ) {
Copy link
Contributor

@mlewand mlewand Sep 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code can be further simplified. Now you have two separate code branches that call this.unmatch() if that's possible you want to reuse code branches if possible - that increases the readability.

Also this code has a bug as it is possible that it will fire unmatch event even if nothing was matched prior testTextMatch call. This is exactly what is happening in your test case. See test empty selection does not fire stray unmatched event unit test, and revert plugins\textwatcher\plugin.js file to previous version to see how it works.

I have fixed this issue in c8bca2a.

<div id="editor"></div>

<script>
var statusEl = document.getElementById( 'status' ),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This manual test case is not relevant for mobile browsers, and should be ignored.

@mlewand
Copy link
Contributor

mlewand commented Sep 13, 2018

I have added a couple of commits that do address some issues.

@jacekbogdanski jacekbogdanski self-assigned this Sep 17, 2018
@jacekbogdanski jacekbogdanski added the pr:frozen ❄ The pull request on which work was suspended due to various reasons. label Mar 23, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pr:frozen ❄ The pull request on which work was suspended due to various reasons.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants