/**
* @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, '' );
expect( getViewData( view, { withoutSelection: true, renderUIElements: true } ) ).to.match( new RegExp(
'' +
'' +
'
' +
'' +
'' +
'' +
''
) );
} );
} );
describe( 'model to data', () => {
it( 'should convert an image with a link', () => {
setModelData( model, '' );
expect( editor.getData() ).to.equal(
'
'
);
} );
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(
'
'
);
expect( getModelData( model, { withoutSelection: true } ) )
.to.equal( '' );
} );
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( '
' );
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(
'
' );
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(
'
'
);
expect( editor.getData() ).to.equal( '
' );
} );
it( 'should not convert if a link misses "href" attribute', () => {
editor.setData(
'
'
);
expect( getModelData( model, { withoutSelection: true } ) )
.to.equal( '' );
} );
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$text>' );
} );
} );
describe( 'a > img', () => {
it( 'should convert a link in an image figure', () => {
editor.setData(
'
'
);
expect( getModelData( model, { withoutSelection: true } ) )
.to.equal( '' );
} );
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( '
' );
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(
'' );
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(
'
'
);
expect( editor.getData() ).to.equal( '
' );
} );
it( 'should not convert if a link misses "href" attribute', () => {
editor.setData(
'
'
);
expect( getModelData( model, { withoutSelection: true } ) )
.to.equal( '' );
} );
} );
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(
'' +
'' +
'
' +
'' +
'' +
'Foo Bar.' +
'' +
''
);
expect( getModelData( editor.model, { withoutSelection: true } ) ).to.equal(
'' +
'Foo Bar.' +
''
);
return editor.destroy();
} );
} );
} );
} );
} );
describe( 'conversion in editing pipeline', () => {
describe( 'model to view', () => {
it( 'should convert the image element', () => {
setModelData( model, '' );
expect( getViewData( view, { withoutSelection: true } ) ).to.equal(
'' +
'' +
'
' +
// Content of the UIElement is skipped here.
'' +
'' +
''
);
} );
it( 'should convert attribute change', () => {
setModelData( model, '' );
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(
'' +
'' +
'
' +
// Content of the UIElement is skipped here.
'' +
'' +
''
);
} );
it( 'should convert attribute removal', () => {
setModelData( model, '' );
const image = model.document.getRoot().getChild( 0 );
model.change( writer => {
writer.removeAttribute( 'linkHref', image );
} );
expect( getViewData( view, { withoutSelection: true } ) ).to.equal(
'' +
'
' +
''
);
} );
} );
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,
'' +
'Foo Bar.' +
''
);
expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal(
'' +
'' +
'
' +
// 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(
'' +
'' +
'
' +
'' +
'Caption' +
'' +
'' +
'' +
'https://cksource.com' +
'' +
'
'
);
expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
'' +
'' +
'' +
'<$text linkHref="https://cksource.com" linkIsDownloadable="true" linkIsExternal="true">' +
'https://cksource.com' +
'$text>' +
''
);
} );
it( 'should upcast the decorators when linked image (a > img)', () => {
// (#7975)
editor.setData(
'' +
'
' +
'' +
'' +
'' +
'https://cksource.com' +
'' +
'
'
);
expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
'' +
'' +
'' +
'<$text linkHref="https://cksource.com" linkIsDownloadable="true" linkIsExternal="true">' +
'https://cksource.com' +
'$text>' +
''
);
} );
} );
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,
'[]' +
'' +
'<$text>https://cksource.com$text>' +
''
);
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(
'' +
'' +
'
' +
'' +
'' +
'' +
'' +
'https://cksource.com' +
'' +
'
'
);
} );
} );
} );
} );