Files
upstream-mirror 083a46e93b
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
snapshot of https://github.com/ueberdosis/tiptap.git (main, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:11:02 +05:00

333 lines
9.6 KiB
TypeScript

import { Editor } from '@tiptap/core'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import Youtube from '@tiptap/extension-youtube'
import { describe, expect, it } from 'vite-plus/test'
describe('extension-youtube', () => {
const editorElClass = 'tiptap'
let editor: Editor | null = null
const createEditorEl = () => {
const editorEl = document.createElement('div')
editorEl.classList.add(editorElClass)
document.body.appendChild(editorEl)
return editorEl
}
const getEditorEl = () => document.querySelector(`.${editorElClass}`)
const invalidUrls = [
// We have to disable the oxlint rule here because we're trying to purposely test eval urls
// oxlint-disable-next-line no-script-url
'javascript:alert(window.origin)//embed/',
'https://youtube.google.com/embed/fdsafsdf',
'https://youtube.com.bad/embed',
'https://www.youtu0be/embed',
]
invalidUrls.forEach(url => {
it(`does not output html for javascript schema or non-youtube links for url ${url}`, () => {
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, Youtube],
content: {
type: 'doc',
content: [
{
type: 'youtube',
attrs: {
src: url,
},
},
],
},
})
expect(editor.getHTML()).not.toContain(url)
editor?.destroy()
getEditorEl()?.remove()
})
})
it('when nocookie youtube url is passed, still outputs html with iframe with the url', () => {
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, Youtube],
content: {
type: 'doc',
content: [
{
type: 'youtube',
attrs: {
src: 'https://www.youtube-nocookie.com/embed/testvideoid',
},
},
],
},
})
expect(editor.getHTML()).toContain('https://www.youtube-nocookie.com/embed/testvideoid')
editor?.destroy()
getEditorEl()?.remove()
})
it.each([
[
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
42,
],
['https://youtu.be/dQw4w9WgXcQ', 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', 0],
[
'https://www.youtube.com/playlist?list=PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf',
'https://www.youtube.com/playlist?list=PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf',
0,
],
])(
'preserves canonical youtube attrs when content is loaded back from rendered HTML for %s',
(originalSrc, expectedSrc, start) => {
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, Youtube],
content: {
type: 'doc',
content: [
{
type: 'youtube',
attrs: {
src: originalSrc,
start,
width: 720,
height: 405,
},
},
],
},
})
const html = editor.getHTML()
editor.destroy()
getEditorEl()?.remove()
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, Youtube],
content: html,
})
expect(editor.getJSON()).toMatchObject({
type: 'doc',
content: [
{
type: 'youtube',
attrs: {
src: expectedSrc,
start,
width: 720,
height: 405,
},
},
],
})
editor?.destroy()
getEditorEl()?.remove()
},
)
describe('iframe dimension handling', () => {
it('preserves percentage width and height when parsing HTML', () => {
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, Youtube],
content:
'<div data-youtube-video><iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ" width="100%" height="56.25%"></iframe></div>',
})
const attrs = editor.getJSON().content?.[0]?.attrs ?? {}
expect(attrs.width).toBe('100%')
expect(attrs.height).toBe('56.25%')
editor?.destroy()
getEditorEl()?.remove()
})
it('keeps percentage dimensions when content is loaded back from rendered HTML', () => {
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, Youtube],
content: {
type: 'doc',
content: [
{
type: 'youtube',
attrs: {
src: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
width: '100%',
height: 480,
},
},
],
},
})
const html = editor.getHTML()
expect(html).toContain('width="100%"')
editor.destroy()
getEditorEl()?.remove()
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, Youtube],
content: html,
})
const attrs = editor.getJSON().content?.[0]?.attrs ?? {}
expect(attrs.width).toBe('100%')
expect(attrs.height).toBe(480)
editor?.destroy()
getEditorEl()?.remove()
})
it('still parses plain numeric dimensions as numbers', () => {
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, Youtube],
content:
'<div data-youtube-video><iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ" width="720" height="405"></iframe></div>',
})
const attrs = editor.getJSON().content?.[0]?.attrs ?? {}
expect(attrs.width).toBe(720)
expect(attrs.height).toBe(405)
editor?.destroy()
getEditorEl()?.remove()
})
it.each([
['100px', '200px'],
['20rem', '10em'],
['50vw', '30vh'],
])('preserves unit-based dimensions like %s when parsing HTML', (width, height) => {
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, Youtube],
content: `<div data-youtube-video><iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ" width="${width}" height="${height}"></iframe></div>`,
})
const attrs = editor.getJSON().content?.[0]?.attrs ?? {}
expect(attrs.width).toBe(width)
expect(attrs.height).toBe(height)
editor?.destroy()
getEditorEl()?.remove()
})
it('preserves non-numeric strings instead of falling back to the default dimensions', () => {
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, Youtube],
content:
'<div data-youtube-video><iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ" width="not-a-size" height="auto"></iframe></div>',
})
const attrs = editor.getJSON().content?.[0]?.attrs ?? {}
expect(attrs.width).toBe('not-a-size')
expect(attrs.height).toBe('auto')
editor?.destroy()
getEditorEl()?.remove()
})
it('accepts a percentage width in the setYoutubeVideo command', () => {
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, Youtube],
})
editor.commands.setYoutubeVideo({
src: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
width: '100%',
height: 405,
})
const attrs = editor.getJSON().content?.[0]?.attrs ?? {}
expect(attrs.width).toBe('100%')
expect(attrs.height).toBe(405)
editor?.destroy()
getEditorEl()?.remove()
})
})
it('does not persist NaN width or height when parsing iframe dimensions from HTML', () => {
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, Youtube],
content:
'<div data-youtube-video><iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ" width="100%" height="auto"></iframe></div>',
})
const attrs = editor.getJSON().content?.[0]?.attrs ?? {}
const html = editor.getHTML()
expect(Number.isNaN(attrs.width)).toBe(false)
expect(Number.isNaN(attrs.height)).toBe(false)
expect(html).not.toContain('width="NaN"')
expect(html).not.toContain('height="NaN"')
editor?.destroy()
getEditorEl()?.remove()
})
describe('missing src attribute', () => {
// The `src` attribute is declared with `default: null` and its `parseHTML` returns
// `undefined` when the iframe carries no `src`, so a node with a null `src` is a
// legitimate parse result rather than a malformed document.
const iframeWithoutSrc =
'<div data-youtube-video=""><iframe class="w-full aspect-video" width="640" height="480" allowfullscreen="true" start="0"></iframe></div>'
it('refuses setYoutubeVideo with a missing src instead of throwing', () => {
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, Youtube],
})
expect(editor.commands.setYoutubeVideo({ src: null as unknown as string })).toBe(false)
editor?.destroy()
getEditorEl()?.remove()
})
it('parses an iframe without a src attribute without throwing', () => {
expect(() => {
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, Youtube],
content: iframeWithoutSrc,
})
}).not.toThrow()
expect(editor?.getHTML()).not.toContain('src=')
editor?.destroy()
getEditorEl()?.remove()
})
})
})