/** * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import LinkImageEditing from '../src/linkimageediting'; import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import normalizeHtml from '@ckeditor/ckeditor5-utils/tests/_utils/normalizehtml'; import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; import ImageCaptionEditing from '@ckeditor/ckeditor5-image/src/imagecaption/imagecaptionediting'; describe( 'LinkImageEditing', () => { let editor, model, view; beforeEach( () => { return VirtualTestEditor .create( { plugins: [ Paragraph, LinkImageEditing ] } ) .then( newEditor => { editor = newEditor; model = editor.model; view = editor.editing.view; } ); } ); afterEach( () => { return editor.destroy(); } ); it( 'should have pluginName', () => { expect( LinkImageEditing.pluginName ).to.equal( 'LinkImageEditing' ); } ); it( 'should be loaded', () => { expect( editor.plugins.get( LinkImageEditing ) ).to.be.instanceOf( LinkImageEditing ); } ); it( 'should set proper schema rules', () => { expect( model.schema.checkAttribute( [ '$root', 'image' ], 'linkHref' ) ).to.be.true; } ); describe( 'conversion in data pipeline', () => { describe( 'model to view', () => { it( 'should attach a link indicator to the image element', () => { setModelData( model, 'alt text' ); expect( getViewData( view, { withoutSelection: true, renderUIElements: true } ) ).to.match( new RegExp( '
' + '' + 'alt text' + '' + ']+>.*<\\/svg>' + '' + '' + '
' ) ); } ); } ); describe( 'model to data', () => { it( 'should convert an image with a link', () => { setModelData( model, 'alt text' ); expect( editor.getData() ).to.equal( '
alt text
' ); } ); it( 'should convert an image with a link and without alt attribute', () => { setModelData( model, '' ); expect( editor.getData() ).to.equal( '
' ); } ); it( 'should convert srcset attribute to srcset and sizes attribute wrapped into a link', () => { setModelData( model, '' + '' ); expect( normalizeHtml( editor.getData() ) ).to.equal( '
' + '' + '' + '' + '
' ); } ); } ); describe( 'view to model', () => { describe( 'figure > a > img', () => { it( 'should convert a link in an image figure', () => { editor.setData( '
alt text
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( 'alt text' ); } ); it( 'should convert an image with a link and without alt attribute', () => { editor.setData( '
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' ); } ); it( 'should not convert without src attribute', () => { editor.setData( '
alt text
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' ); } ); it( 'should not convert in wrong context', () => { model.schema.register( 'div', { inheritAllFrom: '$block' } ); model.schema.addChildCheck( ( ctx, childDef ) => { if ( ctx.endsWith( '$root' ) && childDef.name == 'image' ) { return false; } } ); editor.conversion.elementToElement( { model: 'div', view: 'div' } ); editor.setData( '
' + '
' + '' + 'alt text' + '' + '
' + '
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '
' ); } ); it( 'should not convert "a" element if is already consumed', () => { editor.data.upcastDispatcher.on( 'element:a', ( evt, data, conversionApi ) => { conversionApi.consumable.consume( data.viewItem, { attributes: [ 'href' ] } ); }, { priority: 'highest' } ); editor.setData( '
alt text
' ); expect( editor.getData() ).to.equal( '
alt text
' ); } ); it( 'should not convert if a link misses "href" attribute', () => { editor.setData( '
alt text
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( 'alt text' ); } ); it( 'should convert a link without an image to a paragraph with the link', () => { editor.setData( '
Foo
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '<$text linkHref="http://ckeditor.com">Foo' ); } ); } ); describe( 'a > img', () => { it( 'should convert a link in an image figure', () => { editor.setData( 'alt text' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( 'alt text' ); } ); it( 'should convert an image with a link and without alt attribute', () => { editor.setData( '' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' ); } ); it( 'should not convert without src attribute', () => { editor.setData( 'alt text' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' ); } ); it( 'should not convert in wrong context', () => { model.schema.register( 'div', { inheritAllFrom: '$block' } ); model.schema.addChildCheck( ( ctx, childDef ) => { if ( ctx.endsWith( '$root' ) && childDef.name == 'image' ) { return false; } } ); editor.conversion.elementToElement( { model: 'div', view: 'div' } ); editor.setData( '
' + '' + 'alt text' + '' + '
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '
' ); } ); it( 'should not convert "a" element if is already consumed', () => { editor.data.upcastDispatcher.on( 'element:a', ( evt, data, conversionApi ) => { conversionApi.consumable.consume( data.viewItem, { attributes: [ 'href' ] } ); }, { priority: 'highest' } ); editor.setData( 'alt text' ); expect( editor.getData() ).to.equal( '
alt text
' ); } ); it( 'should not convert if a link misses "href" attribute', () => { editor.setData( 'alt text' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( 'alt text' ); } ); } ); describe( 'figure > a > img + figcaption', () => { it( 'should convert a link and the caption element', () => { return VirtualTestEditor .create( { plugins: [ Paragraph, LinkImageEditing, ImageCaptionEditing ] } ) .then( editor => { editor.setData( '
' + '' + 'alt text' + '' + '
' + 'Foo Bar.' + '
' + '
' ); expect( getModelData( editor.model, { withoutSelection: true } ) ).to.equal( 'alt text' + 'Foo Bar.' + '' ); return editor.destroy(); } ); } ); } ); } ); } ); describe( 'conversion in editing pipeline', () => { describe( 'model to view', () => { it( 'should convert the image element', () => { setModelData( model, 'alt text' ); expect( getViewData( view, { withoutSelection: true } ) ).to.equal( '
' + '' + 'alt text' + // Content of the UIElement is skipped here. '' + '' + '
' ); } ); it( 'should convert attribute change', () => { setModelData( model, 'alt text' ); const image = model.document.getRoot().getChild( 0 ); model.change( writer => { writer.setAttribute( 'linkHref', 'https://ckeditor.com/why-ckeditor/', image ); } ); expect( getViewData( view, { withoutSelection: true } ) ).to.equal( '
' + '' + 'alt text' + // Content of the UIElement is skipped here. '' + '' + '
' ); } ); it( 'should convert attribute removal', () => { setModelData( model, 'alt text' ); const image = model.document.getRoot().getChild( 0 ); model.change( writer => { writer.removeAttribute( 'linkHref', image ); } ); expect( getViewData( view, { withoutSelection: true } ) ).to.equal( '
' + 'alt text' + '
' ); } ); } ); describe( 'figure > a > img + span + figcaption', () => { it( 'should convert a link and the caption element', () => { return VirtualTestEditor .create( { plugins: [ Paragraph, LinkImageEditing, ImageCaptionEditing ] } ) .then( editor => { setModelData( editor.model, 'alt text' + 'Foo Bar.' + '' ); expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( '
' + '' + 'alt text' + // Content of the UIElement is skipped here. '' + '' + '
' + 'Foo Bar.' + '
' + '
' ); return editor.destroy(); } ); } ); } ); } ); describe( 'link attributes decorator', () => { describe( 'default behavior', () => { const testLinks = [ { external: true, url: 'http://example.com' }, { external: true, url: 'https://cksource.com' }, { external: false, url: 'ftp://server.io' }, { external: true, url: '//schemaless.org' }, { external: false, url: 'www.ckeditor.com' }, { external: false, url: '/relative/url.html' }, { external: false, url: 'another/relative/url.html' }, { external: false, url: '#anchor' }, { external: false, url: 'mailto:some@user.org' }, { external: false, url: 'tel:123456789' } ]; describe( 'for link.addTargetToExternalLinks=false', () => { let editor, model; beforeEach( async () => { editor = await VirtualTestEditor.create( { plugins: [ Paragraph, LinkImageEditing ], link: { addTargetToExternalLinks: false } } ); model = editor.model; view = editor.editing.view; } ); afterEach( async () => { await editor.destroy(); } ); testLinks.forEach( link => { it( `link: ${ link.url } should not get 'target' and 'rel' attributes`, () => { // Upcast check. editor.setData( '
' + `` + '' + '' + '
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( `` ); // Downcast check. expect( editor.getData() ).to.equal( '
' + `` + '' + '' + '
' ); } ); } ); } ); describe( 'for link.addTargetToExternalLinks=true', () => { let editor, model; beforeEach( async () => { editor = await VirtualTestEditor.create( { plugins: [ Paragraph, LinkImageEditing ], link: { addTargetToExternalLinks: true } } ); model = editor.model; view = editor.editing.view; } ); afterEach( async () => { await editor.destroy(); } ); testLinks.forEach( link => { it( `link: ${ link.url } should be treat as ${ link.external ? 'external' : 'non-external' } link`, () => { // Upcast check. editor.setData( `` ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( `` ); // Downcast check. if ( link.external ) { expect( editor.getData() ).to.equal( '
' + `` + '' + '' + '
' ); } else { expect( editor.getData() ).to.equal( '
' + `` + '' + '' + '
' ); } } ); } ); } ); } ); describe( 'custom config', () => { describe( 'mode: automatic', () => { let editor; const testLinks = [ { url: 'relative/url.html', attributes: {} }, { url: 'http://exmaple.com', attributes: { target: '_blank' } }, { url: 'https://example.com/download/link.pdf', attributes: { target: '_blank', download: 'download' } }, { url: 'mailto:some@person.io', attributes: { class: 'mail-url' } } ]; beforeEach( async () => { editor = await VirtualTestEditor.create( { plugins: [ Paragraph, LinkImageEditing ], link: { addTargetToExternalLinks: false, decorators: { isExternal: { mode: 'automatic', callback: url => url.startsWith( 'http' ), attributes: { target: '_blank' } }, isDownloadable: { mode: 'automatic', callback: url => url.includes( 'download' ), attributes: { download: 'download' } }, isMail: { mode: 'automatic', callback: url => url.startsWith( 'mailto:' ), attributes: { class: 'mail-url' } } } } } ); model = editor.model; } ); afterEach( () => { return editor.destroy(); } ); testLinks.forEach( link => { it( `Link: ${ link.url } should get attributes: ${ JSON.stringify( link.attributes ) }`, () => { const ORDER = [ 'class', 'href', 'target', 'download' ]; const attributes = Object.assign( {}, link.attributes, { href: link.url } ); const attr = Object.entries( attributes ).sort( ( a, b ) => { const aIndex = ORDER.indexOf( a[ 0 ] ); const bIndex = ORDER.indexOf( b[ 0 ] ); return aIndex - bIndex; } ); const reducedAttr = attr.reduce( ( acc, cur ) => { return acc + `${ cur[ 0 ] }="${ cur[ 1 ] }" `; }, '' ).trim(); editor.setData( `` ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( `` ); // Order of attributes is important, that's why this is assert is construct in such way. expect( editor.getData() ).to.equal( '
' + `` + '' + '' + '
' ); } ); } ); } ); } ); describe( 'upcast converter', () => { let editor, model; beforeEach( () => { return VirtualTestEditor .create( { plugins: [ Paragraph, LinkImageEditing ], link: { decorators: { isExternal: { mode: 'manual', label: 'Open in a new tab', attributes: { target: '_blank', rel: 'noopener noreferrer' } }, isDownloadable: { mode: 'manual', label: 'Downloadable', attributes: { download: 'download' } }, isGallery: { mode: 'manual', label: 'Gallery link', attributes: { class: 'gallery' } } } } } ) .then( newEditor => { editor = newEditor; model = editor.model; } ); } ); afterEach( () => { return editor.destroy(); } ); it( 'should upcast attributes', async () => { editor.setData( '
' + '' + '' + '' + '
' ); expect( getModelData( model, { withoutSelection: true } ) ).to.equal( '' ); await editor.destroy(); } ); it( 'should not upcast partial and incorrect attributes', async () => { editor.setData( '
' + '' + '' + '' + '
' ); expect( getModelData( model, { withoutSelection: true } ) ).to.equal( '' ); } ); it( 'allows overwriting conversion process using highest priority', () => { // Block manual decorator converter. Consume all attributes and do nothing with them. editor.conversion.for( 'upcast' ).add( dispatcher => { dispatcher.on( 'element:a', ( evt, data, conversionApi ) => { const consumableAttributes = { attributes: [ 'target', 'rel', 'download' ] }; conversionApi.consumable.consume( data.viewItem, consumableAttributes ); }, { priority: 'highest' } ); } ); editor.setData( '
' + '' + '' + '' + '
' ); expect( editor.getData() ).to.equal( '
' ); } ); it( 'should upcast the decorators when linked image (figure > a > img)', () => { // (#7975) editor.setData( '
' + '' + 'bar' + '' + '
Caption
' + '
' + '

' + '' + 'https://cksource.com' + '' + '

' ); expect( getModelData( model, { withoutSelection: true } ) ).to.equal( 'bar' + '' + '' + '<$text linkHref="https://cksource.com" linkIsDownloadable="true" linkIsExternal="true">' + 'https://cksource.com' + '' + '' ); } ); it( 'should upcast the decorators when linked image (a > img)', () => { // (#7975) editor.setData( '' + 'bar' + '' + '

' + '' + 'https://cksource.com' + '' + '

' ); expect( getModelData( model, { withoutSelection: true } ) ).to.equal( 'bar' + '' + '' + '<$text linkHref="https://cksource.com" linkIsDownloadable="true" linkIsExternal="true">' + 'https://cksource.com' + '' + '' ); } ); } ); describe( 'downcast converter', () => { let editor, model; beforeEach( () => { return VirtualTestEditor .create( { plugins: [ Paragraph, LinkImageEditing ], link: { decorators: { isExternal: { mode: 'manual', label: 'Open in a new tab', attributes: { target: '_blank', rel: 'noopener noreferrer' } }, isDownloadable: { mode: 'manual', label: 'Downloadable', attributes: { download: 'download' } }, isGallery: { mode: 'manual', label: 'Gallery link', attributes: { class: 'gallery' } } } } } ) .then( newEditor => { editor = newEditor; model = editor.model; } ); } ); afterEach( () => { return editor.destroy(); } ); it( 'should downcast the decorators after applying a change', () => { setModelData( model, '[bar]' + '' + '<$text>https://cksource.com' + '' ); editor.execute( 'link', 'https://cksource.com', { linkIsDownloadable: true, linkIsExternal: true, linkIsGallery: true } ); model.change( writer => { writer.setSelection( model.document.getRoot().getChild( 1 ).getChild( 0 ), 'on' ); } ); editor.execute( 'link', 'https://cksource.com', { linkIsDownloadable: true, linkIsExternal: true, linkIsGallery: true } ); expect( editor.getData() ).to.equal( '
' + '' + 'bar' + '' + '
' + '

' + '' + 'https://cksource.com' + '' + '

' ); } ); } ); } ); } );