|
|
@@ -73,50 +73,6 @@ async function typeDraft(page: Page, rows: readonly string[]): Promise<void> {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * Replace the draft with one pasted block through the composer's own paste
|
|
|
- * path (the keymap's ClipboardEvent route).
|
|
|
- * @param page - the page under test.
|
|
|
- * @param rows - draft lines, joined with '\n' — one paste, not one keystroke each.
|
|
|
- */
|
|
|
-async function pasteDraft(page: Page, rows: readonly string[]): Promise<void> {
|
|
|
- await typeDraft(page, [])
|
|
|
- await surface(page).evaluate((el, text) => {
|
|
|
- const data = new DataTransfer()
|
|
|
- data.setData('text/plain', text)
|
|
|
- el.dispatchEvent(new ClipboardEvent('paste', { clipboardData: data, bubbles: true, cancelable: true }))
|
|
|
- }, rows.join('\n'))
|
|
|
-}
|
|
|
-
|
|
|
-/** The composer's scrollport alone, for measurements taken after an edit removed the marked lines. */
|
|
|
-interface ScrollportMetrics {
|
|
|
- /** True when the draft is taller than the capped box. */
|
|
|
- overflows: boolean
|
|
|
- /** The composer's one scroll offset. */
|
|
|
- scrollTop: number
|
|
|
- /** Furthest that offset can go. */
|
|
|
- scrollMax: number
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * Measure the composer's scrollport without addressing the draft's marked lines.
|
|
|
- * @param page - the page under test.
|
|
|
- * @returns the offset, its maximum, and whether the draft overflows.
|
|
|
- */
|
|
|
-function measureScrollport(page: Page): Promise<ScrollportMetrics> {
|
|
|
- return page.evaluate(() => {
|
|
|
- const input = document.querySelector<HTMLElement>('[data-composer-input][contenteditable="true"]')
|
|
|
- if (input === null) throw new Error('no live composer surface in the DOM')
|
|
|
- const scroll = input.closest<HTMLElement>('[data-input-scroll]')
|
|
|
- if (scroll === null) throw new Error('the composer surface is not inside a draft scrollport')
|
|
|
- return {
|
|
|
- overflows: scroll.scrollHeight > scroll.clientHeight,
|
|
|
- scrollTop: scroll.scrollTop,
|
|
|
- scrollMax: scroll.scrollHeight - scroll.clientHeight,
|
|
|
- }
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
/** The composer's scroll surface as the browser lays it out. */
|
|
|
interface ComposerMetrics {
|
|
|
/** True when the draft is taller than the capped box — the situation under test. */
|
|
|
@@ -332,54 +288,6 @@ describe('web e2e: composer draft scrolling', () => {
|
|
|
expect(tripwire.pageErrors).toEqual([])
|
|
|
}, 60_000)
|
|
|
|
|
|
- it('deleting a pasted draft\'s trailing lines keeps the box at the end', async () => {
|
|
|
- onTestFailed(() => saveFailureShot(page, 'web-e2e-composer-draft-scroll-paste-delete'))
|
|
|
- // A pasted block is where a draft's structure and a typed one diverge: a
|
|
|
- // paste that keeps its newlines inside one text node leaves the browser
|
|
|
- // revealing the caret from a rect that spans the whole block, which jumps
|
|
|
- // the capped box back to the draft's head on the next edit at its end.
|
|
|
- await pasteDraft(page, [...DRAFT_ROWS, '', '', ''])
|
|
|
- await expect.poll(async () => (await measureScrollport(page)).overflows, { timeout: 10_000 }).toBe(true)
|
|
|
- await surface(page).hover()
|
|
|
- await page.mouse.wheel(0, 4000)
|
|
|
- await expect.poll(async () => {
|
|
|
- const m = await measureScrollport(page)
|
|
|
- return m.scrollTop === m.scrollMax
|
|
|
- }, { timeout: 10_000 }).toBe(true)
|
|
|
- // The caret at the draft's end, then the trailing lines removed one at a
|
|
|
- // time; the box must stay at the end, where the caret is. Select-all plus
|
|
|
- // ArrowRight lands the caret on the document end; the collapse has to be
|
|
|
- // observable before Backspace, or the whole selection is what gets deleted.
|
|
|
- const input = surface(page)
|
|
|
- await input.click()
|
|
|
- await page.keyboard.press('ControlOrMeta+KeyA')
|
|
|
- await page.keyboard.press('ArrowRight')
|
|
|
- await expect.poll(
|
|
|
- async () => page.evaluate(() => window.getSelection()?.isCollapsed ?? false),
|
|
|
- { timeout: 10_000 },
|
|
|
- ).toBe(true)
|
|
|
- await page.keyboard.press('Backspace')
|
|
|
- await expect.poll(async () => (await measureScrollport(page)).overflows, { timeout: 10_000 }).toBe(true)
|
|
|
- await expect.poll(async () => {
|
|
|
- const m = await measureScrollport(page)
|
|
|
- return m.scrollTop === m.scrollMax
|
|
|
- }, { timeout: 10_000 }).toBe(true)
|
|
|
- // The box is at its furthest offset rather than back at the draft's head.
|
|
|
- const afterBackspace = await measureScrollport(page)
|
|
|
- expect(afterBackspace.scrollMax).toBeGreaterThan(0)
|
|
|
- expect(afterBackspace.scrollTop).toBe(afterBackspace.scrollMax)
|
|
|
- // The same failure through the selection gesture: select the trailing
|
|
|
- // lines and delete them at once.
|
|
|
- await page.keyboard.press('Shift+ArrowUp')
|
|
|
- await page.keyboard.press('Shift+ArrowUp')
|
|
|
- await page.keyboard.press('Delete')
|
|
|
- await expect.poll(async () => {
|
|
|
- const m = await measureScrollport(page)
|
|
|
- return m.overflows && m.scrollTop === m.scrollMax
|
|
|
- }, { timeout: 10_000 }).toBe(true)
|
|
|
- expect(tripwire.pageErrors).toEqual([])
|
|
|
- }, 60_000)
|
|
|
-
|
|
|
it('matches the committed composer scroll geometry golden', async () => {
|
|
|
onTestFailed(() => saveFailureShot(page, 'web-e2e-composer-draft-scroll-golden'))
|
|
|
// Restore the pristine draft (the edit cases appended to it) and return to
|