drevops / behat-steps

🧪 A collection of Behat step definitions for Drupal
GNU General Public License v3.0
18 stars 13 forks source link

Support for CKEditor v5 #124

Closed xurizaemon closed 9 months ago

xurizaemon commented 1 year ago

While testing #122, I noticed that the current When I fill in WYSIWYG "Body" with "Some content" doesn't locate any iframes - and I think this is because CKEditor 5 does not use iframes.

Screenshot of CKEditor 5 showing there are no iframes in the DOM, and version 35.4.0

If that's true, then WysiwygTrait will need updating to support that.

https://github.com/drevops/behat-steps/blob/master/src/WysiwygTrait.php#L17-L88

Would appreciate anyone being able to confirm this!

ericgsmith commented 1 year ago

Had a look at this today on a project.

Can confirm that CKEditor 5 does not use an iframe.

The sibling element also has the class ck instead of cke

The editor element is child of the sibling with class ck-editor__editable and has the attribute contenteditable="true"

My first thought was we can target the element using the xpath $field->getXpath() . "/following-sibling::div[contains(@class, 'ck')]//div[contains(@class, 'ck-editor__editable')]"; and then using that element as the keyboard trigger.

The project I was testing on is using the ChromeDriver not Selenium, so that stopped me going down that path.

In the end I've got it working using a similar approach to https://stackoverflow.com/questions/72445268/how-can-i-use-behat-to-set-the-value-of-a-ckeditor-5-field

    $editorId = $field->getAttribute('data-ckeditor5-id');
    $this->getSession()->executeScript("Drupal.CKEditor5Instances.get(\"$editorId\").setData(\"$value\");");

Keen to get feedback on that approach used in that Stack overflow issue / above.

AlexSkrypnyk commented 10 months ago

This is a code that is working on the consumer project. It will be a replacement for an existing wysiwygFillField().

The old wysiwygFillField() implementation will be removed (BC break). Anyone needing the old implementation can copy/paste the code into their custom FeatureContext.


  /**
   * Set value for WYSIWYG field.
   *
   * If used with Selenium driver, it will try to find associated WYSIWYG and
   * fill it in. If used with webdriver - it will fill in the field as normal.
   *
   * @When /^(?:|I )fill in WYSIWYG "(?P<field>(?:[^"]|\\")*)" with "(?P<value>(?:[^"]|\\")*)"$/
   */
  public function wysiwygFillField($field, $value) {
    $field = $this->wysiwygFixStepArgument($field);
    $value = $this->wysiwygFixStepArgument($value);

    $page = $this->getSession()->getPage();
    $element = $page->findField($field);

    if ($element === NULL) {
      throw new ElementNotFoundException($this->getSession()->getDriver(), 'form field', 'id|name|label|value|placeholder', $field);
    }

    $driver = $this->getSession()->getDriver();
    try {
      $driver->evaluateScript('true');
    }
    catch (UnsupportedDriverActionException $exception) {
      // For non-JS drivers process field in a standard way.
      $element->setValue($value);

      return;
    }

    // Find parent element.
    $parent_element_xpath = $element->getXpath() . "/ancestor::div[contains(@class, 'form-item--')][1]";
    $parent_elements = $driver->find($parent_element_xpath);
    if (empty($parent_elements[0])) {
      throw new ElementNotFoundException($this->getSession()->getDriver(), 'WYSIWYG form field', 'id|name|label|value|placeholder', $field);
    }

    $parent_field_classes = $parent_elements[0]->getAttribute('class');
    $parent_field_classes = explode(' ', $parent_field_classes);

    // Find exact class name starting with 'form-item--'.
    $parent_field_classes = array_filter($parent_field_classes, function ($class) {
      return str_starts_with($class, 'form-item--');
    });
    $parent_field_class = reset($parent_field_classes);

    $this->getSession()
      ->executeScript(
        "
        const domEditableElement = document.querySelector(\"div.$parent_field_class .ck-editor__editable\");
        if (domEditableElement.ckeditorInstance) {
          const editorInstance = domEditableElement.ckeditorInstance;
          if (editorInstance) {
            editorInstance.setData(\"$value\");
          } else {
            throw new Exception('Could not get the editor instance');
          }
        } else {
          throw new Exception('Could not find the element');
        }
        ");
  }
AlexSkrypnyk commented 9 months ago

Implemented in #168