Verified Commit 0ad0f52f authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3315319 by Wim Leers, Spokje, alexpott, lauriii: Random fails in...

Issue #3315319 by Wim Leers, Spokje, alexpott, lauriii: Random fails in Drupal\Tests\ckeditor5\FunctionalJavascript\AdminUiTest and Drupal\Tests\ckeditor5\FunctionalJavascript\CKEditor5Test

(cherry picked from commit c0d535a9)
parent 204055b9
Loading
Loading
Loading
Loading
+35 −22
Original line number Diff line number Diff line
@@ -31,36 +31,23 @@ public function testSettingsOnlyFireAjaxWithCkeditor5() {
    $this->addNewTextFormat($page, $assert_session, 'unicorn');

    $this->drupalGet('admin/config/content/formats/manage/ckeditor5');
    $number_ajax_instances_before = $this->getSession()->evaluateScript('Drupal.ajax.instances.length');

    // Enable media embed to trigger an AJAX rebuild.
    $this->assertTrue($page->hasUncheckedField('filters[media_embed][status]'));
    $this->assertSame(0, $this->getAjaxResponseCount());
    $page->checkField('filters[media_embed][status]');
    $this->assertNotEmpty($assert_session->waitForElement('css', '.ajax-progress-throbber'));
    $assert_session->assertWaitOnAjaxRequest();
    $assert_session->responseContains('Media types selectable in the Media Library');
    $assert_session->assertWaitOnAjaxRequest();
    $number_ajax_instances_after = $this->getSession()->evaluateScript('Drupal.ajax.instances.length');

    // After the rebuild, there should be more AJAX instances.
    $this->assertGreaterThan($number_ajax_instances_before, $number_ajax_instances_after);
    $this->assertSame(1, $this->getAjaxResponseCount());

    // Perform the same steps as above with CKEditor, and confirm AJAX callbacks
    // are not triggered on settings changes.
    $this->drupalGet('admin/config/content/formats/manage/unicorn');
    $number_ajax_instances_before = $this->getSession()->evaluateScript('Drupal.ajax.instances.length');

    // Enable media embed to confirm a format not using CKEditor 5 will not
    // trigger an AJAX rebuild.
    $this->assertTrue($page->hasUncheckedField('filters[media_embed][status]'));
    $page->checkField('filters[media_embed][status]');
    $this->assertEmpty($assert_session->waitForElement('css', '.ajax-progress-throbber'));
    $assert_session->assertWaitOnAjaxRequest();
    $assert_session->responseContains('Media types selectable in the Media Library');
    $assert_session->assertWaitOnAjaxRequest();

    $number_ajax_instances_after = $this->getSession()->evaluateScript('Drupal.ajax.instances.length');
    $this->assertSame($number_ajax_instances_before, $number_ajax_instances_after);
    $this->assertSame(0, $this->getAjaxResponseCount());

    // Confirm that AJAX updates happen when attempting to switch to CKEditor 5,
    // even if prevented from doing so by validation.
@@ -72,16 +59,13 @@ public function testSettingsOnlyFireAjaxWithCkeditor5() {
    // Enable a filter that is incompatible with CKEditor 5, so validation is
    // triggered when attempting to switch.
    $incompatible_filter_name = 'filters[filter_incompatible][status]';
    $number_ajax_instances_before = $this->getSession()->evaluateScript('Drupal.ajax.instances.length');
    $this->assertTrue($page->hasUncheckedField($incompatible_filter_name));
    $page->checkField($incompatible_filter_name);
    $this->assertEmpty($assert_session->waitForElement('css', '.ajax-progress-throbber'));
    $assert_session->assertWaitOnAjaxRequest();
    $number_ajax_instances_after = $this->getSession()->evaluateScript('Drupal.ajax.instances.length');
    $this->assertSame($number_ajax_instances_before, $number_ajax_instances_after);
    $this->assertSame(0, $this->getAjaxResponseCount());

    $page->selectFieldOption('editor[editor]', 'ckeditor5');
    $assert_session->assertWaitOnAjaxRequest();
    $this->assertSame(1, $this->getAjaxResponseCount());

    $filter_warning = 'CKEditor 5 only works with HTML-based text formats. The "A TYPE_MARKUP_LANGUAGE filter incompatible with CKEditor 5" (filter_incompatible) filter implies this text format is not HTML anymore.';

@@ -94,11 +78,40 @@ public function testSettingsOnlyFireAjaxWithCkeditor5() {
    // been corrected.
    $this->assertTrue($page->hasCheckedField($incompatible_filter_name));
    $page->uncheckField($incompatible_filter_name);
    $this->assertNotEmpty($assert_session->waitForElement('css', '.ajax-progress-throbber'));
    $assert_session->assertWaitOnAjaxRequest();
    $this->assertSame(2, $this->getAjaxResponseCount());
    $assert_session->pageTextNotContains($filter_warning);
  }

  /**
   * Gets the Drupal AJAX response count observed on this page.
   *
   * @return int
   *   The number of completed XHR requests observed since the page was loaded.
   */
  protected function getAjaxResponseCount(): int {
    // Half a second should suffice for any of the test's DOM interactions to
    // have triggered an AJAX request, if any.
    try {
      $this->assertSession()->assertWaitOnAjaxRequest(500);
    }
    catch (\RuntimeException $e) {
      throw new \LogicException('An AJAX request was still being processed, this suggests a assertWaitOnAjaxRequest() call is missing.');
    }

    // Now that there definitely is no more AJAX request in progress, count the
    // number of AJAX responses.
    $javascript = <<<JS
(function(){
  return window.performance
    .getEntries()
    .filter(entry => entry.initiatorType === 'xmlhttprequest' && entry.name.indexOf('_wrapper_format=drupal_ajax') !== -1)
    .length
})()
JS;
    return $this->getSession()->evaluateScript($javascript);
  }

  /**
   * CKEditor5's filter UI modifications should not break it for other editors.
   */
+2 −2
Original line number Diff line number Diff line
@@ -376,9 +376,10 @@ public function testMediaElementAllowedTags() {

    // Enable media embed.
    $this->assertTrue($page->hasUncheckedField('filters[media_embed][status]'));
    $this->assertNull($assert_session->waitForElementVisible('css', '[data-drupal-selector=edit-filters-media-embed-settings]', 0));
    $page->checkField('filters[media_embed][status]');
    $assert_session->assertWaitOnAjaxRequest();
    $assert_session->responseContains('Media types selectable in the Media Library');
    $this->assertNotNull($assert_session->waitForElementVisible('css', '[data-drupal-selector=edit-filters-media-embed-settings]', 0));

    $page->clickLink('Embed media');
    $page->checkField('filters[media_embed][settings][allowed_view_modes][view_mode_1]');
@@ -387,7 +388,6 @@ public function testMediaElementAllowedTags() {

    $allowed_with_media = $this->allowedElements . ' <drupal-media data-entity-type data-entity-uuid alt data-view-mode>';
    $allowed_with_media_without_view_mode = $this->allowedElements . ' <drupal-media data-entity-type data-entity-uuid alt>';
    $assert_session->responseContains('Media types selectable in the Media Library');
    $page->clickLink('Media');
    $assert_session->waitForText('Allow the user to override the default view mode');
    $this->assertTrue($page->hasUncheckedField('editor[settings][plugins][media_media][allow_view_mode_override]'));
+167 −49
Original line number Diff line number Diff line
@@ -302,6 +302,67 @@ public function languageOfPartsPluginTestHelper($page, $assert_session, $predefi
    $this->assertSame(array_values($predefined_languages), $languages);
  }

  /**
   * Gets the titles of the vertical tabs in the given container.
   *
   * @param string $container_selector
   *   The container in which to look for vertical tabs.
   * @param bool $visible_only
   *   (optional) Whether to restrict to only the visible vertical tabs. TRUE by
   *   default.
   *
   * @return string[]
   *   The titles of all vertical tabs menu items, restricted to only
   *   visible ones by default.
   *
   * @throws \LogicException
   */
  private function getVerticalTabs(string $container_selector, bool $visible_only = TRUE): array {
    $page = $this->getSession()->getPage();

    // Ensure the container exists.
    $container = $page->find('css', $container_selector);
    if ($container === NULL) {
      throw new \LogicException('The given container should exist.');
    }

    // Make sure that the container selector contains exactly one Vertical Tabs
    // UI component.
    $vertical_tabs = $container->findAll('css', '.vertical-tabs');
    if (count($vertical_tabs) != 1) {
      throw new \LogicException('The given container should contain exactly one Vertical Tabs component.');
    }

    $vertical_tabs = $container->findAll('css', '.vertical-tabs__menu-item');
    $vertical_tabs_titles = [];
    foreach ($vertical_tabs as $vertical_tab) {
      if ($visible_only && !$vertical_tab->isVisible()) {
        continue;
      }
      $title = $vertical_tab->find('css', '.vertical-tabs__menu-item-title')->getHtml();
      // When retrieving visible vertical tabs, mark the selected one.
      if ($visible_only && $vertical_tab->hasClass('is-selected')) {
        $title = "➡️$title";
      }
      $vertical_tabs_titles[] = $title;
    }
    return $vertical_tabs_titles;
  }

  /**
   * Enables a disabled CKEditor 5 toolbar item.
   *
   * @param string $toolbar_item_id
   *   The toolbar item to enable.
   */
  protected function enableDisabledToolbarItem(string $toolbar_item_id): void {
    $assert_session = $this->assertSession();
    $assert_session->elementExists('css', ".ckeditor5-toolbar-disabled .ckeditor5-toolbar-item-$toolbar_item_id");
    $this->triggerKeyUp(".ckeditor5-toolbar-item-$toolbar_item_id", 'ArrowDown');
    $assert_session->elementNotExists('css', ".ckeditor5-toolbar-disabled .ckeditor5-toolbar-item-$toolbar_item_id");
    $assert_session->elementExists('css', ".ckeditor5-toolbar-active .ckeditor5-toolbar-item-$toolbar_item_id");
  }

  /**
   * Confirms active tab status is intact after AJAX refresh.
   */
@@ -312,77 +373,134 @@ public function testActiveTabsMaintained() {
    $this->createNewTextFormat($page, $assert_session);
    $assert_session->assertWaitOnAjaxRequest();

    // Ensure the HTML filter tab is visible.
    $this->assertNotEmpty($assert_session->waitForElementVisible('css', 'a[href^="#edit-filters-filter-html-settings"]'));
    // Initial vertical tabs: 3 for filters, 1 for CKE5 plugins.
    $this->assertSame([
      'Limit allowed HTML tags and correct faulty HTML',
      'Convert URLs into links',
      'Embed media',
    ], $this->getVerticalTabs('#filter-settings-wrapper', FALSE));
    $this->assertSame([
      'Headings',
    ], $this->getVerticalTabs('#plugin-settings-wrapper', FALSE));

    // Initial visible vertical tabs: 1 for filters, 1 for CKE5 plugins.
    $this->assertSame([
      '➡️Limit allowed HTML tags and correct faulty HTML',
    ], $this->getVerticalTabs('#filter-settings-wrapper'));
    $this->assertSame([
      '➡️Headings',
    ], $this->getVerticalTabs('#plugin-settings-wrapper'));

    // Enable media embed to make a second filter config tab visible.
    // Enable media embed to make a second filter config vertical tab visible.
    $this->assertTrue($page->hasUncheckedField('filters[media_embed][status]'));
    $this->assertNull($assert_session->waitForElementVisible('css', '[data-drupal-selector=edit-filters-media-embed-settings]', 0));
    $page->checkField('filters[media_embed][status]');
    $this->assertNotNull($assert_session->waitForElementVisible('css', '[data-drupal-selector=edit-filters-media-embed-settings]', 0));
    $assert_session->assertWaitOnAjaxRequest();
    $assert_session->responseContains('Media types selectable in the Media Library');
    $assert_session->assertWaitOnAjaxRequest();
    // Filter plugins vertical tabs behavior: the filter plugin settings
    // vertical tab with the heaviest filter weight is active by default.
    // Hence enabling the media_embed filter (weight 100) results in its
    // vertical tab being activated (filter_html's weight is -10).
    // @see core/modules/filter/filter.admin.js
    $this->assertSame([
      'Limit allowed HTML tags and correct faulty HTML',
      '➡️Embed media',
    ], $this->getVerticalTabs('#filter-settings-wrapper'));
    $this->assertSame([
      '➡️Headings',
      'Media',
    ], $this->getVerticalTabs('#plugin-settings-wrapper'));

    // Enable upload image to add one plugin config form.
    $this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-item-drupalInsertImage'));
    $this->triggerKeyUp('.ckeditor5-toolbar-item-drupalInsertImage  ', 'ArrowDown');
    // cSpell:disable-next-line
    $this->assertNotEmpty($assert_session->waitForElement('css', 'a[href^="#edit-editor-settings-plugins-ckeditor5-image"]'));
    $this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-active .ckeditor5-toolbar-item-drupalInsertImage'));
    // Enable upload image to add a third (and fourth) CKE5 plugin vertical tab.
    $this->enableDisabledToolbarItem('drupalInsertImage');
    $assert_session->assertWaitOnAjaxRequest();
    // The active CKE5 plugin settings vertical tab is unchanged.
    $this->assertSame([
      '➡️Headings',
      'Image',
      'Image resize',
      'Media',
    ], $this->getVerticalTabs('#plugin-settings-wrapper'));
    // The active filter plugin settings vertical tab is unchanged.
    $this->assertSame([
      'Limit allowed HTML tags and correct faulty HTML',
      '➡️Embed media',
    ], $this->getVerticalTabs('#filter-settings-wrapper'));

    // Open the CKE5 "Image" plugin settings vertical tab, interact with the
    // subform and observe that the AJAX requests those interactions trigger do
    // not change the active vertical tabs.
    $page->clickLink('Image');
    $assert_session->waitForText('Enable image uploads');
    $this->assertSame([
      'Headings',
      '➡️Image',
      'Image resize',
      'Media',
    ], $this->getVerticalTabs('#plugin-settings-wrapper'));
    $this->assertTrue($page->hasUncheckedField('editor[settings][plugins][ckeditor5_image][status]'));
    $page->checkField('editor[settings][plugins][ckeditor5_image][status]');
    $assert_session->assertWaitOnAjaxRequest();

    // Enable Heading to add a second plugin config form.
    $this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-button-heading'));
    $this->triggerKeyUp('.ckeditor5-toolbar-button-heading', 'ArrowDown');
    $this->assertNotEmpty($assert_session->waitForElement('css', 'a[href^="#edit-editor-settings-plugins-ckeditor5-heading"]'));
    $this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-active .ckeditor5-toolbar-button-heading'));
    $assert_session->assertWaitOnAjaxRequest();
    $this->assertSame([
      'Headings',
      '➡️Image',
      'Image resize',
      'Media',
    ], $this->getVerticalTabs('#plugin-settings-wrapper'));
    $this->assertSame([
      'Limit allowed HTML tags and correct faulty HTML',
      '➡️Embed media',
    ], $this->getVerticalTabs('#filter-settings-wrapper'));

    $page->pressButton('Save configuration');
    $assert_session->pageTextContains('Added text format ckeditor5');

    // Leave and return to the config form, both sets of tabs should then have
    // the first tab active by default.
    // Leave and return to the config form, wait for initialized Vertical Tabs.
    $this->drupalGet('admin/config/content/formats/');
    $this->drupalGet('admin/config/content/formats/manage/ckeditor5');

    $assert_session->waitForElement('css', '.vertical-tabs__menu-item.is-selected');

    $plugin_settings_vertical_tabs = $page->findAll('css', '#plugin-settings-wrapper .vertical-tabs__menu-item');
    $filter_settings = $page->find('xpath', '//*[contains(@class, "js-form-type-vertical-tabs")]/label[contains(text(), "Filter settings")]/..');
    $filter_settings_vertical_tabs = $filter_settings->findAll('css', '.vertical-tabs__menu-item');

    $this->assertTrue($plugin_settings_vertical_tabs[0]->hasClass('is-selected'), "Expected plugin tab 1 selected on initial build");
    $this->assertFalse($plugin_settings_vertical_tabs[1]->hasClass('is-selected'), "Expected plugin tab 2 not selected on initial build");

    $this->assertFalse($filter_settings_vertical_tabs[0]->hasClass('is-selected'), "Expected filter tab 1 not selected on initial build");
    $this->assertTrue($filter_settings_vertical_tabs[2]->hasClass('is-selected'), "Expected (visible) filter tab 2 selected on initial build");

    $plugin_settings_vertical_tabs[1]->click();
    $filter_settings_vertical_tabs[0]->click();
    $assert_session->assertWaitOnAjaxRequest();

    $this->assertFalse($plugin_settings_vertical_tabs[0]->hasClass('is-selected'), "Expected plugin tab 1 deselected after click");
    $this->assertTrue($plugin_settings_vertical_tabs[1]->hasClass('is-selected'), "Expected plugin tab 2 selected after click");

    $this->assertTrue($filter_settings_vertical_tabs[0]->hasClass('is-selected'), "Expected filter tab 1 selected after click");
    $this->assertFalse($filter_settings_vertical_tabs[2]->hasClass('is-selected'), "Expected (visible) filter tab 2 deselected after click");
    // The first CKE5 plugin settings vertical tab is active by default.
    $this->assertSame([
      '➡️Headings',
      'Image',
      'Image resize',
      'Media',
    ], $this->getVerticalTabs('#plugin-settings-wrapper'));
    // Filter plugins vertical tabs behavior: the filter plugin settings
    // vertical tab with the heaviest filter weight is active by default.
    // Hence enabling the media_embed filter (weight 100) results in its
    // vertical tab being activated (filter_html's weight is -10).
    // @see core/modules/filter/filter.admin.js
    $this->assertSame([
      'Limit allowed HTML tags and correct faulty HTML',
      '➡️Embed media',
    ], $this->getVerticalTabs('#filter-settings-wrapper'));

    // Add a plugin just to trigger AJAX refresh.
    $this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-item-blockQuote'));
    $this->triggerKeyUp('.ckeditor5-toolbar-item-blockQuote', 'ArrowDown');
    // Click the 3rd CKE5 plugin vertical tab.
    $page->clickLink($this->getVerticalTabs('#plugin-settings-wrapper')[2]);
    $this->assertSame([
      'Headings',
      'Image',
      '➡️Image resize',
      'Media',
    ], $this->getVerticalTabs('#plugin-settings-wrapper'));

    // Add another CKEditor 5 toolbar item just to trigger an AJAX refresh.
    $this->enableDisabledToolbarItem('blockQuote');
    $assert_session->assertWaitOnAjaxRequest();

    $this->assertFalse($plugin_settings_vertical_tabs[0]->hasClass('is-selected'), "Expected plugin tab 1 deselected after AJAX refresh");
    $this->assertTrue($plugin_settings_vertical_tabs[1]->hasClass('is-selected'), "Expected plugin tab 2 selected after AJAX refresh");

    $this->assertTrue($filter_settings_vertical_tabs[0]->hasClass('is-selected'), "Expected filter tab 1 selected after AJAX refresh");
    $this->assertFalse($filter_settings_vertical_tabs[1]->hasClass('is-selected'), "Expected filter tab 2 deselected after AJAX refresh");
    // The active CKE5 plugin settings vertical tab is unchanged.
    $this->assertSame([
      'Headings',
      'Image',
      '➡️Image resize',
      'Media',
    ], $this->getVerticalTabs('#plugin-settings-wrapper'));
    // The active filter plugin settings vertical tab is unchanged.
    $this->assertSame([
      'Limit allowed HTML tags and correct faulty HTML',
      '➡️Embed media',
    ], $this->getVerticalTabs('#filter-settings-wrapper'));
  }

  /**