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

Fix creating and replacing legacy widgets in customizer #32005

Merged
merged 4 commits into from
May 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ function widgetIdToSettingId( widgetId ) {
return `widget_${ idBase }`;
}

/**
* This is a custom debounce function to call different callbacks depending on
* whether it's the _leading_ call or not.
*
* @param {Function} leading The callback that gets called first.
* @param {Function} callback The callback that gets called after the first time.
* @param {number} timeout The debounced time in milliseconds.
* @return {Function} The debounced function.
*/
function debounce( leading, callback, timeout ) {
let isLeading = false;
let timerID;
Expand Down Expand Up @@ -200,6 +209,13 @@ export default class SidebarAdapter {

_removeWidget( widget ) {
const settingId = widgetIdToSettingId( widget.id );
const setting = this.api( settingId );

if ( setting ) {
const instance = setting.get();
this.widgetsCache.delete( instance );
}

this.api.remove( settingId );
}

Expand Down Expand Up @@ -277,7 +293,10 @@ export default class SidebarAdapter {
return widgetId;
} );

// TODO: We should in theory also handle delete widgets here too.
const deletedWidgets = this.getWidgets().filter(
( widget ) => ! nextWidgetIds.includes( widget.id )
);
deletedWidgets.forEach( ( widget ) => this._removeWidget( widget ) );
kevin940726 marked this conversation as resolved.
Show resolved Hide resolved

this.setting.set( nextWidgetIds );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ function blockToWidget( block, existingWidget = null ) {
idBase: block.attributes.idBase,
instance: {
...existingWidget?.instance,
// Required only for the customizer.
is_widget_customizer_js_value: true,
encoded_serialized_instance: encoded,
instance_hash_key: hash,
raw_instance: raw,
Expand Down
79 changes: 79 additions & 0 deletions packages/e2e-tests/plugins/class-test-widget.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php
/**
* Plugin Name: Gutenberg Test Widgets
* Plugin URI: https://github.com/WordPress/gutenberg
* Author: Gutenberg Team
*
* @package gutenberg-test-widgets
*/

/**
* Test widget to be used in e2e tests.
*/
class Test_Widget extends WP_Widget {
/**
* Sets up a new test widget instance.
*/
function __construct() {
parent::__construct(
'test_widget',
'Test Widget',
array( 'description' => 'Test widget.' )
);
}

/**
* Outputs the content for the widget instance.
*
* @param array $args Display arguments including 'before_title', 'after_title',
* 'before_widget', and 'after_widget'.
* @param array $instance Settings for the current Block widget instance.
*/
public function widget( $args, $instance ) {
$title = apply_filters( 'widget_title', $instance['title'] );
echo $args['before_widget'];
if ( ! empty( $title ) ) {
echo $args['before_title'] . $title . $args['after_title'];
}
echo 'Hello Test Widget';
echo $args['after_widget'];
}

/**
* Outputs the widget settings form.
*
* @param array $instance Current instance.
*/
public function form( $instance ) {
?>
<p>
<label for="<?php echo $this->get_field_id( 'title' ); ?>">Title:</label>
<input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $instance['title'] ); ?>" />
</p>
<?php
}

/**
* Handles updating settings for the current widget instance.
*
* @param array $new_instance New settings for this instance as input by the user via
* WP_Widget::form().
*
* @return array Settings to save or bool false to cancel saving.
* @since 4.8.1
*/
public function update( $new_instance ) {
$instance = array();
$instance['title'] = ( ! empty( $new_instance['title'] ) ) ? strip_tags( $new_instance['title'] ) : '';
return $instance;
}
}

/**
* Register the widget.
*/
function load_test_widget() {
register_widget( 'Test_Widget' );
}

add_action( 'widgets_init', 'load_test_widget' );
152 changes: 152 additions & 0 deletions packages/e2e-tests/specs/widgets/customizing-widgets.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
showBlockToolbar,
clickBlockToolbarButton,
deleteAllWidgets,
createURL,
} from '@wordpress/e2e-test-utils';

/**
Expand Down Expand Up @@ -43,12 +44,14 @@ describe( 'Widgets Customizer', () => {
await deactivatePlugin(
'gutenberg-test-plugin-disables-the-css-animations'
);
await activatePlugin( 'gutenberg-test-widgets' );
} );

afterAll( async () => {
await activatePlugin(
'gutenberg-test-plugin-disables-the-css-animations'
);
await deactivatePlugin( 'gutenberg-test-widgets' );
await activateTheme( 'twentytwentyone' );
} );

Expand Down Expand Up @@ -512,6 +515,141 @@ describe( 'Widgets Customizer', () => {
);
} );

it( 'should handle legacy widgets', async () => {
const widgetsPanel = await find( {
role: 'heading',
name: /Widgets/,
level: 3,
} );
await widgetsPanel.click();

const footer1Section = await find( {
role: 'heading',
name: /^Footer #1/,
level: 3,
} );
await footer1Section.click();

const legacyWidgetBlock = await addBlock( 'Legacy Widget' );
const selectLegacyWidgets = await find( {
role: 'combobox',
name: 'Select a legacy widget to display:',
} );
await selectLegacyWidgets.select( 'test_widget' );

await expect( {
role: 'heading',
name: 'Test Widget',
level: 3,
} ).toBeFound( { root: legacyWidgetBlock } );

let titleInput = await find(
{
role: 'textbox',
name: 'Title:',
},
{
root: legacyWidgetBlock,
}
);

await titleInput.type( 'Hello Title' );

// Unfocus the current legacy widget.
await page.keyboard.press( 'Tab' );

// Disable reason: Sometimes the preview just doesn't fully load,
// it's the only way I know for now to ensure that the iframe is ready.
// eslint-disable-next-line no-restricted-syntax
await page.waitForTimeout( 2000 );
await waitForPreviewIframe();

// Expect the legacy widget to show in the site preview frame.
await expect( {
role: 'heading',
name: 'Hello Title',
} ).toBeFound( {
root: await find( {
name: 'Site Preview',
selector: 'iframe',
} ),
} );

// Expect the preview in block to show when unfocusing the legacy widget block.
await expect( {
role: 'heading',
name: 'Hello Title',
} ).toBeFound( {
root: await find( {
selector: 'iframe',
name: 'Legacy Widget Preview',
} ),
} );

await legacyWidgetBlock.focus();
await showBlockToolbar();

// Testing removing the block.
await clickBlockToolbarButton( 'Options' );
const removeBlockButton = await find( {
role: 'menuitem',
name: /Remove block/,
} );
await removeBlockButton.click();

// Add it back again using the variant.
const testWidgetBlock = await addBlock( 'Test Widget' );

titleInput = await find(
{
role: 'textbox',
name: 'Title:',
},
{
root: testWidgetBlock,
}
);

await titleInput.type( 'Hello again!' );
// Unfocus the current legacy widget.
await page.keyboard.press( 'Tab' );

// Expect the preview in block to show when unfocusing the legacy widget block.
await expect( {
role: 'heading',
name: 'Hello again!',
} ).toBeFound( {
root: await find( {
selector: 'iframe',
name: 'Legacy Widget Preview',
} ),
} );

const publishButton = await find( {
role: 'button',
name: 'Publish',
} );
await publishButton.click();

// Wait for publishing to finish.
await page.waitForResponse( createURL( '/wp-admin/admin-ajax.php' ) );
await expect( publishButton ).toMatchQuery( {
disabled: true,
} );

expect( console ).toHaveWarned(
"The page delivered both an 'X-Frame-Options' header and a 'Content-Security-Policy' header with a 'frame-ancestors' directive. Although the 'X-Frame-Options' header alone would have blocked embedding, it has been ignored."
);

await page.goto( createURL( '/' ) );

// Expect the saved widgets to show on frontend.
await expect( {
role: 'heading',
name: 'Hello again!',
} ).toBeFound();
} );

it( 'should handle esc key events', async () => {
const widgetsPanel = await find( {
role: 'heading',
Expand Down Expand Up @@ -592,6 +730,20 @@ async function addBlock( blockName ) {
);
await addBlockButton.click();

const searchBox = await find( {
role: 'searchbox',
name: 'Search for blocks and patterns',
} );

// Clear the input.
await searchBox.evaluate( ( node ) => {
if ( node.value ) {
node.value = '';
}
} );

await searchBox.type( blockName );

// TODO - remove this timeout when the test plugin for disabling CSS
// animations in tests works properly.
//
Expand Down