tiptapify 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/content.css +28 -0
- package/dist/tiptapify.css +1 -1
- package/dist/tiptapify.mjs +110820 -65669
- package/dist/tiptapify.umd.js +106 -89
- package/index.d.ts +34 -0
- package/package.json +3 -2
- package/src/components/ImageBubbleMenu.vue +5 -33
- package/src/components/MenuBubble.test.ts +120 -0
- package/src/components/MenuBubble.vue +124 -134
- package/src/components/MenuFloating.vue +48 -45
- package/src/components/Tiptapify.upload.test.ts +10 -0
- package/src/components/Tiptapify.vue +85 -45
- package/src/components/Toolbar/Index.vue +10 -0
- package/src/components/UI/BubbleRow.vue +41 -0
- package/src/components/UI/TiptapifyDialog.test.ts +37 -3
- package/src/components/UI/TiptapifyDialog.vue +15 -4
- package/src/components/editorExtensions.test.ts +9 -0
- package/src/components/editorExtensions.ts +21 -1
- package/src/composables/fakeUploadEndpoint.ts +65 -0
- package/src/composables/useTiptapifyConfig.ts +19 -0
- package/src/composables/useTiptapifyLinkPreview.test.ts +51 -0
- package/src/composables/useTiptapifyLinkPreview.ts +60 -0
- package/src/composables/useTiptapifyUpload.test.ts +96 -0
- package/src/composables/useTiptapifyUpload.ts +126 -0
- package/src/extensions/components/ai/Dialog.test.ts +335 -0
- package/src/extensions/components/ai/Dialog.vue +607 -310
- package/src/extensions/components/ai/backend.test.ts +1 -0
- package/src/extensions/components/ai/backend.ts +2 -1
- package/src/extensions/components/ai/context.test.ts +149 -0
- package/src/extensions/components/ai/context.ts +145 -0
- package/src/extensions/components/ai/diff.test.ts +65 -0
- package/src/extensions/components/ai/diff.ts +120 -0
- package/src/extensions/components/ai/errors.test.ts +45 -0
- package/src/extensions/components/ai/errors.ts +67 -0
- package/src/extensions/components/ai/result.test.ts +144 -0
- package/src/extensions/components/ai/result.ts +101 -0
- package/src/extensions/components/ai/thinking.test.ts +45 -0
- package/src/extensions/components/ai/thinking.ts +58 -0
- package/src/extensions/components/formatExtra/sub/Button.vue +1 -1
- package/src/extensions/components/formatExtra/sup/Button.vue +1 -1
- package/src/extensions/components/media/iframe/Button.vue +6 -7
- package/src/extensions/components/media/iframe/IframeDialog.test.ts +177 -0
- package/src/extensions/components/media/iframe/IframeDialog.vue +477 -105
- package/src/extensions/components/media/iframe/embed.test.ts +123 -0
- package/src/extensions/components/media/iframe/embed.ts +236 -0
- package/src/extensions/components/media/iframe/index.test.ts +123 -0
- package/src/extensions/components/media/iframe/index.ts +92 -10
- package/src/extensions/components/media/image/ImageDialog.vue +430 -132
- package/src/extensions/components/media/image/caption.test.ts +66 -1
- package/src/extensions/components/media/image/caption.ts +27 -1
- package/src/extensions/components/media/image/imageDialog.test.ts +200 -0
- package/src/extensions/components/media/image/imageDialog.ts +114 -0
- package/src/extensions/components/media/image/index.ts +2 -1
- package/src/extensions/components/media/link/Button.test.ts +96 -0
- package/src/extensions/components/media/link/Button.vue +6 -2
- package/src/extensions/components/media/link/LinkDialog.display.test.ts +363 -0
- package/src/extensions/components/media/link/LinkDialog.vue +687 -61
- package/src/extensions/components/media/link/index.ts +111 -6
- package/src/extensions/components/media/link/link.marks.test.ts +89 -0
- package/src/extensions/components/media/link/linkDialog.test.ts +296 -0
- package/src/extensions/components/media/link/linkDialog.ts +154 -0
- package/src/extensions/components/media/linkCard/PasteCardMenu.test.ts +133 -0
- package/src/extensions/components/media/linkCard/PasteCardMenu.vue +168 -0
- package/src/extensions/components/media/linkCard/index.test.ts +146 -0
- package/src/extensions/components/media/linkCard/index.ts +177 -0
- package/src/extensions/components/media/linkCard/linkCardDialog.test.ts +87 -0
- package/src/extensions/components/media/linkCard/linkCardDialog.ts +62 -0
- package/src/extensions/components/media/linkCard/linkPreview.test.ts +102 -0
- package/src/extensions/components/media/linkCard/linkPreview.ts +84 -0
- package/src/extensions/components/media/linkCard/makeLinkCard.test.ts +59 -0
- package/src/extensions/components/media/linkCard/makeLinkCard.ts +58 -0
- package/src/extensions/components/media/linkCard/pasteCard.test.ts +190 -0
- package/src/extensions/components/media/linkCard/pasteCard.ts +153 -0
- package/src/extensions/components/media/video/Button.vue +2 -1
- package/src/extensions/components/media/video/VideoDialog.test.ts +233 -0
- package/src/extensions/components/media/video/VideoDialog.vue +748 -59
- package/src/extensions/components/media/video/index.test.ts +154 -0
- package/src/extensions/components/media/video/index.ts +70 -5
- package/src/extensions/components/media/video/providers.test.ts +148 -0
- package/src/extensions/components/media/video/providers.ts +241 -0
- package/src/extensions/components/media/video/videoFile.test.ts +98 -0
- package/src/extensions/components/media/video/videoFile.ts +142 -0
- package/src/extensions/components/misc/fullscreen/Button.test.ts +55 -0
- package/src/extensions/components/misc/fullscreen/Button.vue +2 -2
- package/src/extensions/components/style/StyleColor.test.ts +255 -0
- package/src/extensions/components/style/StyleColor.vue +358 -208
- package/src/extensions/components/style/color/Button.vue +1 -3
- package/src/extensions/components/style/color/colors.test.ts +191 -0
- package/src/extensions/components/style/color/colors.ts +168 -0
- package/src/extensions/components/style/highlight/Button.vue +1 -3
- package/src/i18n/locales/ar.json +226 -23
- package/src/i18n/locales/bg.json +225 -22
- package/src/i18n/locales/ch.json +226 -23
- package/src/i18n/locales/cz.json +226 -23
- package/src/i18n/locales/de.json +226 -23
- package/src/i18n/locales/el.json +225 -22
- package/src/i18n/locales/en.json +225 -22
- package/src/i18n/locales/es.json +226 -23
- package/src/i18n/locales/et.json +225 -22
- package/src/i18n/locales/fi.json +226 -23
- package/src/i18n/locales/fr.json +226 -23
- package/src/i18n/locales/he.json +225 -22
- package/src/i18n/locales/hi.json +225 -22
- package/src/i18n/locales/hr.json +225 -22
- package/src/i18n/locales/hu.json +226 -23
- package/src/i18n/locales/id.json +225 -22
- package/src/i18n/locales/it.json +226 -23
- package/src/i18n/locales/ja.json +226 -23
- package/src/i18n/locales/ko.json +226 -23
- package/src/i18n/locales/la.json +226 -23
- package/src/i18n/locales/lt.json +226 -23
- package/src/i18n/locales/nl.json +226 -23
- package/src/i18n/locales/no.json +227 -24
- package/src/i18n/locales/pl.json +226 -23
- package/src/i18n/locales/pt-BR.json +225 -22
- package/src/i18n/locales/pt.json +226 -23
- package/src/i18n/locales/ro.json +225 -22
- package/src/i18n/locales/ru.json +227 -24
- package/src/i18n/locales/se.json +226 -23
- package/src/i18n/locales/sk.json +225 -22
- package/src/i18n/locales/sl.json +225 -22
- package/src/i18n/locales/sr.json +225 -22
- package/src/i18n/locales/th.json +226 -23
- package/src/i18n/locales/tr.json +226 -23
- package/src/i18n/locales/uk.json +227 -24
- package/src/i18n/locales/vi.json +226 -23
- package/src/index.ts +8 -0
- package/src/skins/registry.test.ts +1 -1
- package/src/skins/tokens.reachability.test.ts +1 -0
- package/src/skins/tokens.ts +3 -3
- package/src/types/editor.ts +40 -1
- package/src/utils/contentStyles.test.ts +116 -0
- package/src/utils/contentStyles.ts +175 -0
- package/src/utils/mediaSize.test.ts +22 -0
- package/src/utils/mediaSize.ts +27 -0
- package/src/utils/validateHref.test.ts +72 -2
- package/src/utils/validateHref.ts +57 -13
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { App, createApp, h, nextTick, provide, ref, shallowRef } from 'vue'
|
|
3
|
+
import { createVuetify } from 'vuetify'
|
|
4
|
+
import { Editor } from '@tiptap/vue-3'
|
|
5
|
+
import { Document } from '@tiptap/extension-document'
|
|
6
|
+
import { Paragraph } from '@tiptap/extension-paragraph'
|
|
7
|
+
import { Text } from '@tiptap/extension-text'
|
|
8
|
+
import { Link } from '@tiptap/extension-link'
|
|
9
|
+
import { TiptapifyLink } from '@tiptapify/extensions/components/media/link'
|
|
10
|
+
import LinkDialog from '@tiptapify/extensions/components/media/link/LinkDialog.vue'
|
|
11
|
+
import { LinkCard, LinkCardDraft } from '@tiptapify/extensions/components/media/linkCard'
|
|
12
|
+
import { resetLinkPreviewChecks } from '@tiptapify/extensions/components/media/linkCard/linkPreview'
|
|
13
|
+
import { TIPTAPIFY_LINK_PREVIEW_KEY } from '@tiptapify/composables/useTiptapifyLinkPreview'
|
|
14
|
+
|
|
15
|
+
// jsdom has no `visualViewport`; Vuetify's overlay positioning reads it
|
|
16
|
+
if (!('visualViewport' in globalThis)) {
|
|
17
|
+
;(globalThis as Record<string, unknown>).visualViewport = {
|
|
18
|
+
scale: 1,
|
|
19
|
+
offsetLeft: 0,
|
|
20
|
+
offsetTop: 0,
|
|
21
|
+
addEventListener: () => {},
|
|
22
|
+
removeEventListener: () => {},
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Nor `ResizeObserver`, which the growing description field reads
|
|
27
|
+
if (!('ResizeObserver' in globalThis)) {
|
|
28
|
+
;(globalThis as Record<string, unknown>).ResizeObserver = class {
|
|
29
|
+
observe() {}
|
|
30
|
+
unobserve() {}
|
|
31
|
+
disconnect() {}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ENDPOINT = 'https://cms.example.com/link-preview'
|
|
36
|
+
|
|
37
|
+
const page = {
|
|
38
|
+
url: 'https://news.example.com/a',
|
|
39
|
+
title: 'Talks enter a new stage',
|
|
40
|
+
description: 'The president named the terms',
|
|
41
|
+
image: 'https://cdn.example.com/og.jpg',
|
|
42
|
+
siteName: 'News',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const cardHtml = '<a class="tiptapify-link-card" data-link-card="compact" href="https://news.example.com/a" target="_blank" rel="noopener noreferrer">'
|
|
46
|
+
+ '<span class="tiptapify-link-card__body"><span class="tiptapify-link-card__title">My own title</span></span></a>'
|
|
47
|
+
|
|
48
|
+
let app: App | null = null
|
|
49
|
+
let editor: Editor | null = null
|
|
50
|
+
let answer: (url: string) => Response = () => json(page)
|
|
51
|
+
const fetchMock = vi.fn(async (input: string) => {
|
|
52
|
+
const url = new URL(input).searchParams.get('url')
|
|
53
|
+
|
|
54
|
+
return url ? answer(url) : json({ tiptapifyLinkPreview: 1 })
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
function json(body: unknown, status = 200): Response {
|
|
58
|
+
return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function pageRequests(): string[] {
|
|
62
|
+
return fetchMock.mock.calls.map(([input]) => new URL(input).searchParams.get('url')).filter(url => url !== null)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function flush() {
|
|
66
|
+
for (let i = 0; i < 10; i++) {
|
|
67
|
+
await nextTick()
|
|
68
|
+
await Promise.resolve()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type OpenOptions = { content?: string, available?: boolean, caret?: number, selectCard?: number, draft?: LinkCardDraft }
|
|
73
|
+
|
|
74
|
+
async function openDialog({ content = '<p></p>', available = true, caret = 1, selectCard, draft }: OpenOptions = {}): Promise<HTMLElement> {
|
|
75
|
+
editor = new Editor({
|
|
76
|
+
element: document.createElement('div'),
|
|
77
|
+
content,
|
|
78
|
+
extensions: [
|
|
79
|
+
Document,
|
|
80
|
+
Paragraph,
|
|
81
|
+
Text,
|
|
82
|
+
Link.configure({ openOnClick: false, enableClickSelection: true, defaultProtocol: 'https' }),
|
|
83
|
+
TiptapifyLink.configure({ openOnClick: false, defaultProtocol: 'https' }),
|
|
84
|
+
LinkCard,
|
|
85
|
+
],
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
app = createApp({
|
|
89
|
+
setup() {
|
|
90
|
+
provide('tiptapifyEditor', shallowRef(editor))
|
|
91
|
+
provide('tiptapifyI18n', {
|
|
92
|
+
t: (key: string, params?: Record<string, unknown>) => (params ? `${key} ${JSON.stringify(params)}` : key),
|
|
93
|
+
})
|
|
94
|
+
provide(TIPTAPIFY_LINK_PREVIEW_KEY, { options: ref(available ? { endpoint: ENDPOINT } : null), available: ref(available) })
|
|
95
|
+
|
|
96
|
+
return () => h(LinkDialog)
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
app.use(createVuetify())
|
|
100
|
+
app.mount(document.body.appendChild(document.createElement('div')))
|
|
101
|
+
await flush()
|
|
102
|
+
|
|
103
|
+
if (selectCard !== undefined) {
|
|
104
|
+
editor.commands.setNodeSelection(selectCard)
|
|
105
|
+
} else {
|
|
106
|
+
editor.commands.setTextSelection(caret)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (draft) {
|
|
110
|
+
editor.commands.showLinkCard(draft)
|
|
111
|
+
} else {
|
|
112
|
+
editor.commands.showLink()
|
|
113
|
+
}
|
|
114
|
+
await flush()
|
|
115
|
+
|
|
116
|
+
return document.querySelector<HTMLElement>('.tiptapify-dialog')!
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function field(card: HTMLElement, label: string): HTMLInputElement {
|
|
120
|
+
const wrapper = [...card.querySelectorAll('.v-input')].find(input => input.querySelector('label')?.textContent?.includes(label))!
|
|
121
|
+
|
|
122
|
+
return wrapper.querySelector<HTMLInputElement>('input, textarea')!
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function type(input: HTMLInputElement, value: string) {
|
|
126
|
+
input.value = value
|
|
127
|
+
input.dispatchEvent(new Event('input'))
|
|
128
|
+
await flush()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function submit(card: HTMLElement) {
|
|
132
|
+
card.querySelector('form')!.requestSubmit()
|
|
133
|
+
await flush()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function buttonWithText(card: HTMLElement, text: string): HTMLButtonElement | undefined {
|
|
137
|
+
return [...card.querySelectorAll('button')].find(button => button.textContent?.includes(text))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function showAs(card: HTMLElement, display: 'link' | 'card') {
|
|
141
|
+
buttonWithText(card, `dialog.linkCard.display_${display}`)!.click()
|
|
142
|
+
await flush()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
beforeEach(() => {
|
|
146
|
+
answer = () => json(page)
|
|
147
|
+
fetchMock.mockClear()
|
|
148
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
afterEach(() => {
|
|
152
|
+
app?.unmount()
|
|
153
|
+
editor?.destroy()
|
|
154
|
+
app = null
|
|
155
|
+
editor = null
|
|
156
|
+
document.body.innerHTML = ''
|
|
157
|
+
vi.useRealTimers()
|
|
158
|
+
vi.unstubAllGlobals()
|
|
159
|
+
resetLinkPreviewChecks()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe('link dialog display', () => {
|
|
163
|
+
it('offers no card without the link preview endpoint', async () => {
|
|
164
|
+
const card = await openDialog({ available: false })
|
|
165
|
+
|
|
166
|
+
expect(buttonWithText(card, 'dialog.linkCard.display_card')).toBeUndefined()
|
|
167
|
+
expect(card.textContent).toContain('dialog.link.text')
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('opens a draft address as a link without the endpoint, asking nothing', async () => {
|
|
171
|
+
const card = await openDialog({ available: false, draft: { href: page.url } })
|
|
172
|
+
|
|
173
|
+
expect(card.textContent).toContain('dialog.link.title_insert')
|
|
174
|
+
expect(buttonWithText(card, 'dialog.linkCard.display_card')).toBeUndefined()
|
|
175
|
+
expect(field(card, 'dialog.link.href').value).toBe(page.url)
|
|
176
|
+
expect(fetchMock).not.toHaveBeenCalled()
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('inserts a card of the page in place of the empty paragraph, asked on submit right after typing', async () => {
|
|
180
|
+
const card = await openDialog({ content: '<p>Before</p><p></p>', caret: 9 })
|
|
181
|
+
|
|
182
|
+
await showAs(card, 'card')
|
|
183
|
+
|
|
184
|
+
// Text, rel and class do not apply to a card
|
|
185
|
+
expect(card.textContent).toContain('dialog.linkCard.title_insert')
|
|
186
|
+
expect(card.textContent).not.toContain('dialog.link.text')
|
|
187
|
+
expect(card.textContent).not.toContain('dialog.link.advanced')
|
|
188
|
+
|
|
189
|
+
await type(field(card, 'dialog.link.href'), page.url)
|
|
190
|
+
await submit(card)
|
|
191
|
+
|
|
192
|
+
await vi.waitFor(() => expect(editor!.state.doc.child(1).type.name).toBe('linkCard'))
|
|
193
|
+
expect(pageRequests()).toEqual([page.url])
|
|
194
|
+
expect(editor!.state.doc.childCount).toBe(2)
|
|
195
|
+
expect(editor!.state.doc.child(1).attrs).toMatchObject({ href: page.url, title: page.title, siteName: page.siteName, newTab: false })
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('fetches the page of a draft address at once and inserts its card with the chosen layout', async () => {
|
|
199
|
+
const card = await openDialog({ draft: { href: page.url, newTab: true } })
|
|
200
|
+
|
|
201
|
+
expect(card.textContent).toContain('dialog.linkCard.title_insert')
|
|
202
|
+
await vi.waitFor(() => expect(field(card, 'dialog.linkCard.page_title').value).toBe(page.title))
|
|
203
|
+
expect(pageRequests()).toEqual([page.url])
|
|
204
|
+
expect(field(card, 'dialog.linkCard.site_name').value).toBe(page.siteName)
|
|
205
|
+
expect(card.querySelector('.tiptapify-link-card-fields__image img')!.getAttribute('src')).toBe(page.image)
|
|
206
|
+
|
|
207
|
+
buttonWithText(card, 'dialog.linkCard.layout_large')!.click()
|
|
208
|
+
await flush()
|
|
209
|
+
await submit(card)
|
|
210
|
+
|
|
211
|
+
expect(editor!.state.doc.firstChild!.type.name).toBe('linkCard')
|
|
212
|
+
expect(editor!.state.doc.firstChild!.attrs).toEqual({
|
|
213
|
+
href: page.url,
|
|
214
|
+
title: page.title,
|
|
215
|
+
description: page.description,
|
|
216
|
+
image: page.image,
|
|
217
|
+
siteName: page.siteName,
|
|
218
|
+
layout: 'large',
|
|
219
|
+
newTab: true,
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('fetches a typed address once the typing stops, and never one without a protocol', async () => {
|
|
224
|
+
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
|
|
225
|
+
const card = await openDialog()
|
|
226
|
+
await showAs(card, 'card')
|
|
227
|
+
const href = field(card, 'dialog.link.href')
|
|
228
|
+
|
|
229
|
+
await type(href, 'news.example.com/a')
|
|
230
|
+
expect(card.textContent).toContain('dialog.link.protocol_missing')
|
|
231
|
+
await vi.advanceTimersByTimeAsync(500)
|
|
232
|
+
expect(pageRequests()).toEqual([])
|
|
233
|
+
|
|
234
|
+
await submit(card)
|
|
235
|
+
expect(pageRequests()).toEqual([])
|
|
236
|
+
|
|
237
|
+
await type(href, 'https://news.example.com/b')
|
|
238
|
+
await type(href, page.url)
|
|
239
|
+
await vi.advanceTimersByTimeAsync(500)
|
|
240
|
+
await flush()
|
|
241
|
+
|
|
242
|
+
expect(pageRequests()).toEqual([page.url])
|
|
243
|
+
expect(field(card, 'dialog.linkCard.page_title').value).toBe(page.title)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('needs an http(s) address for a card', async () => {
|
|
247
|
+
const card = await openDialog()
|
|
248
|
+
await showAs(card, 'card')
|
|
249
|
+
|
|
250
|
+
await type(field(card, 'dialog.link.href'), '/page')
|
|
251
|
+
await submit(card)
|
|
252
|
+
|
|
253
|
+
expect(card.textContent).toContain('dialog.linkCard.protocol_missing')
|
|
254
|
+
expect(pageRequests()).toEqual([])
|
|
255
|
+
expect(editor!.getHTML()).toBe('<p></p>')
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('shows the error of the endpoint under the address and inserts the fields filled by hand', async () => {
|
|
259
|
+
answer = () => json({ error: 'The page answered 404' }, 422)
|
|
260
|
+
const card = await openDialog()
|
|
261
|
+
await showAs(card, 'card')
|
|
262
|
+
|
|
263
|
+
await type(field(card, 'dialog.link.href'), page.url)
|
|
264
|
+
await submit(card)
|
|
265
|
+
|
|
266
|
+
await vi.waitFor(() => expect(card.textContent).toContain('dialog.linkCard.fetch_failed {"error":"The page answered 404"}'))
|
|
267
|
+
expect(editor!.getHTML()).toBe('<p></p>')
|
|
268
|
+
|
|
269
|
+
await type(field(card, 'dialog.linkCard.page_title'), 'By hand')
|
|
270
|
+
await submit(card)
|
|
271
|
+
|
|
272
|
+
expect(pageRequests()).toEqual([page.url])
|
|
273
|
+
expect(editor!.state.doc.firstChild!.attrs).toMatchObject({ href: page.url, title: 'By hand', image: null })
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('converts the link being edited into a card', async () => {
|
|
277
|
+
const card = await openDialog({ content: '<p><a href="https://news.example.com/a">Page</a></p>', caret: 2 })
|
|
278
|
+
|
|
279
|
+
expect(card.textContent).toContain('dialog.link.title_edit')
|
|
280
|
+
expect(buttonWithText(card, 'dialog.link.remove')).toBeDefined()
|
|
281
|
+
|
|
282
|
+
await showAs(card, 'card')
|
|
283
|
+
expect(card.textContent).toContain('dialog.linkCard.title_edit')
|
|
284
|
+
await vi.waitFor(() => expect(field(card, 'dialog.linkCard.page_title').value).toBe(page.title))
|
|
285
|
+
await submit(card)
|
|
286
|
+
|
|
287
|
+
expect(editor!.state.doc.childCount).toBe(1)
|
|
288
|
+
expect(editor!.state.doc.firstChild!.type.name).toBe('linkCard')
|
|
289
|
+
expect(editor!.state.doc.firstChild!.attrs).toMatchObject({ href: page.url, title: page.title })
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('edits the selected card without fetching on open; the data is fetched again only by Refresh', async () => {
|
|
293
|
+
const card = await openDialog({ content: cardHtml, selectCard: 0 })
|
|
294
|
+
|
|
295
|
+
expect(card.textContent).toContain('dialog.linkCard.title_edit')
|
|
296
|
+
expect(buttonWithText(card, 'dialog.remove')).toBeDefined()
|
|
297
|
+
expect(pageRequests()).toEqual([])
|
|
298
|
+
expect(field(card, 'dialog.linkCard.page_title').value).toBe('My own title')
|
|
299
|
+
|
|
300
|
+
card.querySelector<HTMLButtonElement>('[aria-label="dialog.linkCard.refresh"]')!.click()
|
|
301
|
+
await vi.waitFor(() => expect(field(card, 'dialog.linkCard.page_title').value).toBe(page.title))
|
|
302
|
+
expect(pageRequests()).toEqual([page.url])
|
|
303
|
+
|
|
304
|
+
buttonWithText(card, 'dialog.linkCard.layout_text')!.click()
|
|
305
|
+
await flush()
|
|
306
|
+
await submit(card)
|
|
307
|
+
|
|
308
|
+
expect(editor!.state.doc.childCount).toBe(1)
|
|
309
|
+
expect(editor!.state.doc.firstChild!.attrs).toMatchObject({ title: page.title, layout: 'text', image: page.image, newTab: true })
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('edits a card by hand without the endpoint', async () => {
|
|
313
|
+
const card = await openDialog({ content: cardHtml, available: false, selectCard: 0 })
|
|
314
|
+
|
|
315
|
+
expect(card.querySelector('[aria-label="dialog.linkCard.refresh"]')).toBeNull()
|
|
316
|
+
// The card can still be shown as a link
|
|
317
|
+
expect(buttonWithText(card, 'dialog.linkCard.display_link')).toBeDefined()
|
|
318
|
+
|
|
319
|
+
await type(field(card, 'dialog.linkCard.description'), 'Written by hand')
|
|
320
|
+
await submit(card)
|
|
321
|
+
|
|
322
|
+
expect(fetchMock).not.toHaveBeenCalled()
|
|
323
|
+
expect(editor!.state.doc.firstChild!.attrs).toMatchObject({ title: 'My own title', description: 'Written by hand' })
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('turns the selected card back into a link with its title as the text', async () => {
|
|
327
|
+
const card = await openDialog({ content: cardHtml, available: false, selectCard: 0 })
|
|
328
|
+
|
|
329
|
+
await showAs(card, 'link')
|
|
330
|
+
expect(card.textContent).toContain('dialog.link.title_edit')
|
|
331
|
+
await submit(card)
|
|
332
|
+
|
|
333
|
+
expect(editor!.getHTML()).toBe(
|
|
334
|
+
'<p><a target="_blank" rel="noopener noreferrer" href="https://news.example.com/a">My own title</a></p>',
|
|
335
|
+
)
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('removes the selected card', async () => {
|
|
339
|
+
const card = await openDialog({ content: `${cardHtml}<p>Text</p>`, selectCard: 0 })
|
|
340
|
+
|
|
341
|
+
buttonWithText(card, 'dialog.remove')!.click()
|
|
342
|
+
await flush()
|
|
343
|
+
|
|
344
|
+
expect(editor!.getHTML()).toBe('<p>Text</p>')
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it('embeds a picked image as base64 without the upload and removes it', async () => {
|
|
348
|
+
const card = await openDialog({ content: cardHtml, selectCard: 0 })
|
|
349
|
+
|
|
350
|
+
const input = card.querySelector<HTMLInputElement>('input[accept="image/*"]')!
|
|
351
|
+
Object.defineProperty(input, 'files', { value: [new File(['x'], 'og.png', { type: 'image/png' })], configurable: true })
|
|
352
|
+
input.dispatchEvent(new Event('change'))
|
|
353
|
+
await vi.waitFor(() => expect(card.querySelector('.tiptapify-link-card-fields__image img')).not.toBeNull())
|
|
354
|
+
|
|
355
|
+
expect(card.querySelector('.tiptapify-link-card-fields__image img')!.getAttribute('src')).toMatch(/^data:image\/png;base64,/)
|
|
356
|
+
|
|
357
|
+
buttonWithText(card, 'dialog.linkCard.image_remove')!.click()
|
|
358
|
+
await flush()
|
|
359
|
+
await submit(card)
|
|
360
|
+
|
|
361
|
+
expect(editor!.state.doc.firstChild!.attrs.image).toBeNull()
|
|
362
|
+
})
|
|
363
|
+
})
|