feat(editor): link insertion full UI and flow
This commit is contained in:
1 parent
03b39e64f2
commit
6f9ae6207a
16 files changed
+2865
-14
No files matched your search
+217
-1
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user