|
3 | 3 | * For licensing, see LICENSE.md. |
4 | 4 | */ |
5 | 5 |
|
6 | | -/* globals document, window, NodeFilter */ |
| 6 | +/* globals document, window, NodeFilter, MutationObserver */ |
7 | 7 |
|
8 | 8 | import View from '../../src/view/view'; |
9 | 9 | import ViewElement from '../../src/view/element'; |
@@ -2302,7 +2302,7 @@ describe( 'Renderer', () => { |
2302 | 2302 | } ); |
2303 | 2303 |
|
2304 | 2304 | // #1417 |
2305 | | - describe( 'optimal rendering', () => { |
| 2305 | + describe( 'optimal rendering – reusing existing nodes', () => { |
2306 | 2306 | it( 'should render inline element replacement (before text)', () => { |
2307 | 2307 | viewRoot._appendChild( parse( '<container:p><attribute:i>A</attribute:i>1</container:p>' ) ); |
2308 | 2308 |
|
@@ -3126,6 +3126,264 @@ describe( 'Renderer', () => { |
3126 | 3126 | } ); |
3127 | 3127 | } ); |
3128 | 3128 |
|
| 3129 | + describe( 'optimal (minimal) rendering – minimal children changes', () => { |
| 3130 | + let observer; |
| 3131 | + |
| 3132 | + beforeEach( () => { |
| 3133 | + observer = new MutationObserver( () => {} ); |
| 3134 | + |
| 3135 | + observer.observe( domRoot, { |
| 3136 | + childList: true, |
| 3137 | + attributes: false, |
| 3138 | + subtree: false |
| 3139 | + } ); |
| 3140 | + } ); |
| 3141 | + |
| 3142 | + afterEach( () => { |
| 3143 | + observer.disconnect(); |
| 3144 | + } ); |
| 3145 | + |
| 3146 | + it( 'should add only one child (at the beginning)', () => { |
| 3147 | + viewRoot._appendChild( parse( '<container:p>1</container:p>' ) ); |
| 3148 | + |
| 3149 | + renderer.markToSync( 'children', viewRoot ); |
| 3150 | + renderer.render(); |
| 3151 | + cleanObserver( observer ); |
| 3152 | + |
| 3153 | + viewRoot._insertChild( 0, parse( '<container:p>2</container:p>' ) ); |
| 3154 | + |
| 3155 | + renderer.markToSync( 'children', viewRoot ); |
| 3156 | + renderer.render(); |
| 3157 | + |
| 3158 | + expect( getMutationStats( observer.takeRecords() ) ).to.deep.equal( [ |
| 3159 | + 'added: 1, removed: 0' |
| 3160 | + ] ); |
| 3161 | + } ); |
| 3162 | + |
| 3163 | + it( 'should add only one child (at the end)', () => { |
| 3164 | + viewRoot._appendChild( parse( '<container:p>1</container:p>' ) ); |
| 3165 | + |
| 3166 | + renderer.markToSync( 'children', viewRoot ); |
| 3167 | + renderer.render(); |
| 3168 | + cleanObserver( observer ); |
| 3169 | + |
| 3170 | + viewRoot._appendChild( parse( '<container:p>2</container:p>' ) ); |
| 3171 | + |
| 3172 | + renderer.markToSync( 'children', viewRoot ); |
| 3173 | + renderer.render(); |
| 3174 | + |
| 3175 | + expect( getMutationStats( observer.takeRecords() ) ).to.deep.equal( [ |
| 3176 | + 'added: 1, removed: 0' |
| 3177 | + ] ); |
| 3178 | + } ); |
| 3179 | + |
| 3180 | + it( 'should add only one child (in the middle)', () => { |
| 3181 | + viewRoot._appendChild( parse( '<container:p>1</container:p><container:p>2</container:p>' ) ); |
| 3182 | + |
| 3183 | + renderer.markToSync( 'children', viewRoot ); |
| 3184 | + renderer.render(); |
| 3185 | + cleanObserver( observer ); |
| 3186 | + |
| 3187 | + viewRoot._insertChild( 1, parse( '<container:p>3</container:p>' ) ); |
| 3188 | + |
| 3189 | + renderer.markToSync( 'children', viewRoot ); |
| 3190 | + renderer.render(); |
| 3191 | + |
| 3192 | + expect( getMutationStats( observer.takeRecords() ) ).to.deep.equal( [ |
| 3193 | + 'added: 1, removed: 0' |
| 3194 | + ] ); |
| 3195 | + } ); |
| 3196 | + |
| 3197 | + it( 'should not touch elements at all (rendering texts is enough)', () => { |
| 3198 | + viewRoot._appendChild( parse( '<container:p>1</container:p><container:p>2</container:p>' ) ); |
| 3199 | + |
| 3200 | + renderer.markToSync( 'children', viewRoot ); |
| 3201 | + renderer.render(); |
| 3202 | + cleanObserver( observer ); |
| 3203 | + |
| 3204 | + viewRoot._insertChild( 1, parse( '<container:p>3</container:p>' ) ); |
| 3205 | + viewRoot._removeChildren( 0, 1 ); |
| 3206 | + |
| 3207 | + renderer.markToSync( 'children', viewRoot ); |
| 3208 | + renderer.render(); |
| 3209 | + |
| 3210 | + expect( getMutationStats( observer.takeRecords() ) ).to.be.empty; |
| 3211 | + } ); |
| 3212 | + |
| 3213 | + it( 'should add and remove one', () => { |
| 3214 | + viewRoot._appendChild( parse( '<container:p>1</container:p><container:p>2</container:p>' ) ); |
| 3215 | + |
| 3216 | + renderer.markToSync( 'children', viewRoot ); |
| 3217 | + renderer.render(); |
| 3218 | + cleanObserver( observer ); |
| 3219 | + |
| 3220 | + viewRoot._insertChild( 1, parse( '<container:h1>3</container:h1>' ) ); |
| 3221 | + viewRoot._removeChildren( 0, 1 ); |
| 3222 | + |
| 3223 | + renderer.markToSync( 'children', viewRoot ); |
| 3224 | + renderer.render(); |
| 3225 | + |
| 3226 | + expect( getMutationStats( observer.takeRecords() ) ).to.deep.equal( [ |
| 3227 | + 'added: 1, removed: 0', |
| 3228 | + 'added: 0, removed: 1' |
| 3229 | + ] ); |
| 3230 | + } ); |
| 3231 | + |
| 3232 | + it( 'should not touch the FSC when rendering children', () => { |
| 3233 | + viewRoot._appendChild( parse( '<container:p>1</container:p><container:p>2</container:p>' ) ); |
| 3234 | + |
| 3235 | + // Set fake selection on the second paragraph. |
| 3236 | + selection._setTo( viewRoot.getChild( 1 ), 'on', { fake: true } ); |
| 3237 | + |
| 3238 | + renderer.markToSync( 'children', viewRoot ); |
| 3239 | + renderer.render(); |
| 3240 | + cleanObserver( observer ); |
| 3241 | + |
| 3242 | + // Remove the second paragraph. |
| 3243 | + viewRoot._removeChildren( 1, 1 ); |
| 3244 | + // And set the fake selection on the first one. |
| 3245 | + selection._setTo( viewRoot.getChild( 0 ), 'on', { fake: true } ); |
| 3246 | + |
| 3247 | + renderer.markToSync( 'children', viewRoot ); |
| 3248 | + renderer.render(); |
| 3249 | + |
| 3250 | + expect( getMutationStats( observer.takeRecords() ) ).to.deep.equal( [ |
| 3251 | + 'added: 0, removed: 1' |
| 3252 | + ] ); |
| 3253 | + } ); |
| 3254 | + |
| 3255 | + describe( 'using fastDiff() - significant number of nodes in the editor', () => { |
| 3256 | + it( 'should add only one child (at the beginning)', () => { |
| 3257 | + viewRoot._appendChild( parse( makeContainers( 151 ) ) ); |
| 3258 | + |
| 3259 | + renderer.markToSync( 'children', viewRoot ); |
| 3260 | + renderer.render(); |
| 3261 | + cleanObserver( observer ); |
| 3262 | + |
| 3263 | + viewRoot._insertChild( 0, parse( '<container:p>x</container:p>' ) ); |
| 3264 | + |
| 3265 | + renderer.markToSync( 'children', viewRoot ); |
| 3266 | + renderer.render(); |
| 3267 | + |
| 3268 | + expect( getMutationStats( observer.takeRecords() ) ).to.deep.equal( [ |
| 3269 | + 'added: 1, removed: 0' |
| 3270 | + ] ); |
| 3271 | + } ); |
| 3272 | + |
| 3273 | + it( 'should add only one child (at the end)', () => { |
| 3274 | + viewRoot._appendChild( parse( makeContainers( 151 ) ) ); |
| 3275 | + |
| 3276 | + renderer.markToSync( 'children', viewRoot ); |
| 3277 | + renderer.render(); |
| 3278 | + cleanObserver( observer ); |
| 3279 | + |
| 3280 | + viewRoot._appendChild( parse( '<container:p>x</container:p>' ) ); |
| 3281 | + |
| 3282 | + renderer.markToSync( 'children', viewRoot ); |
| 3283 | + renderer.render(); |
| 3284 | + |
| 3285 | + expect( getMutationStats( observer.takeRecords() ) ).to.deep.equal( [ |
| 3286 | + 'added: 1, removed: 0' |
| 3287 | + ] ); |
| 3288 | + } ); |
| 3289 | + |
| 3290 | + it( 'should add only one child (in the middle)', () => { |
| 3291 | + viewRoot._appendChild( parse( makeContainers( 151 ) ) ); |
| 3292 | + |
| 3293 | + renderer.markToSync( 'children', viewRoot ); |
| 3294 | + renderer.render(); |
| 3295 | + cleanObserver( observer ); |
| 3296 | + |
| 3297 | + viewRoot._insertChild( 75, parse( '<container:p>x</container:p>' ) ); |
| 3298 | + |
| 3299 | + renderer.markToSync( 'children', viewRoot ); |
| 3300 | + renderer.render(); |
| 3301 | + |
| 3302 | + expect( getMutationStats( observer.takeRecords() ) ).to.deep.equal( [ |
| 3303 | + 'added: 1, removed: 0' |
| 3304 | + ] ); |
| 3305 | + } ); |
| 3306 | + |
| 3307 | + it( 'should not touch elements at all (rendering texts is enough)', () => { |
| 3308 | + viewRoot._appendChild( parse( makeContainers( 151 ) ) ); |
| 3309 | + |
| 3310 | + renderer.markToSync( 'children', viewRoot ); |
| 3311 | + renderer.render(); |
| 3312 | + cleanObserver( observer ); |
| 3313 | + |
| 3314 | + viewRoot._insertChild( 1, parse( '<container:p>x</container:p>' ) ); |
| 3315 | + viewRoot._removeChildren( 0, 1 ); |
| 3316 | + |
| 3317 | + renderer.markToSync( 'children', viewRoot ); |
| 3318 | + renderer.render(); |
| 3319 | + |
| 3320 | + expect( getMutationStats( observer.takeRecords() ) ).to.be.empty; |
| 3321 | + } ); |
| 3322 | + |
| 3323 | + it( 'should add and remove one', () => { |
| 3324 | + viewRoot._appendChild( parse( makeContainers( 151 ) ) ); |
| 3325 | + |
| 3326 | + renderer.markToSync( 'children', viewRoot ); |
| 3327 | + renderer.render(); |
| 3328 | + cleanObserver( observer ); |
| 3329 | + |
| 3330 | + viewRoot._insertChild( 1, parse( '<container:h1>x</container:h1>' ) ); |
| 3331 | + viewRoot._removeChildren( 0, 1 ); |
| 3332 | + |
| 3333 | + renderer.markToSync( 'children', viewRoot ); |
| 3334 | + renderer.render(); |
| 3335 | + |
| 3336 | + expect( getMutationStats( observer.takeRecords() ) ).to.deep.equal( [ |
| 3337 | + 'added: 1, removed: 0', |
| 3338 | + 'added: 0, removed: 1' |
| 3339 | + ] ); |
| 3340 | + } ); |
| 3341 | + |
| 3342 | + it( 'should not touch the FSC when rendering children', () => { |
| 3343 | + viewRoot._appendChild( parse( makeContainers( 151 ) ) ); |
| 3344 | + |
| 3345 | + // Set fake selection on the second paragraph. |
| 3346 | + selection._setTo( viewRoot.getChild( 1 ), 'on', { fake: true } ); |
| 3347 | + |
| 3348 | + renderer.markToSync( 'children', viewRoot ); |
| 3349 | + renderer.render(); |
| 3350 | + cleanObserver( observer ); |
| 3351 | + |
| 3352 | + // Remove the second paragraph. |
| 3353 | + viewRoot._removeChildren( 1, 1 ); |
| 3354 | + // And set the fake selection on the first one. |
| 3355 | + selection._setTo( viewRoot.getChild( 0 ), 'on', { fake: true } ); |
| 3356 | + |
| 3357 | + renderer.markToSync( 'children', viewRoot ); |
| 3358 | + renderer.render(); |
| 3359 | + |
| 3360 | + expect( getMutationStats( observer.takeRecords() ) ).to.deep.equal( [ |
| 3361 | + 'added: 0, removed: 1' |
| 3362 | + ] ); |
| 3363 | + } ); |
| 3364 | + } ); |
| 3365 | + |
| 3366 | + function getMutationStats( mutationList ) { |
| 3367 | + return mutationList.map( mutation => { |
| 3368 | + return `added: ${ mutation.addedNodes.length }, removed: ${ mutation.removedNodes.length }`; |
| 3369 | + } ); |
| 3370 | + } |
| 3371 | + |
| 3372 | + function cleanObserver( observer ) { |
| 3373 | + observer.takeRecords(); |
| 3374 | + } |
| 3375 | + |
| 3376 | + function makeContainers( howMany ) { |
| 3377 | + const containers = []; |
| 3378 | + |
| 3379 | + for ( let i = 1; i <= howMany; i++ ) { |
| 3380 | + containers.push( `<container:p>${ i }</container:p>` ); |
| 3381 | + } |
| 3382 | + |
| 3383 | + return containers.join( '' ); |
| 3384 | + } |
| 3385 | + } ); |
| 3386 | + |
3129 | 3387 | // #1560 |
3130 | 3388 | describe( 'attributes manipulation on replaced element', () => { |
3131 | 3389 | it( 'should rerender element if it was removed after having its attributes removed (attribute)', () => { |
|
0 commit comments