/**
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import LinkEditing from '../src/linkediting';
import LinkCommand from '../src/linkcommand';
import UnlinkCommand from '../src/unlinkcommand';
import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Enter from '@ckeditor/ckeditor5-enter/src/enter';
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
import { isLinkElement } from '../src/utils';
import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
/* global document */
describe( 'LinkEditing', () => {
let editor, model, view;
beforeEach( () => {
return VirtualTestEditor
.create( {
plugins: [ Paragraph, LinkEditing, Enter ]
} )
.then( newEditor => {
editor = newEditor;
model = editor.model;
view = editor.editing.view;
} );
} );
it( 'should be loaded', () => {
expect( editor.plugins.get( LinkEditing ) ).to.be.instanceOf( LinkEditing );
} );
it( 'should set proper schema rules', () => {
expect( model.schema.checkAttribute( [ '$block', '$text' ], 'linkHref' ) ).to.be.true;
expect( model.schema.checkAttribute( [ '$clipboardHolder', '$text' ], 'linkHref' ) ).to.be.true;
expect( model.schema.checkAttribute( [ '$block' ], 'linkHref' ) ).to.be.false;
} );
it( 'should bind two-step caret movement to `linkHref` attribute', () => {
// Let's check only the minimum to not duplicated `bindTwoStepCaretToAttribute()` tests.
// Testing minimum is better then testing using spies that might give false positive results.
// Put selection before the link element.
setModelData( editor.model, ' foobar foobar
foobar
' ); } ); // https://github.com/ckeditor/ckeditor5/issues/500 it( 'should not pick up ``', () => { editor.setData( 'foobar
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( 'foobar
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( 'foobar
' ); } ); // The editor's role is not to filter out potentially malicious data. // Its job is to not let this code be executed inside the editor (see the test in "editing pipeline conversion"). it( 'should output a link with a potential XSS code', () => { setModelData( model, 'foobar
' ); } ); it( 'should convert to link element instance', () => { setModelData( model, 'foobar
' ); } ); } ); describe( 'link highlighting', () => { it( 'should convert the highlight to a proper view classes', () => { setModelData( model, 'foo b{}ar baz
' ); } ); it( 'should work whenever selection has linkHref attribute - link start', () => { setModelData( model, 'foo {}bar baz
' ); } ); it( 'should work whenever selection has linkHref attribute - link end', () => { setModelData( model, 'foo bar{} baz
' ); } ); it( 'should render highlight correctly after splitting the link', () => { setModelData( model, 'foo li{}nk baz
' ); model.change( writer => writer.setSelection( model.document.getRoot().getChild( 0 ), 0 ) ); expect( getViewData( view ) ).to.equal( '{}foo link baz
' ); } ); it( 'should work correctly when selection is moved inside link', () => { setModelData( model, 'foo li{}nk baz
' ); model.change( writer => writer.setSelection( model.document.getRoot().getChild( 0 ), 5 ) ); expect( getViewData( view ) ).to.equal( 'foo l{}ink baz
' ); } ); describe( 'downcast conversion integration', () => { it( 'works for the #insert event', () => { setModelData( model, 'foo liFOO{}nk baz
' ); } ); it( 'works for the #remove event', () => { setModelData( model, 'i{}nk baz
' ); } ); it( 'works for the #attribute event', () => { setModelData( model, 'foo l{in}k baz
' ); } ); it( 'works for the addMarker and removeMarker events', () => { editor.conversion.for( 'editingDowncast' ).markerToHighlight( { model: 'fooMarker', view: {} } ); setModelData( model, 'foo li{}nk baz
' ); model.change( writer => writer.removeMarker( 'fooMarker' ) ); expect( getViewData( view ) ).to.equal( 'foo li{}nk baz
' ); } ); } ); } ); 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' } ]; it( 'link.targetDecorator is predefined as false value', () => { expect( editor.config.get( 'link.targetDecorator' ) ).to.be.false; } ); describe( 'for link.targetDecorator = false', () => { beforeEach( () => { editor.destroy(); return VirtualTestEditor .create( { plugins: [ Paragraph, LinkEditing, Enter ], link: { targetDecorator: true } } ) .then( newEditor => { editor = newEditor; model = editor.model; view = editor.editing.view; } ); } ); it( 'link.targetDecorator is set as true value', () => { expect( editor.config.get( 'link.targetDecorator' ) ).to.be.true; } ); testLinks.forEach( link => { it( `link: ${ link.url } should be treat as ${ link.external ? 'external' : 'non-external' } link`, () => { editor.setData( `foobar
` ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( `foobar
` ); } else { expect( editor.getData() ).to.equal( `foobar
` ); } } ); } ); } ); testLinks.forEach( link => { it( `link: ${ link.url } should not get 'target' and 'rel' attributes`, () => { editor.setData( `foobar
` ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( `foobar
` ); } ); } ); } ); describe( 'custom config', () => { describe( 'mode: automatic', () => { 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( () => { editor.destroy(); return VirtualTestEditor .create( { plugins: [ Paragraph, LinkEditing, Enter ], link: { targetDecorator: false, decorators: [ { mode: 'automatic', callback: url => url.startsWith( 'http' ), attributes: { target: '_blank' } }, { mode: 'automatic', callback: url => url.includes( 'download' ), attributes: { download: 'download' } }, { mode: 'automatic', callback: url => url.startsWith( 'mailto:' ), attributes: { class: 'mail-url' } } ] } } ) .then( newEditor => { editor = newEditor; model = editor.model; view = editor.editing.view; } ); } ); testLinks.forEach( link => { it( `Link: ${ link.url } should get attributes: ${ JSON.stringify( link.attributes ) }`, () => { const ORDER = [ 'target', 'download', 'class' ]; const attr = Object.entries( link.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 ] }" `; }, '' ); editor.setData( `foobar
` ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( `foobar
` ); } ); } ); } ); } ); describe( 'custom linkHref converter', () => { beforeEach( () => { class CustomLinks extends Plugin { init() { const editor = this.editor; editor.conversion.for( 'downcast' ).add( dispatcher => { dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => { conversionApi.consumable.consume( data.item, 'attribute:linkHref' ); // Very simplified downcast just for test assertion. const viewWriter = conversionApi.writer; const linkElement = viewWriter.createAttributeElement( 'a', { href: data.attributeNewValue }, { priority: 5 } ); viewWriter.setCustomProperty( 'link', true, linkElement ); viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), linkElement ); }, { priority: 'highest' } ); } ); } } editor.destroy(); return VirtualTestEditor .create( { plugins: [ Paragraph, LinkEditing, Enter, CustomLinks ], link: { targetDecorator: true, } } ) .then( newEditor => { editor = newEditor; model = editor.model; view = editor.editing.view; } ); } ); it( 'has possibility to override default one', () => { editor.setData( 'foobar
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( 'foobar
' ); } ); } ); } ); } );