mediaembedediting.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
  6. import MediaEmbedEditing from '../src/mediaembedediting';
  7. import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  8. import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
  9. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  10. describe( 'MediaEmbedEditing', () => {
  11. let editor, model, doc, view;
  12. const mediaDefinitions = {
  13. test: {
  14. url: /(.*)/,
  15. html: id => `<iframe src="${ id }"></iframe>`
  16. }
  17. };
  18. testUtils.createSinonSandbox();
  19. describe( 'constructor()', () => {
  20. describe( 'config.mediaEmbed.media', () => {
  21. beforeEach( () => {
  22. return VirtualTestEditor
  23. .create( {
  24. plugins: [ MediaEmbedEditing ],
  25. mediaEmbed: {
  26. semanticDataOutput: true
  27. }
  28. } )
  29. .then( newEditor => {
  30. editor = newEditor;
  31. view = editor.editing.view;
  32. } );
  33. } );
  34. it( 'upcasts the URL (dailymotion)', () => {
  35. testMediaConfig( [
  36. 'https://www.dailymotion.com/video/foo',
  37. 'www.dailymotion.com/video/foo',
  38. 'dailymotion.com/video/foo',
  39. ],
  40. '<iframe src="https://www.dailymotion.com/embed/video/foo" ' +
  41. 'frameborder="0" width="480" height="270" allowfullscreen="" allow="autoplay">' +
  42. '</iframe>' );
  43. } );
  44. it( 'upcasts the URL (instagram)', () => {
  45. testMediaConfig( [
  46. 'https://www.instagram.com/p/foo',
  47. 'www.instagram.com/p/foo',
  48. 'instagram.com/p/foo',
  49. ],
  50. '<iframe src="http://instagram.com/p/foo/embed" frameborder="0"></iframe>' );
  51. } );
  52. describe( 'spotify', () => {
  53. it( 'upcasts the URL (artist)', () => {
  54. testMediaConfig( [
  55. 'https://www.open.spotify.com/artist/foo',
  56. 'www.open.spotify.com/artist/foo',
  57. 'open.spotify.com/artist/foo',
  58. ],
  59. '<iframe src="https://open.spotify.com/embed/artist/foo" ' +
  60. 'frameborder="0" allowtransparency="true" allow="encrypted-media">' +
  61. '</iframe>' );
  62. } );
  63. it( 'upcasts the URL (album)', () => {
  64. testMediaConfig( [
  65. 'https://www.open.spotify.com/album/foo',
  66. 'www.open.spotify.com/album/foo',
  67. 'open.spotify.com/album/foo',
  68. ],
  69. '<iframe src="https://open.spotify.com/embed/album/foo" ' +
  70. 'frameborder="0" allowtransparency="true" allow="encrypted-media">' +
  71. '</iframe>' );
  72. } );
  73. it( 'upcasts the URL (track)', () => {
  74. testMediaConfig( [
  75. 'https://www.open.spotify.com/track/foo',
  76. 'www.open.spotify.com/track/foo',
  77. 'open.spotify.com/track/foo',
  78. ],
  79. '<iframe src="https://open.spotify.com/embed/track/foo" ' +
  80. 'frameborder="0" allowtransparency="true" allow="encrypted-media">' +
  81. '</iframe>' );
  82. } );
  83. } );
  84. it( 'upcasts the URL (youtube)', () => {
  85. testMediaConfig( [
  86. 'https://www.youtube.com/watch?v=foo',
  87. 'www.youtube.com/watch?v=foo',
  88. 'youtube.com/watch?v=foo',
  89. 'https://www.youtube.com/v/foo',
  90. 'www.youtube.com/v/foo',
  91. 'youtube.com/v/foo',
  92. 'https://www.youtube.com/embed/foo',
  93. 'www.youtube.com/embed/foo',
  94. 'youtube.com/embed/foo',
  95. 'https://youtu.be/foo',
  96. 'youtu.be/foo'
  97. ],
  98. '<iframe src="https://www.youtube.com/embed/foo" ' +
  99. 'frameborder="0" allow="autoplay; encrypted-media" allowfullscreen="">' +
  100. '</iframe>' );
  101. } );
  102. it( 'upcasts the URL (vimeo)', () => {
  103. testMediaConfig( [
  104. 'https://www.vimeo.com/1234',
  105. 'www.vimeo.com/1234',
  106. 'vimeo.com/1234',
  107. 'https://www.vimeo.com/foo/foo/video/1234',
  108. 'www.vimeo.com/foo/foo/video/1234',
  109. 'vimeo.com/foo/foo/video/1234',
  110. 'https://www.vimeo.com/album/foo/video/1234',
  111. 'www.vimeo.com/album/foo/video/1234',
  112. 'vimeo.com/album/foo/video/1234',
  113. 'https://www.vimeo.com/channels/foo/1234',
  114. 'www.vimeo.com/channels/foo/1234',
  115. 'vimeo.com/channels/foo/1234',
  116. 'https://www.vimeo.com/groups/foo/videos/1234',
  117. 'www.vimeo.com/groups/foo/videos/1234',
  118. 'vimeo.com/groups/foo/videos/1234',
  119. 'https://www.vimeo.com/ondemand/foo/1234',
  120. 'www.vimeo.com/ondemand/foo/1234',
  121. 'vimeo.com/ondemand/foo/1234',
  122. 'https://www.player.vimeo.com/video/1234',
  123. 'www.player.vimeo.com/video/1234',
  124. 'player.vimeo.com/video/1234'
  125. ],
  126. '<iframe src="https://player.vimeo.com/video/1234" ' +
  127. 'frameborder="0" webkitallowfullscreen="" mozallowfullscreen="" allowfullscreen="">' +
  128. '</iframe>' );
  129. } );
  130. } );
  131. } );
  132. describe( 'init()', () => {
  133. it( 'should be loaded', () => {
  134. return VirtualTestEditor
  135. .create( {
  136. plugins: [ MediaEmbedEditing ],
  137. } )
  138. .then( newEditor => {
  139. expect( newEditor.plugins.get( MediaEmbedEditing ) ).to.be.instanceOf( MediaEmbedEditing );
  140. } );
  141. } );
  142. it( 'should set proper schema rules', () => {
  143. return VirtualTestEditor
  144. .create( {
  145. plugins: [ MediaEmbedEditing ],
  146. } )
  147. .then( newEditor => {
  148. model = newEditor.model;
  149. expect( model.schema.checkChild( [ '$root' ], 'media' ) ).to.be.true;
  150. expect( model.schema.checkAttribute( [ '$root', 'media' ], 'url' ) ).to.be.true;
  151. expect( model.schema.isObject( 'media' ) ).to.be.true;
  152. expect( model.schema.checkChild( [ '$root', 'media' ], 'media' ) ).to.be.false;
  153. expect( model.schema.checkChild( [ '$root', 'media' ], '$text' ) ).to.be.false;
  154. expect( model.schema.checkChild( [ '$root', '$block' ], 'image' ) ).to.be.false;
  155. } );
  156. } );
  157. describe( 'conversion in the data pipeline', () => {
  158. describe( 'semanticDataOutput=true', () => {
  159. beforeEach( () => {
  160. return VirtualTestEditor
  161. .create( {
  162. plugins: [ MediaEmbedEditing ],
  163. mediaEmbed: {
  164. semanticDataOutput: true,
  165. media: mediaDefinitions
  166. }
  167. } )
  168. .then( newEditor => {
  169. editor = newEditor;
  170. model = editor.model;
  171. doc = model.document;
  172. view = editor.editing.view;
  173. } );
  174. } );
  175. describe( 'model to view', () => {
  176. it( 'should convert', () => {
  177. setModelData( model, '<media url="http://ckeditor.com"></media>' );
  178. expect( editor.getData() ).to.equal(
  179. '<figure class="media">' +
  180. '<div data-oembed-url="http://ckeditor.com"></div>' +
  181. '</figure>' );
  182. } );
  183. it( 'should convert (no url)', () => {
  184. setModelData( model, '<media></media>' );
  185. expect( editor.getData() ).to.equal(
  186. '<figure class="media">' +
  187. '<div></div>' +
  188. '</figure>' );
  189. } );
  190. } );
  191. describe( 'view to model', () => {
  192. it( 'should convert media figure', () => {
  193. editor.setData( '<figure class="media"><div data-oembed-url="http://ckeditor.com"></div></figure>' );
  194. expect( getModelData( model, { withoutSelection: true } ) )
  195. .to.equal( '<media url="http://ckeditor.com"></media>' );
  196. } );
  197. it( 'should not convert if there is no media class', () => {
  198. editor.setData( '<figure class="quote">My quote</figure>' );
  199. expect( getModelData( model, { withoutSelection: true } ) )
  200. .to.equal( '' );
  201. } );
  202. it( 'should not convert if there is no oembed wrapper inside #1', () => {
  203. editor.setData( '<figure class="media"></figure>' );
  204. expect( getModelData( model, { withoutSelection: true } ) )
  205. .to.equal( '' );
  206. } );
  207. it( 'should not convert if there is no oembed wrapper inside #2', () => {
  208. editor.setData( '<figure class="media">test</figure>' );
  209. expect( getModelData( model, { withoutSelection: true } ) )
  210. .to.equal( '' );
  211. } );
  212. it( 'should not convert when the wrapper has no data-oembed-url attribute', () => {
  213. editor.setData( '<figure class="media"><div></div></figure>' );
  214. expect( getModelData( model, { withoutSelection: true } ) )
  215. .to.equal( '' );
  216. } );
  217. it( 'should not convert in the wrong context', () => {
  218. model.schema.register( 'blockquote', { inheritAllFrom: '$block' } );
  219. model.schema.addChildCheck( ( ctx, childDef ) => {
  220. if ( ctx.endsWith( '$root' ) && childDef.name == 'media' ) {
  221. return false;
  222. }
  223. } );
  224. editor.conversion.elementToElement( { model: 'blockquote', view: 'blockquote' } );
  225. editor.setData(
  226. '<blockquote><figure class="media"><div data-oembed-url="http://ckeditor.com"></div></figure></blockquote>' );
  227. expect( getModelData( model, { withoutSelection: true } ) )
  228. .to.equal( '<blockquote></blockquote>' );
  229. } );
  230. it( 'should not convert if the oembed wrapper is already consumed', () => {
  231. editor.data.upcastDispatcher.on( 'element:figure', ( evt, data, conversionApi ) => {
  232. const img = data.viewItem.getChild( 0 );
  233. conversionApi.consumable.consume( img, { name: true } );
  234. }, { priority: 'high' } );
  235. editor.setData( '<figure class="media"><div data-oembed-url="http://ckeditor.com"></div></figure>' );
  236. expect( getModelData( model, { withoutSelection: true } ) )
  237. .to.equal( '' );
  238. } );
  239. it( 'should not convert if the figure is already consumed', () => {
  240. editor.data.upcastDispatcher.on( 'element:figure', ( evt, data, conversionApi ) => {
  241. conversionApi.consumable.consume( data.viewItem, { name: true, class: 'image' } );
  242. }, { priority: 'high' } );
  243. editor.setData( '<figure class="media"><div data-oembed-url="http://ckeditor.com"></div></figure>' );
  244. expect( getModelData( model, { withoutSelection: true } ) )
  245. .to.equal( '' );
  246. } );
  247. } );
  248. } );
  249. describe( 'semanticDataOutput=false', () => {
  250. beforeEach( () => {
  251. return VirtualTestEditor
  252. .create( {
  253. plugins: [ MediaEmbedEditing ],
  254. mediaEmbed: {
  255. media: mediaDefinitions
  256. }
  257. } )
  258. .then( newEditor => {
  259. editor = newEditor;
  260. model = editor.model;
  261. doc = model.document;
  262. view = editor.editing.view;
  263. } );
  264. } );
  265. describe( 'conversion in the data pipeline', () => {
  266. describe( 'model to view', () => {
  267. it( 'should convert', () => {
  268. setModelData( model, '<media url="http://ckeditor.com"></media>' );
  269. expect( editor.getData() ).to.equal(
  270. '<figure class="media">' +
  271. '<div data-oembed-url="http://ckeditor.com">' +
  272. '<div class="ck-media__wrapper__aspect">' +
  273. '<iframe src="http://ckeditor.com"></iframe>' +
  274. '</div>' +
  275. '</div>' +
  276. '</figure>' );
  277. } );
  278. it( 'should convert (no url)', () => {
  279. setModelData( model, '<media></media>' );
  280. expect( editor.getData() ).to.equal(
  281. '<figure class="media">' +
  282. '<div>' +
  283. '<div class="ck-media__wrapper__aspect"></div>' +
  284. '</div>' +
  285. '</figure>' );
  286. } );
  287. } );
  288. describe( 'view to model', () => {
  289. it( 'should convert media figure', () => {
  290. editor.setData(
  291. '<figure class="media">' +
  292. '<div data-oembed-url="http://ckeditor.com">' +
  293. '<div class="ck-media__wrapper__aspect">' +
  294. '<iframe src="http://cksource.com"></iframe>' +
  295. '</div>' +
  296. '</div>' +
  297. '</figure>' );
  298. expect( getModelData( model, { withoutSelection: true } ) )
  299. .to.equal( '<media url="http://ckeditor.com"></media>' );
  300. } );
  301. it( 'should not convert if there is no media class', () => {
  302. editor.setData( '<figure class="quote">My quote</figure>' );
  303. expect( getModelData( model, { withoutSelection: true } ) )
  304. .to.equal( '' );
  305. } );
  306. it( 'should not convert if there is no oembed wrapper inside #1', () => {
  307. editor.setData( '<figure class="media"></figure>' );
  308. expect( getModelData( model, { withoutSelection: true } ) )
  309. .to.equal( '' );
  310. } );
  311. it( 'should not convert if there is no oembed wrapper inside #2', () => {
  312. editor.setData( '<figure class="media">test</figure>' );
  313. expect( getModelData( model, { withoutSelection: true } ) )
  314. .to.equal( '' );
  315. } );
  316. it( 'should not convert in the wrong context', () => {
  317. model.schema.register( 'div', { inheritAllFrom: '$block' } );
  318. model.schema.addChildCheck( ( ctx, childDef ) => {
  319. if ( ctx.endsWith( '$root' ) && childDef.name == 'media' ) {
  320. return false;
  321. }
  322. } );
  323. editor.conversion.elementToElement( { model: 'div', view: 'div' } );
  324. editor.setData(
  325. '<div>' +
  326. '<figure class="media">' +
  327. '<div data-oembed-url="http://ckeditor.com">' +
  328. '<div class="ck-media__wrapper__aspect">' +
  329. '<iframe src="http://cksource.com"></iframe>' +
  330. '</div>' +
  331. '</div>' +
  332. '</figure>' +
  333. '</div>' );
  334. expect( getModelData( model, { withoutSelection: true } ) )
  335. .to.equal( '<div></div>' );
  336. } );
  337. it( 'should not convert if the oembed wrapper is already consumed', () => {
  338. editor.data.upcastDispatcher.on( 'element:figure', ( evt, data, conversionApi ) => {
  339. const img = data.viewItem.getChild( 0 );
  340. conversionApi.consumable.consume( img, { name: true } );
  341. }, { priority: 'high' } );
  342. editor.setData(
  343. '<div>' +
  344. '<figure class="media">' +
  345. '<div data-oembed-url="http://ckeditor.com">' +
  346. '<div class="ck-media__wrapper__aspect">' +
  347. '<iframe src="http://cksource.com"></iframe>' +
  348. '</div>' +
  349. '</div>' +
  350. '</figure>' +
  351. '</div>' );
  352. expect( getModelData( model, { withoutSelection: true } ) )
  353. .to.equal( '' );
  354. } );
  355. it( 'should not convert if the figure is already consumed', () => {
  356. editor.data.upcastDispatcher.on( 'element:figure', ( evt, data, conversionApi ) => {
  357. conversionApi.consumable.consume( data.viewItem, { name: true, class: 'image' } );
  358. }, { priority: 'high' } );
  359. editor.setData( '<figure class="media"><div data-oembed-url="http://ckeditor.com"></div></figure>' );
  360. expect( getModelData( model, { withoutSelection: true } ) )
  361. .to.equal( '' );
  362. } );
  363. } );
  364. } );
  365. } );
  366. } );
  367. describe( 'conversion in the editing pipeline', () => {
  368. describe( 'semanticDataOutput=true', () => {
  369. beforeEach( () => {
  370. return VirtualTestEditor
  371. .create( {
  372. plugins: [ MediaEmbedEditing ],
  373. mediaEmbed: {
  374. media: mediaDefinitions,
  375. semanticDataOutput: true
  376. }
  377. } )
  378. .then( newEditor => {
  379. editor = newEditor;
  380. model = editor.model;
  381. doc = model.document;
  382. view = editor.editing.view;
  383. } );
  384. } );
  385. test();
  386. } );
  387. describe( 'semanticDataOutput=false', () => {
  388. beforeEach( () => {
  389. return VirtualTestEditor
  390. .create( {
  391. plugins: [ MediaEmbedEditing ],
  392. mediaEmbed: {
  393. media: mediaDefinitions
  394. }
  395. } )
  396. .then( newEditor => {
  397. editor = newEditor;
  398. model = editor.model;
  399. doc = model.document;
  400. view = editor.editing.view;
  401. } );
  402. } );
  403. test();
  404. } );
  405. function test() {
  406. describe( 'model to view', () => {
  407. it( 'should convert', () => {
  408. setModelData( model, '<media url="http://ckeditor.com"></media>' );
  409. expect( getViewData( view, { withoutSelection: true } ) ).to.equal(
  410. '<figure class="ck-widget media" contenteditable="false">' +
  411. '<div class="ck-media__wrapper" data-oembed-url="http://ckeditor.com">' +
  412. '<div class="ck-media__wrapper__aspect">' +
  413. '<iframe src="http://ckeditor.com"></iframe>' +
  414. '</div>' +
  415. '</div>' +
  416. '</figure>'
  417. );
  418. } );
  419. it( 'should convert the url attribute change', () => {
  420. setModelData( model, '<media url="http://ckeditor.com"></media>' );
  421. const media = doc.getRoot().getChild( 0 );
  422. model.change( writer => {
  423. writer.setAttribute( 'url', 'http://cksource.com', media );
  424. } );
  425. expect( getViewData( view, { withoutSelection: true } ) ).to.equal(
  426. '<figure class="ck-widget media" contenteditable="false">' +
  427. '<div class="ck-media__wrapper" data-oembed-url="http://cksource.com">' +
  428. '<div class="ck-media__wrapper__aspect">' +
  429. '<iframe src="http://cksource.com"></iframe>' +
  430. '</div>' +
  431. '</div>' +
  432. '</figure>'
  433. );
  434. } );
  435. it( 'should convert the url attribute removal', () => {
  436. setModelData( model, '<media url="http://ckeditor.com"></media>' );
  437. const media = doc.getRoot().getChild( 0 );
  438. model.change( writer => {
  439. writer.removeAttribute( 'url', media );
  440. } );
  441. expect( getViewData( view, { withoutSelection: true } ) )
  442. .to.equal(
  443. '<figure class="ck-widget media" contenteditable="false">' +
  444. '<div class="ck-media__wrapper">' +
  445. '<div class="ck-media__wrapper__aspect">' +
  446. '<p>No embeddable media found for given URL.</p>' +
  447. '</div>' +
  448. '</div>' +
  449. '</figure>'
  450. );
  451. } );
  452. it( 'should not convert the url attribute removal if is already consumed', () => {
  453. setModelData( model, '<media url="http://ckeditor.com"></media>' );
  454. const media = doc.getRoot().getChild( 0 );
  455. editor.editing.downcastDispatcher.on( 'attribute:url:media', ( evt, data, conversionApi ) => {
  456. conversionApi.consumable.consume( data.item, 'attribute:url' );
  457. }, { priority: 'high' } );
  458. model.change( writer => {
  459. writer.removeAttribute( 'url', media );
  460. } );
  461. expect( getViewData( view, { withoutSelection: true } ) ).to.equal(
  462. '<figure class="ck-widget media" contenteditable="false">' +
  463. '<div class="ck-media__wrapper" data-oembed-url="http://ckeditor.com">' +
  464. '<div class="ck-media__wrapper__aspect">' +
  465. '<iframe src="http://ckeditor.com"></iframe>' +
  466. '</div>' +
  467. '</div>' +
  468. '</figure>'
  469. );
  470. } );
  471. } );
  472. }
  473. } );
  474. } );
  475. function testMediaConfig( urls, expected ) {
  476. for ( const url of urls ) {
  477. editor.setData(
  478. `<figure class="media"><div data-oembed-url="${ url }"></div></figure>` );
  479. expect( getViewData( view, { withoutSelection: true } ) ).to.match(
  480. new RegExp( '<figure[^>]+>' +
  481. '<div[^>]+>' +
  482. `<div[^>]+>${ expected }</div>` +
  483. '</div>' +
  484. '</figure>' ), `assertion for "${ url }"` );
  485. }
  486. }
  487. } );