Unverified Commit d2d6fd61 authored by Lauri Timmanee's avatar Lauri Timmanee
Browse files

Issue #3249592 by hooroomoo, vlyalko, Wim Leers, lauriii: [drupalImage] <img...

Issue #3249592 by hooroomoo, vlyalko, Wim Leers, lauriii: [drupalImage] <img width> upcast assumes HTML5: px unit, but HTML4 allowed % unit

(cherry picked from commit d3e6e774)
parent 52911813
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.CKEditor5=e():(t.CKEditor5=t.CKEditor5||{},t.CKEditor5.drupalImage=e())}(self,(function(){return function(){var t={"ckeditor5/src/core.js":function(t,e,i){t.exports=i("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/upload.js":function(t,e,i){t.exports=i("dll-reference CKEditor5.dll")("./src/upload.js")},"ckeditor5/src/utils.js":function(t,e,i){t.exports=i("dll-reference CKEditor5.dll")("./src/utils.js")},"dll-reference CKEditor5.dll":function(t){"use strict";t.exports=CKEditor5.dll}},e={};function i(r){var n=e[r];if(void 0!==n)return n.exports;var a=e[r]={exports:{}};return t[r](a,a.exports,i),a.exports}i.d=function(t,e){for(var r in e)i.o(e,r)&&!i.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:e[r]})},i.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)};var r={};return function(){"use strict";i.d(r,{default:function(){return p}});var t=i("ckeditor5/src/core.js");function e(t){return t.createEmptyElement("img")}function n(){function t(t,e,i){if(!i.consumable.consume(e.item,t.name))return;const r=i.mapper.toViewElement(e.item),n=i.writer,a=n.createContainerElement("a",{href:e.attributeNewValue});n.insert(n.createPositionBefore(r),a),n.move(n.createRangeOn(r),n.createPositionAt(a,0)),i.consumable.consume(e.item,"attribute:htmlLinkAttributes:imageBlock")&&function(t,e,i){if(e.attributes)for(const[r,n]of Object.entries(e.attributes))t.setAttribute(r,n,i);e.styles&&t.setStyle(e.styles,i),e.classes&&t.addClass(e.classes,i)}(i.writer,e.item.getAttribute("htmlLinkAttributes"),a)}return e=>{e.on("attribute:linkHref:imageBlock",t,{priority:"high"})}}class a extends t.Plugin{static get pluginName(){return"DrupalImageEditing"}init(){const{editor:t}=this,{conversion:i}=t,{schema:r}=t.model;r.isRegistered("imageInline")&&r.extend("imageInline",{allowAttributes:["dataEntityUuid","dataEntityType","width","height"]}),r.isRegistered("imageBlock")&&r.extend("imageBlock",{allowAttributes:["dataEntityUuid","dataEntityType","width","height"]}),i.for("upcast").add(function(t){function e(e,i,r){const{viewItem:n}=i,{writer:a,consumable:o,safeInsert:s,updateConversionResult:u,schema:l}=r,d=[];let c;if(o.test(n,{name:!0,attributes:"src"})){if(c=l.checkChild(i.modelCursor,"imageInline")?a.createElement("imageInline",{src:n.getAttribute("src")}):a.createElement("imageBlock",{src:n.getAttribute("src")}),t.plugins.has("ImageStyleEditing")&&o.test(n,{name:!0,attributes:"data-align"})){const t={left:"alignBlockLeft",center:"alignCenter",right:"alignBlockRight"},e={left:"alignLeft",right:"alignRight"},i=n.getAttribute("data-align"),r=c.is("element","imageBlock")?t[i]:e[i];a.setAttribute("imageStyle",r,c),d.push("data-align")}if(c.is("element","imageBlock")&&o.test(n,{name:!0,attributes:"data-caption"})){const e=a.createElement("caption"),i=t.data.processor.toView(n.getAttribute("data-caption")),o=a.createDocumentFragment();r.consumable.constructor.createFrom(i,r.consumable),r.convertChildren(i,o);for(const t of Array.from(o.getChildren()))a.append(t,e);a.append(e,c),d.push("data-caption")}o.test(n,{name:!0,attributes:"data-entity-uuid"})&&(a.setAttribute("dataEntityUuid",n.getAttribute("data-entity-uuid"),c),d.push("data-entity-uuid")),o.test(n,{name:!0,attributes:"data-entity-type"})&&(a.setAttribute("dataEntityType",n.getAttribute("data-entity-type"),c),d.push("data-entity-type")),s(c,i.modelCursor)&&(o.consume(n,{name:!0,attributes:d}),u(c,i))}}return t=>{t.on("element:img",e,{priority:"high"})}}(t)).attributeToAttribute({view:{name:"img",key:"width"},model:{key:"width",value:t=>`${t.getAttribute("width")}px`}}).attributeToAttribute({view:{name:"img",key:"height"},model:{key:"height",value:t=>`${t.getAttribute("height")}px`}}),i.for("downcast").add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("data-entity-uuid",e.attributeNewValue,s||o)}return e=>{e.on("attribute:dataEntityUuid",t)}}()).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("data-entity-type",e.attributeNewValue,s||o)}return e=>{e.on("attribute:dataEntityType",t)}}()),i.for("dataDowncast").add(function(t){return e=>{e.on("insert:caption",((e,i,r)=>{const{consumable:n,writer:a,mapper:o}=r;if(!n.consume(i.item,"insert"))return;const s=t.model.createRangeIn(i.item),u=a.createDocumentFragment();o.bindElements(i.item,u);for(const{item:e}of Array.from(s)){const i={item:e,range:t.model.createRangeOn(e)},n=`insert:${e.name||"$text"}`;t.data.downcastDispatcher.fire(n,i,r);for(const n of e.getAttributeKeys())Object.assign(i,{attributeKey:n,attributeOldValue:null,attributeNewValue:i.item.getAttribute(n)}),t.data.downcastDispatcher.fire(`attribute:${n}`,i,r)}for(const t of a.createRangeIn(u).getItems())o.unbindViewElement(t);o.unbindViewElement(u);const l=t.data.processor.toData(u);if(l){const t=o.toViewElement(i.item.parent);a.setAttribute("data-caption",l,t)}}),{priority:"high"})}}(t)).elementToElement({model:"imageBlock",view:(t,{writer:i})=>e(i),converterPriority:"high"}).elementToElement({model:"imageInline",view:(t,{writer:i})=>e(i),converterPriority:"high"}).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i,o={alignLeft:"left",alignRight:"right",alignCenter:"center",alignBlockRight:"right",alignBlockLeft:"left"};if(!o[e.attributeNewValue]||!n.consume(r,t.name))return;const s=i.mapper.toViewElement(r),u=Array.from(s.getChildren()).find((t=>"img"===t.name));a.setAttribute("data-align",o[e.attributeNewValue],u||s)}return e=>{e.on("attribute:imageStyle",t,{priority:"high"})}}()).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("width",e.attributeNewValue.replace("px",""),s||o)}return e=>{e.on("attribute:width:imageInline",t,{priority:"high"}),e.on("attribute:width:imageBlock",t,{priority:"high"})}}()).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("height",e.attributeNewValue.replace("px",""),s||o)}return e=>{e.on("attribute:height:imageInline",t,{priority:"high"}),e.on("attribute:height:imageBlock",t,{priority:"high"})}}()).add(n())}}class o extends t.Plugin{static get requires(){return[a]}static get pluginName(){return"DrupalImage"}}var s=o;class u extends t.Plugin{init(){const{editor:t}=this;t.plugins.get("ImageUploadEditing").on("uploadComplete",((e,{data:i,imageElement:r})=>{t.model.change((t=>{t.setAttribute("dataEntityUuid",i.dataEntityUuid,r),t.setAttribute("dataEntityType",i.dataEntityType,r)}))}))}static get pluginName(){return"DrupalImageUploadEditing"}}var l=i("ckeditor5/src/upload.js"),d=i("ckeditor5/src/utils.js");class c{constructor(t,e){this.loader=t,this.options=e}upload(){return this.loader.file.then((t=>new Promise(((e,i)=>{this._initRequest(),this._initListeners(e,i,t),this._sendRequest(t)}))))}abort(){this.xhr&&this.xhr.abort()}_initRequest(){this.xhr=new XMLHttpRequest,this.xhr.open("POST",this.options.uploadUrl,!0),this.xhr.responseType="json"}_initListeners(t,e,i){const r=this.xhr,n=this.loader,a=`Couldn't upload file: ${i.name}.`;r.addEventListener("error",(()=>e(a))),r.addEventListener("abort",(()=>e())),r.addEventListener("load",(()=>{const i=r.response;if(!i||i.error)return e(i&&i.error&&i.error.message?i.error.message:a);t({urls:{default:i.url},dataEntityUuid:i.uuid?i.uuid:"",dataEntityType:i.entity_type?i.entity_type:""})})),r.upload&&r.upload.addEventListener("progress",(t=>{t.lengthComputable&&(n.uploadTotal=t.total,n.uploaded=t.loaded)}))}_sendRequest(t){const e=this.options.headers||{},i=this.options.withCredentials||!1;Object.keys(e).forEach((t=>{this.xhr.setRequestHeader(t,e[t])})),this.xhr.withCredentials=i;const r=new FormData;r.append("upload",t),this.xhr.send(r)}}class m extends t.Plugin{static get requires(){return[l.FileRepository]}static get pluginName(){return"DrupalFileRepository"}init(){const t=this.editor.config.get("drupalImageUpload");t&&(t.uploadUrl?this.editor.plugins.get(l.FileRepository).createUploadAdapter=e=>new c(e,t):(0,d.logWarning)("simple-upload-adapter-missing-uploadurl"))}}class g extends t.Plugin{static get requires(){return[m,u]}static get pluginName(){return"DrupalImageUpload"}}var p={DrupalImage:s,DrupalImageUpload:g}}(),r=r.default}()}));
 No newline at end of file
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.CKEditor5=e():(t.CKEditor5=t.CKEditor5||{},t.CKEditor5.drupalImage=e())}(self,(function(){return function(){var t={"ckeditor5/src/core.js":function(t,e,i){t.exports=i("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/upload.js":function(t,e,i){t.exports=i("dll-reference CKEditor5.dll")("./src/upload.js")},"ckeditor5/src/utils.js":function(t,e,i){t.exports=i("dll-reference CKEditor5.dll")("./src/utils.js")},"dll-reference CKEditor5.dll":function(t){"use strict";t.exports=CKEditor5.dll}},e={};function i(r){var n=e[r];if(void 0!==n)return n.exports;var a=e[r]={exports:{}};return t[r](a,a.exports,i),a.exports}i.d=function(t,e){for(var r in e)i.o(e,r)&&!i.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:e[r]})},i.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)};var r={};return function(){"use strict";i.d(r,{default:function(){return h}});var t=i("ckeditor5/src/core.js");function e(t){return t.createEmptyElement("img")}function n(t){const e=parseFloat(t);return!Number.isNaN(e)&&t===String(e)}function a(){function t(t,e,i){if(!i.consumable.consume(e.item,t.name))return;const r=i.mapper.toViewElement(e.item),n=i.writer,a=n.createContainerElement("a",{href:e.attributeNewValue});n.insert(n.createPositionBefore(r),a),n.move(n.createRangeOn(r),n.createPositionAt(a,0)),i.consumable.consume(e.item,"attribute:htmlLinkAttributes:imageBlock")&&function(t,e,i){if(e.attributes)for(const[r,n]of Object.entries(e.attributes))t.setAttribute(r,n,i);e.styles&&t.setStyle(e.styles,i),e.classes&&t.addClass(e.classes,i)}(i.writer,e.item.getAttribute("htmlLinkAttributes"),a)}return e=>{e.on("attribute:linkHref:imageBlock",t,{priority:"high"})}}class o extends t.Plugin{static get pluginName(){return"DrupalImageEditing"}init(){const{editor:t}=this,{conversion:i}=t,{schema:r}=t.model;r.isRegistered("imageInline")&&r.extend("imageInline",{allowAttributes:["dataEntityUuid","dataEntityType","width","height"]}),r.isRegistered("imageBlock")&&r.extend("imageBlock",{allowAttributes:["dataEntityUuid","dataEntityType","width","height"]}),i.for("upcast").add(function(t){function e(e,i,r){const{viewItem:n}=i,{writer:a,consumable:o,safeInsert:s,updateConversionResult:u,schema:l}=r,d=[];let c;if(o.test(n,{name:!0,attributes:"src"})){if(c=l.checkChild(i.modelCursor,"imageInline")?a.createElement("imageInline",{src:n.getAttribute("src")}):a.createElement("imageBlock",{src:n.getAttribute("src")}),t.plugins.has("ImageStyleEditing")&&o.test(n,{name:!0,attributes:"data-align"})){const t={left:"alignBlockLeft",center:"alignCenter",right:"alignBlockRight"},e={left:"alignLeft",right:"alignRight"},i=n.getAttribute("data-align"),r=c.is("element","imageBlock")?t[i]:e[i];a.setAttribute("imageStyle",r,c),d.push("data-align")}if(c.is("element","imageBlock")&&o.test(n,{name:!0,attributes:"data-caption"})){const e=a.createElement("caption"),i=t.data.processor.toView(n.getAttribute("data-caption")),o=a.createDocumentFragment();r.consumable.constructor.createFrom(i,r.consumable),r.convertChildren(i,o);for(const t of Array.from(o.getChildren()))a.append(t,e);a.append(e,c),d.push("data-caption")}o.test(n,{name:!0,attributes:"data-entity-uuid"})&&(a.setAttribute("dataEntityUuid",n.getAttribute("data-entity-uuid"),c),d.push("data-entity-uuid")),o.test(n,{name:!0,attributes:"data-entity-type"})&&(a.setAttribute("dataEntityType",n.getAttribute("data-entity-type"),c),d.push("data-entity-type")),s(c,i.modelCursor)&&(o.consume(n,{name:!0,attributes:d}),u(c,i))}}return t=>{t.on("element:img",e,{priority:"high"})}}(t)).attributeToAttribute({view:{name:"img",key:"width"},model:{key:"width",value:t=>n(t.getAttribute("width"))?`${t.getAttribute("width")}px`:`${t.getAttribute("width")}`}}).attributeToAttribute({view:{name:"img",key:"height"},model:{key:"height",value:t=>n(t.getAttribute("height"))?`${t.getAttribute("height")}px`:`${t.getAttribute("height")}`}}),i.for("downcast").add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("data-entity-uuid",e.attributeNewValue,s||o)}return e=>{e.on("attribute:dataEntityUuid",t)}}()).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("data-entity-type",e.attributeNewValue,s||o)}return e=>{e.on("attribute:dataEntityType",t)}}()),i.for("dataDowncast").add(function(t){return e=>{e.on("insert:caption",((e,i,r)=>{const{consumable:n,writer:a,mapper:o}=r;if(!n.consume(i.item,"insert"))return;const s=t.model.createRangeIn(i.item),u=a.createDocumentFragment();o.bindElements(i.item,u);for(const{item:e}of Array.from(s)){const i={item:e,range:t.model.createRangeOn(e)},n=`insert:${e.name||"$text"}`;t.data.downcastDispatcher.fire(n,i,r);for(const n of e.getAttributeKeys())Object.assign(i,{attributeKey:n,attributeOldValue:null,attributeNewValue:i.item.getAttribute(n)}),t.data.downcastDispatcher.fire(`attribute:${n}`,i,r)}for(const t of a.createRangeIn(u).getItems())o.unbindViewElement(t);o.unbindViewElement(u);const l=t.data.processor.toData(u);if(l){const t=o.toViewElement(i.item.parent);a.setAttribute("data-caption",l,t)}}),{priority:"high"})}}(t)).elementToElement({model:"imageBlock",view:(t,{writer:i})=>e(i),converterPriority:"high"}).elementToElement({model:"imageInline",view:(t,{writer:i})=>e(i),converterPriority:"high"}).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i,o={alignLeft:"left",alignRight:"right",alignCenter:"center",alignBlockRight:"right",alignBlockLeft:"left"};if(!o[e.attributeNewValue]||!n.consume(r,t.name))return;const s=i.mapper.toViewElement(r),u=Array.from(s.getChildren()).find((t=>"img"===t.name));a.setAttribute("data-align",o[e.attributeNewValue],u||s)}return e=>{e.on("attribute:imageStyle",t,{priority:"high"})}}()).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("width",e.attributeNewValue.replace("px",""),s||o)}return e=>{e.on("attribute:width:imageInline",t,{priority:"high"}),e.on("attribute:width:imageBlock",t,{priority:"high"})}}()).add(function(){function t(t,e,i){const{item:r}=e,{consumable:n,writer:a}=i;if(!n.consume(r,t.name))return;const o=i.mapper.toViewElement(r),s=Array.from(o.getChildren()).find((t=>"img"===t.name));a.setAttribute("height",e.attributeNewValue.replace("px",""),s||o)}return e=>{e.on("attribute:height:imageInline",t,{priority:"high"}),e.on("attribute:height:imageBlock",t,{priority:"high"})}}()).add(a())}}class s extends t.Plugin{static get requires(){return[o]}static get pluginName(){return"DrupalImage"}}var u=s;class l extends t.Plugin{init(){const{editor:t}=this;t.plugins.get("ImageUploadEditing").on("uploadComplete",((e,{data:i,imageElement:r})=>{t.model.change((t=>{t.setAttribute("dataEntityUuid",i.dataEntityUuid,r),t.setAttribute("dataEntityType",i.dataEntityType,r)}))}))}static get pluginName(){return"DrupalImageUploadEditing"}}var d=i("ckeditor5/src/upload.js"),c=i("ckeditor5/src/utils.js");class m{constructor(t,e){this.loader=t,this.options=e}upload(){return this.loader.file.then((t=>new Promise(((e,i)=>{this._initRequest(),this._initListeners(e,i,t),this._sendRequest(t)}))))}abort(){this.xhr&&this.xhr.abort()}_initRequest(){this.xhr=new XMLHttpRequest,this.xhr.open("POST",this.options.uploadUrl,!0),this.xhr.responseType="json"}_initListeners(t,e,i){const r=this.xhr,n=this.loader,a=`Couldn't upload file: ${i.name}.`;r.addEventListener("error",(()=>e(a))),r.addEventListener("abort",(()=>e())),r.addEventListener("load",(()=>{const i=r.response;if(!i||i.error)return e(i&&i.error&&i.error.message?i.error.message:a);t({urls:{default:i.url},dataEntityUuid:i.uuid?i.uuid:"",dataEntityType:i.entity_type?i.entity_type:""})})),r.upload&&r.upload.addEventListener("progress",(t=>{t.lengthComputable&&(n.uploadTotal=t.total,n.uploaded=t.loaded)}))}_sendRequest(t){const e=this.options.headers||{},i=this.options.withCredentials||!1;Object.keys(e).forEach((t=>{this.xhr.setRequestHeader(t,e[t])})),this.xhr.withCredentials=i;const r=new FormData;r.append("upload",t),this.xhr.send(r)}}class g extends t.Plugin{static get requires(){return[d.FileRepository]}static get pluginName(){return"DrupalFileRepository"}init(){const t=this.editor.config.get("drupalImageUpload");t&&(t.uploadUrl?this.editor.plugins.get(d.FileRepository).createUploadAdapter=e=>new m(e,t):(0,c.logWarning)("simple-upload-adapter-missing-uploadurl"))}}class p extends t.Plugin{static get requires(){return[g,l]}static get pluginName(){return"DrupalImageUpload"}}var h={DrupalImage:u,DrupalImageUpload:p}}(),r=r.default}()}));
 No newline at end of file
