feat(editor): link insertion full UI and flow

This commit is contained in:
Florian Sylvain committed 2026-10-12 17:12:36 +02:00
1 parent 03b39e64f2
commit 6f9ae6207a
16 files changed
+2865 -14

No files matched your search

+217 -1
View File
@@ -396,6 +396,63 @@ async function markVeryLongDirty(page: Page) {
await focusBoldSample(page)
}
const linkLongUrl =
'https://www.example.org/planning/guidance/listed-buildings/windows-and-doors/…/2026-update.pdf'
const linkSentence =
'<p>We also check the <a href="/articles/winter-checklist" target="_blank" rel="noopener noreferrer">timber windows</a> before any tiling starts.</p>'
const linkedBody = bathroomBody.replace('</p><h2>', `</p>${linkSentence}<h2>`)
const linkImageSrc = '/static/uploadedImages/blank.svg'
const linkedImageBody = `${linkedBody}<img src="${linkImageSrc}" alt="" data-image-id="1">`
const MOBILE_MAX_WIDTH = 640
const TOOLTIP_SETTLE_MS = 700
function linkedApi(body = linkedBody) {
return editorApi({
'GET /v1/article/12': { status: 200, body: { ...editorArticle, body } },
})
}
function isNarrow(page: Page) {
return (page.viewportSize()?.width ?? MOBILE_MAX_WIDTH) < MOBILE_MAX_WIDTH
}
function linkButton(page: Page) {
return page.getByRole('button', { name: 'Insert link', exact: true }).first()
}
function linkDialog(page: Page) {
return page.locator('[data-presentation]')
}
async function typeIntoLinkField(page: Page, label: string, value: string) {
await linkDialog(page).getByLabel(label, { exact: true }).fill(value)
}
async function openLinkDialogOnWord(page: Page) {
await markDirty(page)
await page.getByText('choose yourself').dblclick()
await page.keyboard.press('Control+k')
await linkDialog(page).waitFor()
}
async function openLinkDialogOnLink(page: Page) {
await markDirty(page)
await page.locator('.article-content a').click()
await page.getByRole('button', { name: 'Edit', exact: true }).click()
await linkDialog(page).waitFor()
}
async function dirtyWithLinks(page: Page) {
await focusBoldSample(page)
await markDirty(page)
await focusBoldSample(page)
}
async function useTouchPointer(page: Page) {
const session = await page.context().newCDPSession(page)
await session.send('Emulation.setTouchEmulationEnabled', { enabled: true, maxTouchPoints: 1 })
}
function mib(value: number) {
return Math.round(value * 1024 * 1024)
}
@@ -4946,7 +5003,7 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
steps: async (page) => {
await markDirty(page)
await focusBoldSample(page)
await page.getByRole('button', { name: 'Back to Articles' }).click()
await page.locator('[aria-label="Back to Articles"]:visible').click()
await page.getByRole('alertdialog').waitFor()
},
},
@@ -4975,6 +5032,165 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
'GET /v1/article/9999': { status: 404 },
},
},
'Link: create from selection': {
path: '/article/12/edit',
api: editorApi(),
steps: async (page) => {
await openLinkDialogOnWord(page)
await typeIntoLinkField(page, 'Link text', 'timber windows')
await linkDialog(page).getByLabel('URL', { exact: true }).focus()
},
},
'Link: create with no selection': {
path: '/article/12/edit',
api: editorApi(),
steps: async (page) => {
await markDirty(page)
await page.keyboard.press('Control+k')
await linkDialog(page).waitFor()
},
},
'Link: edit existing': {
path: '/article/12/edit',
api: linkedApi(),
steps: openLinkDialogOnLink,
},
'Link: invalid URL': {
path: '/article/12/edit',
api: editorApi(),
steps: async (page) => {
await openLinkDialogOnWord(page)
await typeIntoLinkField(page, 'Link text', 'timber windows')
await typeIntoLinkField(page, 'URL', 'www.example')
await page.keyboard.press('Tab')
await linkDialog(page).getByText('Enter a valid URL').waitFor()
await linkDialog(page).getByText('Links open in the same tab').click()
},
},
'Link: empty text': {
path: '/article/12/edit',
api: editorApi(),
steps: async (page) => {
await markDirty(page)
await page.keyboard.press('Control+k')
await linkDialog(page).waitFor()
await typeIntoLinkField(page, 'URL', '#measuring')
await linkDialog(page).getByRole('button', { name: 'Insert link' }).click()
await linkDialog(page).getByText('Enter the link text.').waitFor()
await linkDialog(page).getByLabel('URL', { exact: true }).focus()
await linkDialog(page).getByText('Links open in the same tab').click()
},
},
'Link: long URL': {
path: '/article/12/edit',
api: linkedApi(),
steps: async (page) => {
await openLinkDialogOnLink(page)
await typeIntoLinkField(page, 'Link text', 'planning guidance')
await typeIntoLinkField(page, 'URL', linkLongUrl)
await linkDialog(page).getByLabel('URL', { exact: true }).focus()
},
},
'Link button: default': {
path: '/article/12/edit',
api: linkedApi(),
steps: dirtyWithLinks,
},
'Link button: hover and tooltip': {
path: '/article/12/edit',
api: linkedApi(),
steps: async (page) => {
await dirtyWithLinks(page)
await linkButton(page).hover()
await page.waitForTimeout(TOOLTIP_SETTLE_MS)
},
},
'Link button: focus-visible': {
path: '/article/12/edit',
api: linkedApi(),
steps: async (page) => {
await focusBoldSample(page)
await markDirty(page)
await linkButton(page).focus()
},
},
'Link button: active (cursor in a link)': {
path: '/article/12/edit',
api: linkedApi(),
steps: async (page) => {
await dirtyWithLinks(page)
await page.locator('.article-content a').click()
await page.keyboard.press('Escape')
},
},
'Link button: disabled': {
path: '/article/12/edit',
api: linkedApi(linkedImageBody),
steps: async (page) => {
await dirtyWithLinks(page)
await page.getByTestId('article-editor-image').click({ force: true })
await page.addStyleTag({
content: '[data-testid="article-editor-image"] { display: none; }',
})
},
},
'Link popover and sheet': {
path: '/article/12/edit',
api: linkedApi(),
steps: async (page) => {
await dirtyWithLinks(page)
if (isNarrow(page)) {
await useTouchPointer(page)
}
await page.locator('.article-content a').click()
await page.locator('[aria-label="Link"]').waitFor()
},
},
'Rendered link: default': {
path: '/article/12/edit',
api: linkedApi(),
steps: dirtyWithLinks,
},
'Rendered link: hover': {
path: '/article/12/edit',
api: linkedApi(),
steps: async (page) => {
await dirtyWithLinks(page)
await page.locator('.article-content a').hover()
},
},
'Rendered link: focus': {
path: '/article/12/edit',
api: linkedApi(),
steps: async (page) => {
await markDirty(page)
await page.getByText('before any tiling starts').click()
await page.keyboard.press('Home')
for (let step = 0; step < 21; step += 1) {
await page.keyboard.press('ArrowRight')
}
},
},
'Rendered link: selected': {
path: '/article/12/edit',
api: linkedApi(),
steps: async (page) => {
await dirtyWithLinks(page)
await page.locator('.article-content a').selectText()
},
},
'Toolbar overflow (390 px)': {
path: '/article/12/edit',
api: linkedApi(),
steps: async (page) => {
await dirtyWithLinks(page)
if (isNarrow(page)) {
await page
.locator('.article-toolbar')
.evaluate((toolbar) => toolbar.scrollTo({ left: toolbar.scrollWidth }))
}
},
},
'Upload: choose': {
path: '/article/12/edit',
now: editorNow,