/** * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /* globals console */ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import MediaEmbedEditing from '../src/mediaembedediting'; import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; import normalizeHtml from '@ckeditor/ckeditor5-utils/tests/_utils/normalizehtml'; describe( 'MediaEmbedEditing', () => { let editor, model, doc, view; const testProviders = { A: { name: 'A', url: /^foo\.com\/(\w+)/, html: match => `A, id=${ match[ 1 ] }` }, B: { name: 'B', url: /^bar\.com\/(\w+)/, html: match => `B, id=${ match[ 1 ] }` }, C: { name: 'C', url: /^\w+\.com\/(\w+)/, html: match => `C, id=${ match[ 1 ] }` }, extraA: { name: 'extraA', url: /^foo\.com\/(\w+)/, html: match => `extraA, id=${ match[ 1 ] }` }, extraB: { name: 'extraB', url: /^\w+\.com\/(\w+)/, html: match => `extraB, id=${ match[ 1 ] }` }, previewLess: { name: 'preview-less', url: /^https:\/\/preview-less/ }, allowEverything: { name: 'allow-everything', url: /(.*)/, html: match => `allow-everything, id=${ match[ 1 ] }` } }; afterEach( () => { sinon.restore(); } ); it( 'should be named', () => { expect( MediaEmbedEditing.pluginName ).to.equal( 'MediaEmbedEditing' ); } ); describe( 'constructor()', () => { describe( 'configuration', () => { describe( '#providers', () => { it( 'should warn when provider has no name', () => { const consoleWarnStub = sinon.stub( console, 'warn' ); const provider = { url: /.*/ }; return createTestEditor( { providers: [ provider ] } ).then( () => { expect( consoleWarnStub.calledOnce ).to.equal( true ); expect( consoleWarnStub.firstCall.args[ 0 ] ).to.match( /^media-embed-no-provider-name:/ ); expect( consoleWarnStub.firstCall.args[ 1 ].provider ).to.deep.equal( provider ); } ); } ); it( 'can override all providers', () => { return createTestEditor( { providers: [] } ).then( editor => { editor.setData( '
' ); expect( getViewData( editor.editing.view, { withoutSelection: true, renderUIElements: true } ) ).to.equal( '' ); } ); } ); it( 'upcast media according to the order', () => { return createTestEditor( { providers: [ testProviders.A, testProviders.B, testProviders.C ] } ).then( editor => { editor.setData( '
' ); expect( getViewData( editor.editing.view, { withoutSelection: true, renderUIElements: true } ) ).to.equal( '
' + '
' + 'A, id=123' + '
' + '
' ); editor.setData( '
' ); expect( getViewData( editor.editing.view, { withoutSelection: true, renderUIElements: true } ) ).to.equal( '
' + '
' + 'B, id=123' + '
' + '
' ); editor.setData( '
' ); expect( getViewData( editor.editing.view, { withoutSelection: true, renderUIElements: true } ) ).to.equal( '
' + '
' + 'C, id=123' + '
' + '
' ); } ); } ); describe( 'default value', () => { beforeEach( () => { return createTestEditor() .then( newEditor => { editor = newEditor; view = editor.editing.view; } ); } ); describe( 'with preview', () => { it( 'upcasts the URL (dailymotion)', () => { testMediaUpcast( [ 'https://www.dailymotion.com/video/foo', 'www.dailymotion.com/video/foo', 'dailymotion.com/video/foo' ], '
' + '' + '
' ); } ); describe( 'spotify', () => { it( 'upcasts the URL (artist)', () => { testMediaUpcast( [ 'https://www.open.spotify.com/artist/foo', 'www.open.spotify.com/artist/foo', 'open.spotify.com/artist/foo' ], '
' + '' + '
' ); } ); it( 'upcasts the URL (album)', () => { testMediaUpcast( [ 'https://www.open.spotify.com/album/foo', 'www.open.spotify.com/album/foo', 'open.spotify.com/album/foo' ], '
' + '' + '
' ); } ); it( 'upcasts the URL (track)', () => { testMediaUpcast( [ 'https://www.open.spotify.com/track/foo', 'www.open.spotify.com/track/foo', 'open.spotify.com/track/foo' ], '
' + '' + '
' ); } ); } ); it( 'upcasts the URL (youtube)', () => { testMediaUpcast( [ 'https://www.youtube.com/watch?v=foo', 'www.youtube.com/watch?v=foo', 'youtube.com/watch?v=foo', 'https://m.youtube.com/watch?v=foo', 'm.youtube.com/watch?v=foo', 'https://www.youtube.com/v/foo', 'www.youtube.com/v/foo', 'youtube.com/v/foo', 'https://m.youtube.com/v/foo', 'm.youtube.com/v/foo', 'https://www.youtube.com/embed/foo', 'www.youtube.com/embed/foo', 'youtube.com/embed/foo', 'https://youtu.be/foo', 'youtu.be/foo' ], '
' + '' + '
' ); } ); // See: https://github.com/ckeditor/ckeditor5-media-embed/issues/26 it( 'upcasts the URL that contains a dash (youtube)', () => { testMediaUpcast( [ 'https://www.youtube.com/watch?v=euqbMkM-QQk' ], '
' + '' + '
' ); } ); it( 'upcasts the URL (vimeo)', () => { testMediaUpcast( [ 'https://www.vimeo.com/1234', 'www.vimeo.com/1234', 'vimeo.com/1234', 'https://www.vimeo.com/foo/foo/video/1234', 'www.vimeo.com/foo/foo/video/1234', 'vimeo.com/foo/foo/video/1234', 'https://www.vimeo.com/album/foo/video/1234', 'www.vimeo.com/album/foo/video/1234', 'vimeo.com/album/foo/video/1234', 'https://www.vimeo.com/channels/foo/1234', 'www.vimeo.com/channels/foo/1234', 'vimeo.com/channels/foo/1234', 'https://www.vimeo.com/groups/foo/videos/1234', 'www.vimeo.com/groups/foo/videos/1234', 'vimeo.com/groups/foo/videos/1234', 'https://www.vimeo.com/ondemand/foo/1234', 'www.vimeo.com/ondemand/foo/1234', 'vimeo.com/ondemand/foo/1234', 'https://www.player.vimeo.com/video/1234', 'www.player.vimeo.com/video/1234', 'player.vimeo.com/video/1234' ], '
' + '' + '
' ); } ); } ); describe( 'preview-less', () => { it( 'upcasts the URL (instagram)', () => { testMediaUpcast( [ 'https://www.instagram.com/p/foo', 'www.instagram.com/p/foo', 'instagram.com/p/foo' ] ); } ); it( 'upcasts the URL (twitter)', () => { testMediaUpcast( [ 'https://www.twitter.com/foo/bar', 'www.twitter.com/foo/bar', 'twitter.com/foo/bar' ] ); } ); it( 'upcasts the URL (google maps)', () => { testMediaUpcast( [ 'https://www.google.com/maps/foo', 'www.google.com/maps/foo', 'google.com/maps/foo' ] ); } ); it( 'upcasts the URL (flickr)', () => { testMediaUpcast( [ 'https://www.flickr.com/foo/bar', 'www.flickr.com/foo/bar', 'flickr.com/foo/bar' ] ); } ); it( 'upcasts the URL (facebook)', () => { testMediaUpcast( [ 'https://www.facebook.com/foo/bar', 'www.facebook.com/foo/bar', 'facebook.com/foo/bar' ] ); } ); } ); } ); } ); describe( '#extraProviders', () => { it( 'should warn when provider has no name', () => { const consoleWarnStub = sinon.stub( console, 'warn' ); const provider = { url: /.*/ }; return createTestEditor( { extraProviders: [ provider ] } ).then( () => { expect( consoleWarnStub.calledOnce ).to.equal( true ); expect( consoleWarnStub.firstCall.args[ 0 ] ).to.match( /^media-embed-no-provider-name:/ ); expect( consoleWarnStub.firstCall.args[ 1 ].provider ).to.deep.equal( provider ); } ); } ); it( 'extend #providers but with the lower priority', () => { return createTestEditor( { providers: [ testProviders.A ], extraProviders: [ testProviders.extraA, testProviders.extraB ] } ).then( editor => { editor.setData( '
' ); expect( getViewData( editor.editing.view, { withoutSelection: true, renderUIElements: true } ) ).to.equal( '
' + '
' + 'A, id=123' + '
' + '
' ); editor.setData( '
' ); expect( getViewData( editor.editing.view, { withoutSelection: true, renderUIElements: true } ) ).to.equal( '
' + '
' + 'extraB, id=123' + '
' + '
' ); } ); } ); } ); describe( '#removeProviders', () => { it( 'removes #providers', () => { return createTestEditor( { providers: [ testProviders.A, testProviders.B ], removeProviders: [ 'A' ] } ).then( editor => { editor.setData( '
' + '
' ); expect( getViewData( editor.editing.view, { withoutSelection: true, renderUIElements: true } ) ).to.equal( '
' + '
' + 'B, id=123' + '
' + '
' ); } ); } ); it( 'removes #extraProviders', () => { return createTestEditor( { providers: [], extraProviders: [ testProviders.A, testProviders.B ], removeProviders: [ 'A' ] } ).then( editor => { editor.setData( '
' + '
' ); expect( getViewData( editor.editing.view, { withoutSelection: true, renderUIElements: true } ) ).to.equal( '
' + '
' + 'B, id=123' + '
' + '
' ); } ); } ); it( 'removes even when the name of the provider repeats', () => { return createTestEditor( { providers: [ testProviders.A, testProviders.A ], extraProviders: [ testProviders.A, testProviders.A, testProviders.B ], removeProviders: [ 'A' ] } ).then( editor => { editor.setData( '
' + '
' ); expect( getViewData( editor.editing.view, { withoutSelection: true, renderUIElements: true } ) ).to.equal( '
' + '
' + 'B, id=123' + '
' + '
' ); } ); } ); } ); } ); } ); describe( 'init()', () => { const providerDefinitions = [ testProviders.previewLess, testProviders.allowEverything ]; it( 'should be loaded', () => { return createTestEditor() .then( newEditor => { expect( newEditor.plugins.get( MediaEmbedEditing ) ).to.be.instanceOf( MediaEmbedEditing ); } ); } ); it( 'should set proper schema rules', () => { return createTestEditor() .then( newEditor => { model = newEditor.model; expect( model.schema.checkChild( [ '$root' ], 'media' ) ).to.be.true; expect( model.schema.checkAttribute( [ '$root', 'media' ], 'url' ) ).to.be.true; expect( model.schema.isObject( 'media' ) ).to.be.true; expect( model.schema.checkChild( [ '$root', 'media' ], 'media' ) ).to.be.false; expect( model.schema.checkChild( [ '$root', 'media' ], '$text' ) ).to.be.false; expect( model.schema.checkChild( [ '$root', '$block' ], 'image' ) ).to.be.false; } ); } ); describe( 'conversion in the data pipeline', () => { describe( 'previewsInData=false', () => { beforeEach( () => { return createTestEditor( { providers: providerDefinitions } ) .then( newEditor => { editor = newEditor; model = editor.model; doc = model.document; view = editor.editing.view; } ); } ); describe( 'model to view', () => { it( 'should convert', () => { setModelData( model, '' ); expect( editor.getData() ).to.equal( '
' + '' + '
' ); } ); it( 'should convert (no url)', () => { setModelData( model, '' ); expect( editor.getData() ).to.equal( '
' + '' + '
' ); } ); it( 'should convert (preview-less media)', () => { setModelData( model, '' ); expect( editor.getData() ).to.equal( '
' + '' + '
' ); } ); } ); describe( 'view to model', () => { it( 'should convert media figure', () => { editor.setData( '
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' ); } ); it( 'should not convert if there is no media class', () => { editor.setData( '
My quote
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' ); } ); it( 'should not convert if there is no oembed wrapper inside #1', () => { editor.setData( '
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' ); } ); it( 'should not convert if there is no oembed wrapper inside #2', () => { editor.setData( '
test
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' ); } ); it( 'should not convert when the wrapper has no data-oembed-url attribute', () => { editor.setData( '
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' ); } ); it( 'should not convert in the wrong context', () => { model.schema.register( 'blockquote', { inheritAllFrom: '$block' } ); model.schema.addChildCheck( ( ctx, childDef ) => { if ( ctx.endsWith( '$root' ) && childDef.name == 'media' ) { return false; } } ); editor.conversion.elementToElement( { model: 'blockquote', view: 'blockquote' } ); editor.setData( '
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '
' ); } ); it( 'should not convert if the oembed wrapper is already consumed', () => { editor.data.upcastDispatcher.on( 'element:figure', ( evt, data, conversionApi ) => { const img = data.viewItem.getChild( 0 ); conversionApi.consumable.consume( img, { name: true } ); }, { priority: 'high' } ); editor.setData( '
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' ); } ); it( 'should not convert if the figure is already consumed', () => { editor.data.upcastDispatcher.on( 'element:figure', ( evt, data, conversionApi ) => { conversionApi.consumable.consume( data.viewItem, { name: true, class: 'image' } ); }, { priority: 'high' } ); editor.setData( '
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' ); } ); it( 'should discard the contents of the media', () => { editor.setData( '
foo bar
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' ); } ); it( 'should not convert unknown media', () => { return createTestEditor( { providers: [ testProviders.A ] } ) .then( newEditor => { newEditor.setData( '
' + '
' ); expect( getModelData( newEditor.model, { withoutSelection: true } ) ) .to.equal( '' ); return newEditor.destroy(); } ); } ); } ); } ); describe( 'previewsInData=true', () => { beforeEach( () => { return createTestEditor( { providers: providerDefinitions, previewsInData: true } ) .then( newEditor => { editor = newEditor; model = editor.model; doc = model.document; view = editor.editing.view; } ); } ); describe( 'conversion in the data pipeline', () => { describe( 'model to view', () => { it( 'should convert', () => { setModelData( model, '' ); expect( editor.getData() ).to.equal( '
' + '
' + 'allow-everything, id=https://ckeditor.com' + '
' + '
' ); } ); it( 'should convert (no url)', () => { setModelData( model, '' ); expect( editor.getData() ).to.equal( '
' + '' + '' + '
' ); } ); it( 'should convert (preview-less media)', () => { setModelData( model, '' ); expect( editor.getData() ).to.equal( '
' + '' + '
' ); } ); } ); describe( 'view to model', () => { it( 'should convert media figure', () => { editor.setData( '
' + '
' + 'allow-everything, id=https://cksource.com>' + '
' + '
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' ); } ); it( 'should not convert if there is no media class', () => { editor.setData( '
My quote
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' ); } ); it( 'should not convert if there is no oembed wrapper inside #1', () => { editor.setData( '
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' ); } ); it( 'should not convert if there is no oembed wrapper inside #2', () => { editor.setData( '
test
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' ); } ); it( 'should not convert in the wrong context', () => { model.schema.register( 'div', { inheritAllFrom: '$block' } ); model.schema.addChildCheck( ( ctx, childDef ) => { if ( ctx.endsWith( '$root' ) && childDef.name == 'media' ) { return false; } } ); editor.conversion.elementToElement( { model: 'div', view: 'div' } ); editor.setData( '
' + '
' + '
' + '' + '
' + '
' + '
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '
' ); } ); it( 'should not convert if the oembed wrapper is already consumed', () => { editor.data.upcastDispatcher.on( 'element:figure', ( evt, data, conversionApi ) => { const img = data.viewItem.getChild( 0 ); conversionApi.consumable.consume( img, { name: true } ); }, { priority: 'high' } ); editor.setData( '
' + '
' + '
' + '' + '
' + '
' + '
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' ); } ); it( 'should not convert if the figure is already consumed', () => { editor.data.upcastDispatcher.on( 'element:figure', ( evt, data, conversionApi ) => { conversionApi.consumable.consume( data.viewItem, { name: true, class: 'image' } ); }, { priority: 'high' } ); editor.setData( '
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' ); } ); it( 'should discard the contents of the media', () => { editor.setData( '
' + '
' + 'foo bar baz' + '
' + '
' ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' ); } ); it( 'should not convert unknown media', () => { return createTestEditor( { providers: [ testProviders.A ] } ) .then( newEditor => { newEditor.setData( '
' + '
' + '
' + '
' + '
' + '
' ); expect( getModelData( newEditor.model, { withoutSelection: true } ) ) .to.equal( '' ); return newEditor.destroy(); } ); } ); } ); } ); } ); } ); describe( 'conversion in the editing pipeline', () => { describe( 'previewsInData=false', () => { beforeEach( () => { return createTestEditor( { providers: providerDefinitions } ) .then( newEditor => { editor = newEditor; model = editor.model; doc = model.document; view = editor.editing.view; } ); } ); test(); } ); describe( 'previewsInData=true', () => { beforeEach( () => { return createTestEditor( { providers: providerDefinitions, previewsInData: true } ) .then( newEditor => { editor = newEditor; model = editor.model; doc = model.document; view = editor.editing.view; } ); } ); test(); } ); function test() { describe( 'model to view', () => { it( 'should convert', () => { setModelData( model, '' ); expect( getViewData( view, { withoutSelection: true, renderUIElements: true } ) ).to.equal( '
' + '
' + 'allow-everything, id=https://ckeditor.com' + '
' + '
' ); } ); it( 'should convert the url attribute change', () => { setModelData( model, '' ); const media = doc.getRoot().getChild( 0 ); model.change( writer => { writer.setAttribute( 'url', 'https://cksource.com', media ); } ); expect( getViewData( view, { withoutSelection: true, renderUIElements: true } ) ).to.equal( '
' + '
' + 'allow-everything, id=https://cksource.com' + '
' + '
' ); } ); it( 'should convert the url attribute removal', () => { setModelData( model, '' ); const media = doc.getRoot().getChild( 0 ); model.change( writer => { writer.removeAttribute( 'url', media ); } ); expect( getViewData( view, { withoutSelection: true, renderUIElements: true } ) ) .to.equal( '
' + '
' + '
' + '
' ); } ); it( 'should not convert the url attribute removal if is already consumed', () => { setModelData( model, '' ); const media = doc.getRoot().getChild( 0 ); editor.editing.downcastDispatcher.on( 'attribute:url:media', ( evt, data, conversionApi ) => { conversionApi.consumable.consume( data.item, 'attribute:url' ); }, { priority: 'high' } ); model.change( writer => { writer.removeAttribute( 'url', media ); } ); expect( getViewData( view, { withoutSelection: true, renderUIElements: true } ) ).to.equal( '
' + '
' + 'allow-everything, id=https://ckeditor.com' + '
' + '
' ); } ); // Related to https://github.com/ckeditor/ckeditor5/issues/407. it( 'should not discard internals (e.g. UI) injected by other features when converting the url attribute', () => { setModelData( model, '' ); const media = doc.getRoot().getChild( 0 ); editor.editing.view.change( writer => { const widgetViewElement = editor.editing.mapper.toViewElement( media ); const externalUIElement = writer.createUIElement( 'div', null, function( domDocument ) { const domElement = this.toDomElement( domDocument ); domElement.innerHTML = 'external UI'; return domElement; } ); writer.insert( writer.createPositionAt( widgetViewElement, 'end' ), externalUIElement ); } ); expect( getViewData( view, { withoutSelection: true, renderUIElements: true } ) ).to.equal( '
' + '
' + 'allow-everything, id=https://ckeditor.com' + '
' + '
external UI
' + '
' ); model.change( writer => { writer.setAttribute( 'url', 'https://cksource.com', media ); } ); expect( getViewData( view, { withoutSelection: true, renderUIElements: true } ) ).to.equal( '
' + '
' + 'allow-everything, id=https://cksource.com' + '
' + '
external UI
' + '
' ); } ); } ); } } ); } ); function testMediaUpcast( urls, expected ) { for ( const url of urls ) { editor.setData( `
` ); const viewData = getViewData( view, { withoutSelection: true, renderUIElements: true } ); let expectedRegExp; const expectedUrl = url.match( /^https?:\/\// ) ? url : 'https://' + url; if ( expected ) { expectedRegExp = new RegExp( ']+>' + ']+>' + normalizeHtml( expected ) + '' + '' ); } else { expectedRegExp = new RegExp( ']+>' + ']+>' + '' + '' + '' ); } expect( normalizeHtml( viewData ) ).to.match( expectedRegExp, `assertion for "${ url }"` ); } } function createTestEditor( mediaEmbedConfig ) { return VirtualTestEditor .create( { plugins: [ MediaEmbedEditing ], mediaEmbed: mediaEmbedConfig } ); } } );