diff --git a/src/js/media/models/attachment.js b/src/js/media/models/attachment.js index 267624b7d6c48..6c525cb32e76b 100644 --- a/src/js/media/models/attachment.js +++ b/src/js/media/models/attachment.js @@ -71,9 +71,14 @@ Attachment = Backbone.Model.extend(/** @lends wp.media.model.Attachment.prototyp } else if ( 'delete' === method ) { options = options || {}; - if ( ! options.wait ) { - this.destroyed = true; - } + /* + * Mark as destroyed before the request so mirrored collections + * filter it out when the `destroy` event fires. With `wait: true`, + * Backbone triggers `destroy` before the request's `done` handler, + * so setting the flag there was too late and the deleted item + * lingered in the grid. Restored on failure. See #65513. + */ + this.destroyed = true; options.context = this; options.data = _.extend( options.data || {}, { @@ -82,9 +87,7 @@ Attachment = Backbone.Model.extend(/** @lends wp.media.model.Attachment.prototyp _wpnonce: this.get('nonces')['delete'] }); - return wp.media.ajax( options ).done( function() { - this.destroyed = true; - }).fail( function() { + return wp.media.ajax( options ).fail( function() { this.destroyed = false; }); diff --git a/src/js/media/models/attachments.js b/src/js/media/models/attachments.js index 23510bd949f4c..2189cab66c611 100644 --- a/src/js/media/models/attachments.js +++ b/src/js/media/models/attachments.js @@ -204,7 +204,7 @@ var Attachments = Backbone.Collection.extend(/** @lends wp.media.model.Attachmen * Start observing another attachments collection change events * and replicate them on this collection. * - * @param {wp.media.model.Attachments} The attachments collection to observe. + * @param {wp.media.model.Attachments} attachments The attachments collection to observe. * @return {wp.media.model.Attachments} Returns itself to allow chaining. */ observe: function( attachments ) { @@ -212,9 +212,18 @@ var Attachments = Backbone.Collection.extend(/** @lends wp.media.model.Attachmen this.observers.push( attachments ); attachments.on( 'add change remove', this._validateHandler, this ); - attachments.on( 'add', this._addToTotalAttachments, this ); - attachments.on( 'remove', this._removeFromTotalAttachments, this ); attachments.on( 'reset', this._validateAllHandler, this ); + + /* + * Only count the mirrored query collection. Counting other observed + * collections such as the selection double-counts an uploaded image + * that is also auto-selected, breaking the count. See #65513. + */ + if ( attachments === this.mirroring ) { + attachments.on( 'add', this._addToTotalAttachments, this ); + attachments.on( 'remove', this._removeFromTotalAttachments, this ); + } + this.validateAll( attachments ); return this; }, @@ -239,7 +248,7 @@ var Attachments = Backbone.Collection.extend(/** @lends wp.media.model.Attachmen return this; }, /** - * Update total attachment count when items are added to a collection. + * Update total attachment count when items are removed from a collection. * * @access private * diff --git a/tests/e2e/specs/featured-image-modal.test.js b/tests/e2e/specs/featured-image-modal.test.js new file mode 100644 index 0000000000000..0068d5acd6437 --- /dev/null +++ b/tests/e2e/specs/featured-image-modal.test.js @@ -0,0 +1,79 @@ +/** + * WordPress dependencies + */ +import { test, expect } from '@wordpress/e2e-test-utils-playwright'; + +/** + * External dependencies + */ +import path from 'path'; + +const TEST_IMAGE = path.join( + __dirname, + '..', + '..', + 'phpunit', + 'data', + 'images', + 'test-image-1-100x100.jpg' +); + +/** + * @see https://core.trac.wordpress.org/ticket/65513 + */ +test.describe( 'Featured image modal media count and deletion', () => { + test.beforeEach( async ( { requestUtils } ) => { + // Start from an empty Media Library so the count is deterministic. + await requestUtils.deleteAllMedia(); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await requestUtils.deleteAllMedia(); + } ); + + test( 'reports the correct count on first upload and removes a deleted item from the grid', async ( { + page, + admin, + } ) => { + await admin.createNewPost( { showWelcomeGuide: false } ); + + // `wp.media` is enqueued on the post editing screen. + await page.waitForFunction( + () => window.wp && window.wp.media && window.wp.media.featuredImage + ); + + // The Featured Image modal opens on the Upload tab. + await page.evaluate( () => window.wp.media.featuredImage.frame().open() ); + + const modal = page.locator( '.media-modal' ); + await expect( modal ).toBeVisible(); + + await modal + .locator( 'input[type="file"]' ) + .setInputFiles( TEST_IMAGE ); + + await page.evaluate( () => window.wp.media.frame.content.mode( 'browse' ) ); + + await expect( + modal.locator( '.attachments .attachment' ) + ).toHaveCount( 1 ); + + // The uploaded image must be counted once, not once per collection it + // belongs to (mirrored query + auto-selected selection). + await expect( modal.locator( '.load-more-count' ) ).toHaveText( + 'Showing 1 of 1 media items' + ); + + page.on( 'dialog', ( dialog ) => dialog.accept() ); + + // The uploaded image is auto-selected, so its details are already shown. + await modal + .locator( '.attachment-details .delete-attachment' ) + .click(); + + // The deleted attachment must not linger in the grid. + await expect( + modal.locator( '.attachments .attachment' ) + ).toHaveCount( 0 ); + } ); +} );