diff --git a/bin/add-test-widgets-to-sidebar.php b/bin/add-test-widgets-to-sidebar.php
new file mode 100644
index 00000000000..b84313a6e29
--- /dev/null
+++ b/bin/add-test-widgets-to-sidebar.php
@@ -0,0 +1,459 @@
+ 'media_audio',
+ 'settings' => array_merge(
+ amp_media_widget( 'audio' ),
+ array(
+ 'title' => 'Test Audio Widget: No Loop or Preload',
+ )
+ ),
+ ),
+ array(
+ 'widget' => 'media_audio',
+ 'settings' => array_merge(
+ amp_media_widget( 'audio' ),
+ array(
+ 'title' => 'Test Audio Widget: With Loop, Preload',
+ 'loop' => true,
+ 'preload' => 'auto',
+ )
+ ),
+ ),
+ array(
+ 'widget' => 'archives',
+ 'settings' => array(
+ 'title' => 'Test Archives Widget: No Dropdown or Count',
+ ),
+ ),
+ array(
+ 'widget' => 'archives',
+ 'settings' => array(
+ 'title' => 'Test Archives Widget: With Dropdown, Count',
+ 'count' => 1,
+ 'dropdown' => 1,
+ ),
+ ),
+ array(
+ 'widget' => 'calendar',
+ ),
+ array(
+ 'widget' => 'categories',
+ 'settings' => array(
+ 'title' => 'Test Categories Widget: No Count, Dropdown, or Hierarchical',
+ ),
+ ),
+ array(
+ 'widget' => 'categories',
+ 'settings' => array(
+ 'title' => 'Test Categories Widget: With Count',
+ 'count' => 1,
+ ),
+ ),
+ array(
+ 'widget' => 'categories',
+ 'settings' => array(
+ 'title' => 'Test Categories Widget: With Count, Dropdown, And Hierarchical',
+ 'count' => 1,
+ 'dropdown' => 1,
+ 'hierarchical' => 1,
+ ),
+ ),
+ array(
+ 'widget' => 'custom_html',
+ 'settings' => array(
+ 'title' => 'Test Custom HTML Widget',
+ 'content' => '
Example Custom HTML widget
Here is some custom HTML with a link',
+ ),
+ ),
+ array(
+ 'widget' => 'media_gallery',
+ 'settings' => array_merge(
+ amp_gallery_widget(),
+ array(
+ 'link_type' => 'post',
+ 'orderby_random' => false,
+ 'size' => 'thumbnail',
+ )
+ ),
+ ),
+ array(
+ 'widget' => 'media_image',
+ 'settings' => array_merge(
+ amp_image_widget(),
+ array(
+ 'alt' => 'Example alt value',
+ 'caption' => 'Example caption',
+ 'image_classes' => 'foo bar',
+ 'image_title' => 'example image title',
+ 'link_classes' => 'baz bar',
+ 'link_rel' => 'nofollow',
+ 'link_target_blank' => false,
+ 'link_type' => 'custom',
+ 'link_url' => 'http://example.com/amp-test',
+ 'size' => 'full',
+ )
+ ),
+ ),
+ array(
+ 'widget' => 'meta',
+ ),
+ array(
+ 'widget' => 'nav_menu',
+ 'settings' => array(
+ 'nav_menu' => amp_menu(),
+ ),
+ ),
+ array(
+ 'widget' => 'pages',
+ ),
+ array(
+ 'widget' => 'recent-comments',
+ ),
+ array(
+ 'widget' => 'recent-posts',
+ 'settings' => array(
+ 'show_date' => true,
+ ),
+ ),
+ array(
+ 'widget' => 'rss',
+ 'settings' => array(
+ 'title' => 'Test RSS Widget: No Content, Author, or Date',
+ 'url' => 'https://amphtml.wordpress.com/feed/',
+ 'show_author' => 0,
+ 'show_date' => 0,
+ 'show_summary' => 0,
+ ),
+ ),
+ array(
+ 'widget' => 'rss',
+ 'settings' => array(
+ 'title' => 'Test RSS Widget: With Content, Author, Date',
+ 'url' => 'https://amphtml.wordpress.com/feed/',
+ 'show_author' => 1,
+ 'show_date' => 1,
+ 'show_summary' => 1,
+ ),
+ ),
+ array(
+ 'widget' => 'search',
+ ),
+ array(
+ 'widget' => 'tag_cloud',
+ 'settings' => array(
+ 'title' => 'Test Tag Widget, No Count',
+ ),
+ ),
+ array(
+ 'widget' => 'tag_cloud',
+ 'settings' => array(
+ 'title' => 'Test Tag Widget, With Count',
+ 'count' => 1,
+ ),
+ ),
+ array(
+ 'widget' => 'text',
+ 'settings' => array(
+ 'filter' => true,
+ 'text' => 'Example Headline- This is to test possible text
- This should display as expected
',
+ 'visual' => true,
+ ),
+ ),
+ array(
+ 'widget' => 'media_video',
+ 'settings' => array_merge(
+ amp_media_widget( 'video' ),
+ array(
+ 'title' => 'Test Video Widget: No Loop or Preload',
+ 'loop' => false,
+ 'preload' => 'none',
+ )
+ ),
+ ),
+ array(
+ 'widget' => 'media_video',
+ 'settings' => array_merge(
+ amp_media_widget( 'video' ),
+ array(
+ 'title' => 'Test Video Widget: With Loop, Preload',
+ 'loop' => true,
+ 'preload' => 'metadata',
+ )
+ ),
+ ),
+ );
+}
+
+/**
+ * Get the settings for a media widget.
+ *
+ * Can apply to 'audio' or 'video' widgets.
+ * Queries for a single post of the media type.
+ * If none exists, amp_media() will throw an error.
+ *
+ * @param string $type The type of media. like 'audio' or 'video'.
+ * @return array $audio_widget The settings for the media widget.
+ */
+function amp_media_widget( $type ) {
+ $all_media = amp_media( $type, 1 );
+ $media = reset( $all_media );
+ return array(
+ 'attachment_id' => $media->ID,
+ 'url' => $media->guid,
+ );
+}
+
+/**
+ * Get the settings for the gallery widget.
+ *
+ * @return array $audio_widget The settings for the gallery widget.
+ */
+function amp_gallery_widget() {
+ $images = amp_media( 'image', 3 );
+ return array(
+ 'ids' => wp_list_pluck( $images, 'ID' ),
+ );
+}
+
+/**
+ * Gets the settings for the image widget.
+ *
+ * @return array $image_widget The settings for the image widget.
+ */
+function amp_image_widget() {
+ $all_images = amp_media( 'image', 1 );
+ $image = reset( $all_images );
+ $metadata = wp_get_attachment_metadata( $image->ID );
+ $default_dimension = 100;
+ return array(
+ 'attachment_id' => $image->ID,
+ 'height' => isset( $metadata['height'] ) ? $metadata['height'] : $default_dimension,
+ 'width' => isset( $metadata['width'] ) ? $metadata['width'] : $default_dimension,
+ 'url' => wp_get_attachment_url( $image->ID ),
+ );
+}
+
+/**
+ * Get media items of a certain type.
+ *
+ * @throws Exception If not enough posts exist.
+ * @param integer $type The post_mime_type of the media item.
+ * @param integer $count The number of images for which to query.
+ * @return array|WP_CLI::error The media IDs, or an error on failure.
+ */
+function amp_media( $type, $count = 3 ) {
+ $query = new \WP_Query(
+ array(
+ 'post_type' => 'attachment',
+ 'post_mime_type' => $type,
+ 'post_status' => 'inherit',
+ 'posts_per_page' => $count,
+ )
+ );
+ if ( $query->post_count < $count ) {
+ throw new Exception(
+ sprintf(
+ 'Please ensure at least %1$s "%2$s" attachments are accessible and run this again. There are currently only %3$s.',
+ $count,
+ $type,
+ $query->found_posts
+ )
+ );
+ }
+ return $query->get_posts();
+}
+
+/**
+ * Gets a menu ID, if it has more than 4 items.
+ *
+ * Iterates through all of the menus.
+ * And returns the first that has 4 items.
+ *
+ * @throws Exception When there is no menu with 4 items.
+ * @return integer|WP_CLI::error $menu_id The menu ID, or an error if no menu has at least 4 items.
+ */
+function amp_menu() {
+ $menus = wp_get_nav_menus();
+ $minimum_count = 4;
+ foreach ( $menus as $menu ) {
+ if ( $menu->count >= $minimum_count ) {
+ return $menu->term_id;
+ }
+ }
+ throw new Exception( 'Please add at least 4 items to a menu.' );
+}
+
+/**
+ * Whether a widget with the same values exists in the sidebar.
+ *
+ * Iterate through all of the widgets in the sidebar.
+ * Find all of the widgets with the same ID base, like 'text.'
+ * Then, find if one of those has the same settings as the new widget settings.
+ *
+ * @param array $widget The settings of the widget.
+ * @param string $sidebar The slug of the sidebar.
+ * @return boolean $in_sidebar Whether the widget is in the sidebar.
+ */
+function amp_widget_already_in_sidebar( $widget, $sidebar ) {
+ $sidebars = wp_get_sidebars_widgets();
+ if ( empty( $sidebars[ $sidebar ] ) ) {
+ return false;
+ }
+
+ $id_base = $widget['widget'];
+ $all_widget_data = get_option( 'widget_' . $id_base, array() );
+
+ foreach ( $sidebars[ $sidebar ] as $possible_widget ) {
+ if ( false !== strpos( $possible_widget, $id_base ) ) {
+ /*
+ * If there aren't any settings for the widget, any instance of it is enough.
+ * For example, a 'Pages' widget.
+ */
+ if ( ! isset( $widget['settings'] ) ) {
+ return true;
+ }
+
+ preg_match( '/\d+/', $possible_widget, $matches );
+ $id = $matches[0];
+ if ( isset( $all_widget_data[ $id ] ) ) {
+ $widget_data = $all_widget_data[ $id ];
+ } else {
+ continue;
+ }
+
+ // Find if all of the settings in $widget['settings'] are present in the widget that's already in the sidebar.
+ if ( array() === array_diff_assoc( array_map( 'serialize', $widget['settings'] ), array_map( 'serialize', $widget_data ) ) ) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+/**
+ * Creates a widget, based on the passed settings.
+ *
+ * @param array $widget The data for the widget.
+ * @return void.
+ */
+function amp_create_widget( $widget ) {
+ if ( ! isset( $widget['widget'] ) ) {
+ return;
+ }
+
+ $id_base = $widget['widget'];
+ $option_key = 'widget_' . $id_base;
+ $widgets = get_option( $option_key, array() );
+ $settings = isset( $widget['settings'] ) ? $widget['settings'] : array();
+
+ if ( ! isset( $settings['title'] ) ) {
+ $title = str_replace( '_', ' ', $id_base );
+ $title = str_replace( '-', ' ', $title );
+ $settings['title'] = sprintf( 'Test %s Widget', ucwords( $title ) );
+ }
+ $widgets[] = $settings;
+ update_option( $option_key, $widgets );
+}
+
+/**
+ * Gets the first registered sidebar.
+ *
+ * @throws Exception When there is no registered sidebar.
+ * @return string|WP_CLI::error The first registered sidebar on success; error otherwise.
+ */
+function amp_get_first_sidebar() {
+ $sidebar = reset( $GLOBALS['wp_registered_sidebars'] );
+ if ( ! isset( $sidebar['id'] ) ) {
+ throw new Exception( 'Please make sure at least one sidebar is registered.' );
+ }
+ return $sidebar['id'];
+}
+
+/**
+ * Gets a widget ID, based on its name.
+ *
+ * Mainly copied from import_theme_starter_content().
+ * Increments the ID, based on the number of existing widgets.
+ *
+ * @see WP_Customize_Manager::import_theme_starter_content().
+ * @param string $id_base The ID base of the widget, like 'text'.
+ * @return string $widget_id The ID of the widget, like 'text-2'.
+ */
+function amp_get_widget_id( $id_base ) {
+ $settings = get_option( "widget_{$id_base}", array() );
+ if ( $settings instanceof ArrayObject || $settings instanceof ArrayIterator ) {
+ $settings = $settings->getArrayCopy();
+ }
+
+ // Get the widget's maximum widget number.
+ $widget_numbers = array_keys( $settings );
+ if ( count( $widget_numbers ) > 0 ) {
+ $widget_numbers[] = 1;
+ $widget_number = call_user_func_array( 'max', $widget_numbers );
+ } else {
+ $widget_number = 1;
+ }
+ return sprintf( '%s-%d', $id_base, $widget_number );
+}
+
+/**
+ * Adds the newly-created widgets to the sidebar.
+ *
+ * @param array $widgets The widget IDs to add to the sidebar.
+ * @param string $sidebar The sidebar to which to add the widget, like sidebar-1.
+ * @return void
+ */
+function amp_add_widgets_to_sidebar( $widgets, $sidebar ) {
+ $sidebars = wp_get_sidebars_widgets();
+ foreach ( $widgets as $widget ) {
+ $sidebars[ $sidebar ][] = $widget;
+ }
+ update_option( 'sidebars_widgets', $sidebars );
+}
+
+// Bootstrap the file.
+if ( defined( 'WP_CLI' ) ) {
+ try {
+ $sidebar = amp_populate_sidebar();
+ WP_CLI::success( sprintf( 'Please visit a page with the sidebar: %s.', esc_html( $sidebar ) ) );
+ } catch ( Exception $e ) {
+ WP_CLI::error( $e->getMessage() );
+ }
+} else {
+ echo 'Must be run in WP-CLI via: wp eval-file bin/add-test-widgets-to-sidebar.php.';
+ exit( 1 );
+}
diff --git a/contributing.md b/contributing.md
index b90ed521857..5d821a7c072 100644
--- a/contributing.md
+++ b/contributing.md
@@ -24,6 +24,15 @@ To run it:
3. run `wp eval-file bin/create-embed-test-post.php`
4. go to the URL that is output in the command line
+## Testing Widgets Support
+
+The following script adds an instance of every default WordPress widget to the first registered sidebar.
+To run it:
+1. `ssh` into an environment like [VVV](https://github.com/Varying-Vagrant-Vagrants/VVV)
+2. `cd` to the root of this plugin
+3. run `wp eval-file bin/add-test-widgets-to-sidebar.php`
+4. There will be a message indicating which sidebar has the widgets. Please visit a page with that sidebar.
+
## PHPUnit Testing
Please run these tests in an environment with WordPress unit tests installed, like [VVV](https://github.com/Varying-Vagrant-Vagrants/VVV).