|
| 1 | +import type { Page } from '@playwright/test'; |
| 2 | +import { expect, test } from '../fixtures/muya'; |
| 3 | +import { getMarkdown } from '../helpers/api'; |
| 4 | +import { editor, floats } from '../helpers/selectors'; |
| 5 | + |
| 6 | +/** |
| 7 | + * Both resize handles must track the pointer 1:1 (#5392). |
| 8 | + * |
| 9 | + * A left-aligned image grows on its right side, so its right edge is not an |
| 10 | + * invariant the left handle can measure against: reading it live once per |
| 11 | + * mousemove made every event re-add the whole accumulated delta, so a 60 px |
| 12 | + * drag grew the image by 210 px and kept the pointer far behind the handle. |
| 13 | + * |
| 14 | + * The fixture is 400×300 on purpose. `usesDefaultObjectSize` pins an inline |
| 15 | + * `style="width"` on any image measuring 300 wide or 150 tall (#4991), and |
| 16 | + * that style beats the `width` attribute the bar writes, so such an image |
| 17 | + * never resizes at all. 400 also leaves room inside the editor column for the |
| 18 | + * drags below, so no `max-width: 100%` clamp interferes. |
| 19 | + */ |
| 20 | +const IMAGE = `data:image/svg+xml;base64,${btoa( |
| 21 | + '<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300"><rect width="400" height="300" fill="#48c"/></svg>', |
| 22 | +)}`; |
| 23 | + |
| 24 | +const DRAG = 60; |
| 25 | +// Only sub-pixel geometry and the integer truncation of the written width |
| 26 | +// separate the two; the accumulation bug missed by 150 px on this drag. |
| 27 | +const TOLERANCE = 4; |
| 28 | + |
| 29 | +async function showHandles(page: Page) { |
| 30 | + await page.evaluate(uri => window.muya!.setContent(``), IMAGE); |
| 31 | + |
| 32 | + const image = page.locator(editor.image).first(); |
| 33 | + await expect(image).toBeVisible(); |
| 34 | + await expect.poll(async () => image.evaluate(el => |
| 35 | + el.classList.contains('mu-image-success')), { timeout: 5_000 }).toBe(true); |
| 36 | + |
| 37 | + const innerImg = image.locator('img').first(); |
| 38 | + await expect(innerImg).toBeVisible(); |
| 39 | + await innerImg.click(); |
| 40 | + await expect(page.locator(floats.imageTransformerHandle)).toHaveCount(2); |
| 41 | + |
| 42 | + const box = await innerImg.boundingBox(); |
| 43 | + if (!box) |
| 44 | + throw new Error('image has no bounding box'); |
| 45 | + |
| 46 | + return { startWidth: Math.round(box.width) }; |
| 47 | +} |
| 48 | + |
| 49 | +/** Drags one handle `dx` px and reports the width it saved and the width it shows. */ |
| 50 | +async function dragHandle(page: Page, position: 'left' | 'right', dx: number) { |
| 51 | + const box = await page.locator(`${floats.imageTransformer} .bar.${position}`).boundingBox(); |
| 52 | + if (!box) |
| 53 | + throw new Error(`${position} handle has no bounding box`); |
| 54 | + |
| 55 | + const cx = box.x + box.width / 2; |
| 56 | + const cy = box.y + box.height / 2; |
| 57 | + await page.mouse.move(cx, cy); |
| 58 | + await page.mouse.down(); |
| 59 | + // Six discrete moves: the bug compounded per event, so a stepped drag |
| 60 | + // separates "follows the pointer" from "re-adds the delta every time". |
| 61 | + for (let i = 1; i <= 6; i++) |
| 62 | + await page.mouse.move(cx + (dx * i) / 6, cy); |
| 63 | + |
| 64 | + await page.mouse.up(); |
| 65 | + |
| 66 | + // Mouseup rewrites the markdown around the image as an explicit <img>. |
| 67 | + await expect.poll(async () => getMarkdown(page), { |
| 68 | + timeout: 5_000, |
| 69 | + intervals: [50, 100, 250, 500], |
| 70 | + }).toMatch(/<img\s[^>]*width="\d+"/i); |
| 71 | + |
| 72 | + const saved = Number.parseInt((await getMarkdown(page)).match(/<img\s[^>]*width="(\d+)"/i)![1]); |
| 73 | + const shown = await page.locator(editor.image).first().locator('img').first() |
| 74 | + .evaluate(el => Math.round(el.getBoundingClientRect().width)); |
| 75 | + |
| 76 | + return { saved, shown }; |
| 77 | +} |
| 78 | + |
| 79 | +test.describe('ImageResizeBar handle tracking (#5392)', () => { |
| 80 | + test('dragging the left handle outwards widens by the distance dragged', async ({ page }) => { |
| 81 | + const { startWidth } = await showHandles(page); |
| 82 | + |
| 83 | + const { saved, shown } = await dragHandle(page, 'left', -DRAG); |
| 84 | + |
| 85 | + expect(Math.abs(saved - (startWidth + DRAG))).toBeLessThanOrEqual(TOLERANCE); |
| 86 | + // The width written to the file is the width on screen — the runaway |
| 87 | + // used to save 710 for an image displayed at the 650 px column width. |
| 88 | + expect(Math.abs(shown - saved)).toBeLessThanOrEqual(TOLERANCE); |
| 89 | + }); |
| 90 | + |
| 91 | + test('dragging the left handle inwards narrows by the distance dragged', async ({ page }) => { |
| 92 | + const { startWidth } = await showHandles(page); |
| 93 | + |
| 94 | + const { saved } = await dragHandle(page, 'left', DRAG); |
| 95 | + |
| 96 | + expect(Math.abs(saved - (startWidth - DRAG))).toBeLessThanOrEqual(TOLERANCE); |
| 97 | + }); |
| 98 | + |
| 99 | + test('dragging the right handle outwards widens by the distance dragged', async ({ page }) => { |
| 100 | + const { startWidth } = await showHandles(page); |
| 101 | + |
| 102 | + const { saved } = await dragHandle(page, 'right', DRAG); |
| 103 | + |
| 104 | + expect(Math.abs(saved - (startWidth + DRAG))).toBeLessThanOrEqual(TOLERANCE); |
| 105 | + }); |
| 106 | +}); |
0 commit comments