+15 −2
Original line number Diff line number Diff line
@@ -7,6 +7,13 @@ function createImageViewElement(writer) {
  return writer.createEmptyElement('img');
}

// A simple helper method to detect number strings.
function isNumberString(value) {
  const parsedValue = parseFloat(value);

  return !Number.isNaN(parsedValue) && value === String(parsedValue);
}

function modelEntityUuidToDataAttribute() {
  function converter(evt, data, conversionApi) {
    const { item } = data;
@@ -476,7 +483,10 @@ export default class DrupalImageEditing extends Plugin {
        model: {
          key: 'width',
          value: (viewElement) => {
            if (isNumberString(viewElement.getAttribute('width'))) {
              return `${viewElement.getAttribute('width')}px`;
            }
            return `${viewElement.getAttribute('width')}`;
          },
        },
      })
@@ -488,7 +498,10 @@ export default class DrupalImageEditing extends Plugin {
        model: {
          key: 'height',
          value: (viewElement) => {
            if (isNumberString(viewElement.getAttribute('height'))) {
              return `${viewElement.getAttribute('height')}px`;
            }
            return `${viewElement.getAttribute('height')}`;
          },
        },
      });
+60 −0
Original line number Diff line number Diff line
@@ -310,4 +310,64 @@ public function providerLinkability(): array {
    ];
  }

  /**
   * Checks that width attribute is correct after upcasting, then downcasting.
   *
   * @param string $width
   *   The width input for source editing.
   *
   * @dataProvider providerWidth
   */
  public function testWidth(string $width): void {
    $page = $this->getSession()->getPage();
    $assert_session = $this->assertSession();

    $this->drupalGet('node/add');
    $page->fillField('title[0][value]', 'My test content');
    $this->assertNotEmpty($image_upload_field = $page->find('css', '.ck-file-dialog-button input[type="file"]'));
    $image = $this->getTestFiles('image')[0];
    $image_upload_field->attachFile($this->container->get('file_system')->realpath($image->uri));
    $assert_session->assertWaitOnAjaxRequest();
    $assert_session->waitForElementVisible('css', '.figure.image');

    // Edit the source of the image through the UI.
    $page->pressButton('Source');
    // Get editor data.
    $editor_data = $this->getEditorDataAsDom();
    // Get the image element data from the editor then set the new width.
    $image = $editor_data->getElementsByTagName('img')->item(0);
    $image->setAttribute('width', $width);
    $new_html = $image->C14N();
    $text_area = $page->find('css', '.ck-source-editing-area > textarea');
    // Set the value of the source code to the updated HTML that has the width
    // attribute.
    $text_area->setValue($new_html);
    // Toggle source editing to force upcasting.
    $page->pressButton('Source');
    $assert_session->waitForElementVisible('css', 'img');
    // Toggle source editing to force downcasting.
    $page->pressButton('Source');
    // Get editor data.
    $editor_data = $this->getEditorDataAsDom();
    $width_from_editor = $editor_data->getElementsByTagName('img')->item(0)->getAttribute('width');
    // Check the contents of the source editing area.
    $this->assertSame($width, $width_from_editor);
  }

  /**
   * Data provider for ::testWidth().
   *
   * @return \string[][]
   */
  public function providerWidth(): array {
    return [
      'Image resize with percent unit (only allowed in HTML 4)' => [
        'width' => '33%',
      ],
      'Image resize with (implied) px unit' => [
        'width' => '100',
      ],
    ];
  }

}