diff --git a/.playwright/screenshots/ellipsize-between-empty-paragraphs-clip.png b/.playwright/screenshots/ellipsize-between-empty-paragraphs-clip.png new file mode 100644 index 000000000..f36c8d91c Binary files /dev/null and b/.playwright/screenshots/ellipsize-between-empty-paragraphs-clip.png differ diff --git a/.playwright/screenshots/ellipsize-between-empty-paragraphs-head.png b/.playwright/screenshots/ellipsize-between-empty-paragraphs-head.png new file mode 100644 index 000000000..84b46e35f Binary files /dev/null and b/.playwright/screenshots/ellipsize-between-empty-paragraphs-head.png differ diff --git a/.playwright/screenshots/ellipsize-between-empty-paragraphs-tail.png b/.playwright/screenshots/ellipsize-between-empty-paragraphs-tail.png new file mode 100644 index 000000000..c7640c0ad Binary files /dev/null and b/.playwright/screenshots/ellipsize-between-empty-paragraphs-tail.png differ diff --git a/.playwright/screenshots/ellipsize-between-multiple-empty-lines-clip.png b/.playwright/screenshots/ellipsize-between-multiple-empty-lines-clip.png new file mode 100644 index 000000000..ff283877a Binary files /dev/null and b/.playwright/screenshots/ellipsize-between-multiple-empty-lines-clip.png differ diff --git a/.playwright/screenshots/ellipsize-between-multiple-empty-lines-head.png b/.playwright/screenshots/ellipsize-between-multiple-empty-lines-head.png new file mode 100644 index 000000000..84b46e35f Binary files /dev/null and b/.playwright/screenshots/ellipsize-between-multiple-empty-lines-head.png differ diff --git a/.playwright/screenshots/ellipsize-between-multiple-empty-lines-tail.png b/.playwright/screenshots/ellipsize-between-multiple-empty-lines-tail.png new file mode 100644 index 000000000..84b46e35f Binary files /dev/null and b/.playwright/screenshots/ellipsize-between-multiple-empty-lines-tail.png differ diff --git a/.playwright/screenshots/ellipsize-breaklines-head-fallback-middle.png b/.playwright/screenshots/ellipsize-breaklines-head-fallback-middle.png new file mode 100644 index 000000000..3b8ad949e Binary files /dev/null and b/.playwright/screenshots/ellipsize-breaklines-head-fallback-middle.png differ diff --git a/.playwright/screenshots/ellipsize-checkbox-list-clip.png b/.playwright/screenshots/ellipsize-checkbox-list-clip.png new file mode 100644 index 000000000..2df8714af Binary files /dev/null and b/.playwright/screenshots/ellipsize-checkbox-list-clip.png differ diff --git a/.playwright/screenshots/ellipsize-checkbox-list-head.png b/.playwright/screenshots/ellipsize-checkbox-list-head.png new file mode 100644 index 000000000..f9f442bd7 Binary files /dev/null and b/.playwright/screenshots/ellipsize-checkbox-list-head.png differ diff --git a/.playwright/screenshots/ellipsize-checkbox-list-only-empty-clip.png b/.playwright/screenshots/ellipsize-checkbox-list-only-empty-clip.png new file mode 100644 index 000000000..88c61c087 Binary files /dev/null and b/.playwright/screenshots/ellipsize-checkbox-list-only-empty-clip.png differ diff --git a/.playwright/screenshots/ellipsize-checkbox-list-only-empty-head.png b/.playwright/screenshots/ellipsize-checkbox-list-only-empty-head.png new file mode 100644 index 000000000..548dfa920 Binary files /dev/null and b/.playwright/screenshots/ellipsize-checkbox-list-only-empty-head.png differ diff --git a/.playwright/screenshots/ellipsize-checkbox-list-only-empty-tail.png b/.playwright/screenshots/ellipsize-checkbox-list-only-empty-tail.png new file mode 100644 index 000000000..548dfa920 Binary files /dev/null and b/.playwright/screenshots/ellipsize-checkbox-list-only-empty-tail.png differ diff --git a/.playwright/screenshots/ellipsize-checkbox-list-tail.png b/.playwright/screenshots/ellipsize-checkbox-list-tail.png new file mode 100644 index 000000000..921e96b13 Binary files /dev/null and b/.playwright/screenshots/ellipsize-checkbox-list-tail.png differ diff --git a/.playwright/screenshots/ellipsize-checkbox-list-with-empty-clip.png b/.playwright/screenshots/ellipsize-checkbox-list-with-empty-clip.png new file mode 100644 index 000000000..006d3c40c Binary files /dev/null and b/.playwright/screenshots/ellipsize-checkbox-list-with-empty-clip.png differ diff --git a/.playwright/screenshots/ellipsize-checkbox-list-with-empty-head.png b/.playwright/screenshots/ellipsize-checkbox-list-with-empty-head.png new file mode 100644 index 000000000..76a7d41c8 Binary files /dev/null and b/.playwright/screenshots/ellipsize-checkbox-list-with-empty-head.png differ diff --git a/.playwright/screenshots/ellipsize-checkbox-list-with-empty-tail.png b/.playwright/screenshots/ellipsize-checkbox-list-with-empty-tail.png new file mode 100644 index 000000000..3bdb7f6d5 Binary files /dev/null and b/.playwright/screenshots/ellipsize-checkbox-list-with-empty-tail.png differ diff --git a/.playwright/screenshots/ellipsize-different-blocks-head-fallback-middle.png b/.playwright/screenshots/ellipsize-different-blocks-head-fallback-middle.png new file mode 100644 index 000000000..a20039d38 Binary files /dev/null and b/.playwright/screenshots/ellipsize-different-blocks-head-fallback-middle.png differ diff --git a/.playwright/screenshots/ellipsize-different-paragraph-style-clip.png b/.playwright/screenshots/ellipsize-different-paragraph-style-clip.png new file mode 100644 index 000000000..aaa78b98d Binary files /dev/null and b/.playwright/screenshots/ellipsize-different-paragraph-style-clip.png differ diff --git a/.playwright/screenshots/ellipsize-different-paragraph-style-head.png b/.playwright/screenshots/ellipsize-different-paragraph-style-head.png new file mode 100644 index 000000000..73ab67473 Binary files /dev/null and b/.playwright/screenshots/ellipsize-different-paragraph-style-head.png differ diff --git a/.playwright/screenshots/ellipsize-different-paragraph-style-middle.png b/.playwright/screenshots/ellipsize-different-paragraph-style-middle.png new file mode 100644 index 000000000..b53a35bbc Binary files /dev/null and b/.playwright/screenshots/ellipsize-different-paragraph-style-middle.png differ diff --git a/.playwright/screenshots/ellipsize-different-paragraph-style-multiple-lines-middle.png b/.playwright/screenshots/ellipsize-different-paragraph-style-multiple-lines-middle.png new file mode 100644 index 000000000..111980d76 Binary files /dev/null and b/.playwright/screenshots/ellipsize-different-paragraph-style-multiple-lines-middle.png differ diff --git a/.playwright/screenshots/ellipsize-different-paragraph-style-tail.png b/.playwright/screenshots/ellipsize-different-paragraph-style-tail.png new file mode 100644 index 000000000..5db8387f1 Binary files /dev/null and b/.playwright/screenshots/ellipsize-different-paragraph-style-tail.png differ diff --git a/.playwright/screenshots/ellipsize-different-paragraph-style-with-empty-line-clip.png b/.playwright/screenshots/ellipsize-different-paragraph-style-with-empty-line-clip.png new file mode 100644 index 000000000..268d926e6 Binary files /dev/null and b/.playwright/screenshots/ellipsize-different-paragraph-style-with-empty-line-clip.png differ diff --git a/.playwright/screenshots/ellipsize-different-paragraph-style-with-empty-line-head.png b/.playwright/screenshots/ellipsize-different-paragraph-style-with-empty-line-head.png new file mode 100644 index 000000000..565421f3e Binary files /dev/null and b/.playwright/screenshots/ellipsize-different-paragraph-style-with-empty-line-head.png differ diff --git a/.playwright/screenshots/ellipsize-different-paragraph-style-with-empty-line-tail.png b/.playwright/screenshots/ellipsize-different-paragraph-style-with-empty-line-tail.png new file mode 100644 index 000000000..7d248d8f4 Binary files /dev/null and b/.playwright/screenshots/ellipsize-different-paragraph-style-with-empty-line-tail.png differ diff --git a/.playwright/screenshots/ellipsize-empty-in-the-middle-clip.png b/.playwright/screenshots/ellipsize-empty-in-the-middle-clip.png new file mode 100644 index 000000000..04b9902ca Binary files /dev/null and b/.playwright/screenshots/ellipsize-empty-in-the-middle-clip.png differ diff --git a/.playwright/screenshots/ellipsize-empty-in-the-middle-head.png b/.playwright/screenshots/ellipsize-empty-in-the-middle-head.png new file mode 100644 index 000000000..c84d280d1 Binary files /dev/null and b/.playwright/screenshots/ellipsize-empty-in-the-middle-head.png differ diff --git a/.playwright/screenshots/ellipsize-empty-in-the-middle-tail.png b/.playwright/screenshots/ellipsize-empty-in-the-middle-tail.png new file mode 100644 index 000000000..f7894459f Binary files /dev/null and b/.playwright/screenshots/ellipsize-empty-in-the-middle-tail.png differ diff --git a/.playwright/screenshots/ellipsize-empty-lines-middle.png b/.playwright/screenshots/ellipsize-empty-lines-middle.png new file mode 100644 index 000000000..2206b3ec4 Binary files /dev/null and b/.playwright/screenshots/ellipsize-empty-lines-middle.png differ diff --git a/.playwright/screenshots/ellipsize-htmlstyle-blockquote-gapwidth-recompute.png b/.playwright/screenshots/ellipsize-htmlstyle-blockquote-gapwidth-recompute.png new file mode 100644 index 000000000..9a3233583 Binary files /dev/null and b/.playwright/screenshots/ellipsize-htmlstyle-blockquote-gapwidth-recompute.png differ diff --git a/.playwright/screenshots/ellipsize-htmlstyle-heading-fontsize-recompute.png b/.playwright/screenshots/ellipsize-htmlstyle-heading-fontsize-recompute.png new file mode 100644 index 000000000..33f1da974 Binary files /dev/null and b/.playwright/screenshots/ellipsize-htmlstyle-heading-fontsize-recompute.png differ diff --git a/.playwright/screenshots/ellipsize-htmlstyle-ol-marginleft-recompute.png b/.playwright/screenshots/ellipsize-htmlstyle-ol-marginleft-recompute.png new file mode 100644 index 000000000..f280ddcc4 Binary files /dev/null and b/.playwright/screenshots/ellipsize-htmlstyle-ol-marginleft-recompute.png differ diff --git a/.playwright/screenshots/ellipsize-inline-images-clip.png b/.playwright/screenshots/ellipsize-inline-images-clip.png new file mode 100644 index 000000000..8b148c0a4 Binary files /dev/null and b/.playwright/screenshots/ellipsize-inline-images-clip.png differ diff --git a/.playwright/screenshots/ellipsize-inline-images-head.png b/.playwright/screenshots/ellipsize-inline-images-head.png new file mode 100644 index 000000000..f6c7e1870 Binary files /dev/null and b/.playwright/screenshots/ellipsize-inline-images-head.png differ diff --git a/.playwright/screenshots/ellipsize-inline-images-tail.png b/.playwright/screenshots/ellipsize-inline-images-tail.png new file mode 100644 index 000000000..2bf67c328 Binary files /dev/null and b/.playwright/screenshots/ellipsize-inline-images-tail.png differ diff --git a/.playwright/screenshots/ellipsize-multiple-short-paragraphs-clip.png b/.playwright/screenshots/ellipsize-multiple-short-paragraphs-clip.png new file mode 100644 index 000000000..ca3b78f5f Binary files /dev/null and b/.playwright/screenshots/ellipsize-multiple-short-paragraphs-clip.png differ diff --git a/.playwright/screenshots/ellipsize-multiple-short-paragraphs-head.png b/.playwright/screenshots/ellipsize-multiple-short-paragraphs-head.png new file mode 100644 index 000000000..da05a5261 Binary files /dev/null and b/.playwright/screenshots/ellipsize-multiple-short-paragraphs-head.png differ diff --git a/.playwright/screenshots/ellipsize-multiple-short-paragraphs-tail.png b/.playwright/screenshots/ellipsize-multiple-short-paragraphs-tail.png new file mode 100644 index 000000000..0b76dcb15 Binary files /dev/null and b/.playwright/screenshots/ellipsize-multiple-short-paragraphs-tail.png differ diff --git a/.playwright/screenshots/ellipsize-multiple-short-paragraphs-with-last-overflow-clip.png b/.playwright/screenshots/ellipsize-multiple-short-paragraphs-with-last-overflow-clip.png new file mode 100644 index 000000000..ca3b78f5f Binary files /dev/null and b/.playwright/screenshots/ellipsize-multiple-short-paragraphs-with-last-overflow-clip.png differ diff --git a/.playwright/screenshots/ellipsize-multiple-short-paragraphs-with-last-overflow-head.png b/.playwright/screenshots/ellipsize-multiple-short-paragraphs-with-last-overflow-head.png new file mode 100644 index 000000000..d021554b9 Binary files /dev/null and b/.playwright/screenshots/ellipsize-multiple-short-paragraphs-with-last-overflow-head.png differ diff --git a/.playwright/screenshots/ellipsize-multiple-short-paragraphs-with-last-overflow-tail.png b/.playwright/screenshots/ellipsize-multiple-short-paragraphs-with-last-overflow-tail.png new file mode 100644 index 000000000..0b76dcb15 Binary files /dev/null and b/.playwright/screenshots/ellipsize-multiple-short-paragraphs-with-last-overflow-tail.png differ diff --git a/.playwright/screenshots/ellipsize-one-paragraph-many-inline-images-middle.png b/.playwright/screenshots/ellipsize-one-paragraph-many-inline-images-middle.png new file mode 100644 index 000000000..f4d7bc56a Binary files /dev/null and b/.playwright/screenshots/ellipsize-one-paragraph-many-inline-images-middle.png differ diff --git a/.playwright/screenshots/ellipsize-one-paragraph-many-lines-overflowing-middle.png b/.playwright/screenshots/ellipsize-one-paragraph-many-lines-overflowing-middle.png new file mode 100644 index 000000000..285ea9805 Binary files /dev/null and b/.playwright/screenshots/ellipsize-one-paragraph-many-lines-overflowing-middle.png differ diff --git a/.playwright/screenshots/ellipsize-one-paragraph-one-line-overflowing-middle.png b/.playwright/screenshots/ellipsize-one-paragraph-one-line-overflowing-middle.png new file mode 100644 index 000000000..9d7674141 Binary files /dev/null and b/.playwright/screenshots/ellipsize-one-paragraph-one-line-overflowing-middle.png differ diff --git a/.playwright/screenshots/ellipsize-one-word-wrapping-clip.png b/.playwright/screenshots/ellipsize-one-word-wrapping-clip.png new file mode 100644 index 000000000..32d8960fb Binary files /dev/null and b/.playwright/screenshots/ellipsize-one-word-wrapping-clip.png differ diff --git a/.playwright/screenshots/ellipsize-one-word-wrapping-head.png b/.playwright/screenshots/ellipsize-one-word-wrapping-head.png new file mode 100644 index 000000000..0ac20f336 Binary files /dev/null and b/.playwright/screenshots/ellipsize-one-word-wrapping-head.png differ diff --git a/.playwright/screenshots/ellipsize-one-word-wrapping-tail.png b/.playwright/screenshots/ellipsize-one-word-wrapping-tail.png new file mode 100644 index 000000000..ee119f893 Binary files /dev/null and b/.playwright/screenshots/ellipsize-one-word-wrapping-tail.png differ diff --git a/.playwright/screenshots/ellipsize-ordered-list-clip.png b/.playwright/screenshots/ellipsize-ordered-list-clip.png new file mode 100644 index 000000000..1109dcc45 Binary files /dev/null and b/.playwright/screenshots/ellipsize-ordered-list-clip.png differ diff --git a/.playwright/screenshots/ellipsize-ordered-list-head.png b/.playwright/screenshots/ellipsize-ordered-list-head.png new file mode 100644 index 000000000..312599ad6 Binary files /dev/null and b/.playwright/screenshots/ellipsize-ordered-list-head.png differ diff --git a/.playwright/screenshots/ellipsize-ordered-list-only-empty-clip.png b/.playwright/screenshots/ellipsize-ordered-list-only-empty-clip.png new file mode 100644 index 000000000..0f7105d91 Binary files /dev/null and b/.playwright/screenshots/ellipsize-ordered-list-only-empty-clip.png differ diff --git a/.playwright/screenshots/ellipsize-ordered-list-only-empty-head.png b/.playwright/screenshots/ellipsize-ordered-list-only-empty-head.png new file mode 100644 index 000000000..c698835f4 Binary files /dev/null and b/.playwright/screenshots/ellipsize-ordered-list-only-empty-head.png differ diff --git a/.playwright/screenshots/ellipsize-ordered-list-only-empty-tail.png b/.playwright/screenshots/ellipsize-ordered-list-only-empty-tail.png new file mode 100644 index 000000000..c698835f4 Binary files /dev/null and b/.playwright/screenshots/ellipsize-ordered-list-only-empty-tail.png differ diff --git a/.playwright/screenshots/ellipsize-ordered-list-tail.png b/.playwright/screenshots/ellipsize-ordered-list-tail.png new file mode 100644 index 000000000..2ea1e547b Binary files /dev/null and b/.playwright/screenshots/ellipsize-ordered-list-tail.png differ diff --git a/.playwright/screenshots/ellipsize-ordered-list-with-empty-clip.png b/.playwright/screenshots/ellipsize-ordered-list-with-empty-clip.png new file mode 100644 index 000000000..d2ceef00a Binary files /dev/null and b/.playwright/screenshots/ellipsize-ordered-list-with-empty-clip.png differ diff --git a/.playwright/screenshots/ellipsize-ordered-list-with-empty-head.png b/.playwright/screenshots/ellipsize-ordered-list-with-empty-head.png new file mode 100644 index 000000000..08fbc1044 Binary files /dev/null and b/.playwright/screenshots/ellipsize-ordered-list-with-empty-head.png differ diff --git a/.playwright/screenshots/ellipsize-ordered-list-with-empty-tail.png b/.playwright/screenshots/ellipsize-ordered-list-with-empty-tail.png new file mode 100644 index 000000000..b14cf8dd5 Binary files /dev/null and b/.playwright/screenshots/ellipsize-ordered-list-with-empty-tail.png differ diff --git a/.playwright/screenshots/ellipsize-other-paragraphs-and-break-lines-inside-block-style-middle.png b/.playwright/screenshots/ellipsize-other-paragraphs-and-break-lines-inside-block-style-middle.png new file mode 100644 index 000000000..5ca41106b Binary files /dev/null and b/.playwright/screenshots/ellipsize-other-paragraphs-and-break-lines-inside-block-style-middle.png differ diff --git a/.playwright/screenshots/ellipsize-paragraph-fit-clip.png b/.playwright/screenshots/ellipsize-paragraph-fit-clip.png new file mode 100644 index 000000000..87805c906 Binary files /dev/null and b/.playwright/screenshots/ellipsize-paragraph-fit-clip.png differ diff --git a/.playwright/screenshots/ellipsize-paragraph-fit-head.png b/.playwright/screenshots/ellipsize-paragraph-fit-head.png new file mode 100644 index 000000000..87805c906 Binary files /dev/null and b/.playwright/screenshots/ellipsize-paragraph-fit-head.png differ diff --git a/.playwright/screenshots/ellipsize-paragraph-fit-tail.png b/.playwright/screenshots/ellipsize-paragraph-fit-tail.png new file mode 100644 index 000000000..87805c906 Binary files /dev/null and b/.playwright/screenshots/ellipsize-paragraph-fit-tail.png differ diff --git a/.playwright/screenshots/ellipsize-paragraph-styles-mix-clip.png b/.playwright/screenshots/ellipsize-paragraph-styles-mix-clip.png new file mode 100644 index 000000000..770d71cb9 Binary files /dev/null and b/.playwright/screenshots/ellipsize-paragraph-styles-mix-clip.png differ diff --git a/.playwright/screenshots/ellipsize-paragraph-styles-mix-head.png b/.playwright/screenshots/ellipsize-paragraph-styles-mix-head.png new file mode 100644 index 000000000..3299f8262 Binary files /dev/null and b/.playwright/screenshots/ellipsize-paragraph-styles-mix-head.png differ diff --git a/.playwright/screenshots/ellipsize-paragraph-styles-mix-tail.png b/.playwright/screenshots/ellipsize-paragraph-styles-mix-tail.png new file mode 100644 index 000000000..8d5bf475a Binary files /dev/null and b/.playwright/screenshots/ellipsize-paragraph-styles-mix-tail.png differ diff --git a/.playwright/screenshots/ellipsize-single-word-clip.png b/.playwright/screenshots/ellipsize-single-word-clip.png new file mode 100644 index 000000000..d80cfbe05 Binary files /dev/null and b/.playwright/screenshots/ellipsize-single-word-clip.png differ diff --git a/.playwright/screenshots/ellipsize-single-word-head.png b/.playwright/screenshots/ellipsize-single-word-head.png new file mode 100644 index 000000000..ffb8d932e Binary files /dev/null and b/.playwright/screenshots/ellipsize-single-word-head.png differ diff --git a/.playwright/screenshots/ellipsize-single-word-tail.png b/.playwright/screenshots/ellipsize-single-word-tail.png new file mode 100644 index 000000000..d141cde28 Binary files /dev/null and b/.playwright/screenshots/ellipsize-single-word-tail.png differ diff --git a/.playwright/screenshots/ellipsize-style-fontsize-recompute.png b/.playwright/screenshots/ellipsize-style-fontsize-recompute.png new file mode 100644 index 000000000..090943f10 Binary files /dev/null and b/.playwright/screenshots/ellipsize-style-fontsize-recompute.png differ diff --git a/.playwright/screenshots/ellipsize-unordered-list-clip.png b/.playwright/screenshots/ellipsize-unordered-list-clip.png new file mode 100644 index 000000000..08dc90d43 Binary files /dev/null and b/.playwright/screenshots/ellipsize-unordered-list-clip.png differ diff --git a/.playwright/screenshots/ellipsize-unordered-list-head.png b/.playwright/screenshots/ellipsize-unordered-list-head.png new file mode 100644 index 000000000..537577a91 Binary files /dev/null and b/.playwright/screenshots/ellipsize-unordered-list-head.png differ diff --git a/.playwright/screenshots/ellipsize-unordered-list-only-empty-clip.png b/.playwright/screenshots/ellipsize-unordered-list-only-empty-clip.png new file mode 100644 index 000000000..623aa9e71 Binary files /dev/null and b/.playwright/screenshots/ellipsize-unordered-list-only-empty-clip.png differ diff --git a/.playwright/screenshots/ellipsize-unordered-list-only-empty-head.png b/.playwright/screenshots/ellipsize-unordered-list-only-empty-head.png new file mode 100644 index 000000000..280ea9eb5 Binary files /dev/null and b/.playwright/screenshots/ellipsize-unordered-list-only-empty-head.png differ diff --git a/.playwright/screenshots/ellipsize-unordered-list-only-empty-tail.png b/.playwright/screenshots/ellipsize-unordered-list-only-empty-tail.png new file mode 100644 index 000000000..280ea9eb5 Binary files /dev/null and b/.playwright/screenshots/ellipsize-unordered-list-only-empty-tail.png differ diff --git a/.playwright/screenshots/ellipsize-unordered-list-tail.png b/.playwright/screenshots/ellipsize-unordered-list-tail.png new file mode 100644 index 000000000..4ec8141d5 Binary files /dev/null and b/.playwright/screenshots/ellipsize-unordered-list-tail.png differ diff --git a/.playwright/screenshots/ellipsize-unordered-list-with-empty-clip.png b/.playwright/screenshots/ellipsize-unordered-list-with-empty-clip.png new file mode 100644 index 000000000..98a7731fc Binary files /dev/null and b/.playwright/screenshots/ellipsize-unordered-list-with-empty-clip.png differ diff --git a/.playwright/screenshots/ellipsize-unordered-list-with-empty-head.png b/.playwright/screenshots/ellipsize-unordered-list-with-empty-head.png new file mode 100644 index 000000000..b7c8a879f Binary files /dev/null and b/.playwright/screenshots/ellipsize-unordered-list-with-empty-head.png differ diff --git a/.playwright/screenshots/ellipsize-unordered-list-with-empty-tail.png b/.playwright/screenshots/ellipsize-unordered-list-with-empty-tail.png new file mode 100644 index 000000000..0d02686fa Binary files /dev/null and b/.playwright/screenshots/ellipsize-unordered-list-with-empty-tail.png differ diff --git a/.playwright/screenshots/ellipsize-very-long-paragraph-clip.png b/.playwright/screenshots/ellipsize-very-long-paragraph-clip.png new file mode 100644 index 000000000..963c39083 Binary files /dev/null and b/.playwright/screenshots/ellipsize-very-long-paragraph-clip.png differ diff --git a/.playwright/screenshots/ellipsize-very-long-paragraph-head.png b/.playwright/screenshots/ellipsize-very-long-paragraph-head.png new file mode 100644 index 000000000..50e3681ac Binary files /dev/null and b/.playwright/screenshots/ellipsize-very-long-paragraph-head.png differ diff --git a/.playwright/screenshots/ellipsize-very-long-paragraph-tail.png b/.playwright/screenshots/ellipsize-very-long-paragraph-tail.png new file mode 100644 index 000000000..eb2525254 Binary files /dev/null and b/.playwright/screenshots/ellipsize-very-long-paragraph-tail.png differ diff --git a/.playwright/tests/ellipsizeVisual.spec.ts b/.playwright/tests/ellipsizeVisual.spec.ts new file mode 100644 index 000000000..d55a1691e --- /dev/null +++ b/.playwright/tests/ellipsizeVisual.spec.ts @@ -0,0 +1,383 @@ +import { test, expect, type Locator, type Page } from '@playwright/test'; + +test.setTimeout(90_000); + +type EllipsizeMode = 'head' | 'middle' | 'tail' | 'clip'; + +const LOREM_IPSUM = `

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur porta placerat nulla vitae tincidunt. In tellus neque, volutpat id molestie ut, dapibus sit amet justo. Sed nec lobortis orci. Vestibulum magna est, placerat vitae libero ut, viverra mollis purus. Curabitur lacinia a libero non congue. Vestibulum et velit lacinia, feugiat odio eget, faucibus sapien. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus eu erat commodo, condimentum augue eu, fringilla purus. Morbi at nisi eget felis dignissim fringilla. Pellentesque blandit porttitor libero. Donec accumsan, leo eget ultricies rutrum, lorem purus aliquet quam, sit amet pellentesque lectus nulla non nulla.

`; + +const sel = { + root: '[data-testid="test-ellipsize-root"]', + display: '[data-testid="test-ellipsize-display"]', + displayInner: '[data-testid="test-ellipsize-display"] .et-view', + htmlInput: '[data-testid="test-ellipsize-html-input"]', + setValueButton: '[data-testid="test-ellipsize-set-value-button"]', + numberOfLinesInput: '[data-testid="test-ellipsize-number-of-lines-input"]', + numberOfLinesOutput: '[data-testid="test-ellipsize-number-of-lines-output"]', + modeButton: (mode: EllipsizeMode) => + `[data-testid="test-ellipsize-mode-${mode}"]`, + modeOutput: '[data-testid="test-ellipsize-mode-output"]', + valueOutput: '[data-testid="test-ellipsize-value-output"]', + toggleButton: (key: OverrideKey) => + `[data-testid="test-ellipsize-toggle-${key}"]`, + overrideOutput: (key: OverrideKey) => + `[data-testid="test-ellipsize-${key}-override-output"]`, +} as const; + +type OverrideKey = 'style' | 'headings' | 'ol' | 'blockquote'; + +function displayLocator(page: Page): Locator { + return page.locator(sel.display); +} + +async function gotoTestEllipsize(page: Page): Promise { + await page.goto('/test-ellipsize'); + await page.waitForSelector(sel.displayInner); +} + +async function setValue(page: Page, html: string): Promise { + await page.fill(sel.htmlInput, html); + await page.click(sel.setValueButton); + await expect + .poll(async () => (await page.locator(sel.valueOutput).textContent()) ?? '') + .toBe(html); +} + +async function setNumberOfLines(page: Page, lines: number): Promise { + await page.fill(sel.numberOfLinesInput, String(lines)); + await expect + .poll( + async () => + (await page.locator(sel.numberOfLinesOutput).textContent())?.trim() ?? + '' + ) + .toBe(String(lines)); +} + +async function setMode(page: Page, mode: EllipsizeMode): Promise { + await page.click(sel.modeButton(mode)); + await expect + .poll( + async () => + (await page.locator(sel.modeOutput).textContent())?.trim() ?? '' + ) + .toBe(mode); +} + +async function toggleOverride(page: Page, key: OverrideKey): Promise { + await page.click(sel.toggleButton(key)); + await expect + .poll( + async () => + (await page.locator(sel.overrideOutput(key)).textContent())?.trim() ?? + '' + ) + .toContain('true'); +} + +const SHARED_MODES: EllipsizeMode[] = ['tail', 'head', 'clip']; + +type EllipsizeCase = { + name: string; + slug: string; + html: string; + numberOfLines: number; +}; + +const SHARED_CASES: EllipsizeCase[] = [ + { + name: 'paragraph that fits', + slug: 'paragraph-fit', + html: `

hello that is a paragraph that should fit and require no ellipsis

`, + numberOfLines: 2, + }, + { + name: 'single word wrapping to multiple lines', + slug: 'single-word', + html: + `

Verylongpieceoftextfitintoonelinesowecancheckthiscasewherethereis` + + `averylongpieceoftextanditwrapsaroundsomenumberoflinesandthatwillbetruncated` + + `accordinglytothesetellipsizemodeintheenrichedtextcomponent

`, + numberOfLines: 2, + }, + { + name: 'a long word that causes line wrapping', + slug: 'one-word-wrapping', + html: + `

Some paragraph with a long word that will not fit and wrap.` + + ` The long word: Konstantynopolitanczykowianeczka

`, + numberOfLines: 2, + }, + { + name: 'paragraph with a number of lines much greater than the given limit', + slug: 'very-long-paragraph', + html: LOREM_IPSUM, + numberOfLines: 2, + }, + { + name: 'a number of paragraphs much greater than the given limit', + slug: 'multiple-short-paragraphs', + html: `

first

second

third

fourth

fifth

sixth

seventh

`, + numberOfLines: 4, + }, + { + name: 'a number of paragraphs much greater than the given limit and the last one overflows', + slug: 'multiple-short-paragraphs-with-last-overflow', + html: `

first

second

third

fourth

fifth

sixth

seventh line that will be long enough that it overflows to the next 8th line

`, + numberOfLines: 4, + }, + { + name: 'some text between empty paragraphs', + slug: 'between-empty-paragraphs', + html: `

between empty paragraphs


`, + numberOfLines: 2, + }, + { + name: 'some text between multiple empty lines', + slug: 'between-multiple-empty-lines', + html: `

between empty paragraphs


`, + numberOfLines: 2, + }, + { + name: 'some empty paragraphs in the middle', + slug: 'empty-in-the-middle', + html: `

first line




last line

`, + numberOfLines: 2, + }, + { + name: 'unordered list', + slug: 'unordered-list', + html: `
  • first
  • second
  • third
`, + numberOfLines: 2, + }, + { + name: 'unordered list with empty elements', + slug: 'unordered-list-with-empty', + html: `
  • second
  • fourth
`, + numberOfLines: 3, + }, + { + name: 'unordered list with only empty elements', + slug: 'unordered-list-only-empty', + html: `
`, + numberOfLines: 2, + }, + { + name: 'ordered list', + slug: 'ordered-list', + html: `
  1. first
  2. second
  3. third
`, + numberOfLines: 2, + }, + { + name: 'ordered list with empty elements', + slug: 'ordered-list-with-empty', + html: `
  1. second
  2. fourth
`, + numberOfLines: 3, + }, + { + name: 'ordered list with only empty elements', + slug: 'ordered-list-only-empty', + html: `
`, + numberOfLines: 2, + }, + { + name: 'checkbox list', + slug: 'checkbox-list', + html: `
  • first
  • second
  • third
`, + numberOfLines: 2, + }, + { + name: 'checkbox list with empty elements', + slug: 'checkbox-list-with-empty', + html: `
  • second
  • fourth
`, + numberOfLines: 3, + }, + { + name: 'checkbox list with only empty elements', + slug: 'checkbox-list-only-empty', + html: `
`, + numberOfLines: 2, + }, + { + name: 'different paragraph style', + slug: 'different-paragraph-style', + html: `

this is a pretty long blockquote - a different block style paragraph, long enough to overflow in the given number of lines.

`, + numberOfLines: 2, + }, + { + name: 'different paragraph style with an empty line', + slug: 'different-paragraph-style-with-empty-line', + html: `

this is a pretty short blockquote.


This is a line after an empty line.

`, + numberOfLines: 2, + }, + { + name: 'mixing different paragraph styles', + slug: 'paragraph-styles-mix', + html: `

This is a normal paragraph.

This is a codeblock that will be long enough to overflow.

`, + numberOfLines: 2, + }, + { + name: 'inline images', + slug: 'inline-images', + html: + `

This is a line with an inline image.` + + `

This is a second line and the inline image ` + + `will make it overflow to the next line.

This is the last line with an inline image.

`, + numberOfLines: 2, + }, +]; + +for (const mode of SHARED_MODES) { + test.describe(`EnrichedText ellipsize - ${mode}`, () => { + for (const c of SHARED_CASES) { + test(c.name, async ({ page }) => { + await gotoTestEllipsize(page); + await setValue(page, c.html); + await setNumberOfLines(page, c.numberOfLines); + await setMode(page, mode); + + await expect(displayLocator(page)).toHaveScreenshot( + `ellipsize-${c.slug}-${mode}.png` + ); + }); + } + }); +} + +test.describe('EnrichedText ellipsize - middle', () => { + const middleEllipsizeCases: EllipsizeCase[] = [ + { + name: 'all text within one paragraph, with one line exceeding the limit', + slug: 'one-paragraph-one-line-overflowing', + html: + `

This is a fairly long paragraph that should wrap` + + ` across three lines so the truncation has something to chew on.

`, + numberOfLines: 2, + }, + { + name: 'all text within one paragraph, but with at least two lines exceeding the limit', + slug: 'one-paragraph-many-lines-overflowing', + html: + `

This is a fairly long paragraph that should wrap across four lines so the` + + ` truncation has something to chew on. Lorem ipsum dolor sit amet, consectetur adipiscing elit.

`, + numberOfLines: 2, + }, + { + name: 'all text within one paragraph but with multiple inline images', + slug: 'one-paragraph-many-inline-images', + html: + `

Some paragraph text overflowing but with some inline images inside the eaten text and before, after dummytext the rendered ellipsis

`, + numberOfLines: 2, + }, + { + name: 'different paragraphs will be truncated with the middle ellipsis if the needed last lines belong to a single paragraph', + slug: 'different-paragraph-style', + html: `

first paragraph.

Different paragraph style that will overflow to the next line
`, + numberOfLines: 2, + }, + { + name: 'different paragraph style with many lines', + slug: 'different-paragraph-style-multiple-lines', + html: + `

first paragraph.

Different paragraph style and the text ` + + `that will overflow to the next lines of that block verylongwordthatwillnotfitinthelineandwilloverflow

`, + numberOfLines: 2, + }, + { + name: 'ellipsis on the last paragraph with leading other paragraphs and break lines', + slug: 'other-paragraphs-and-break-lines-inside-block-style', + html: + `

Normal style.

First line before empty ones.



Last ` + + `paragraph inside a block style that will overflow to three different lines, so we can check this case.

`, + numberOfLines: 5, + }, + { + name: 'empty lines are preserved', + slug: 'empty-lines', + html: `

first line


last line that will overflow and will have an ellipsis

`, + numberOfLines: 4, + }, + // tests checking a proper fallback to the head ellipsis + { + name: 'when the overflowing lines are different blocks, it fallbacks to head ellipsis', + slug: 'different-blocks-head-fallback', + html: `

first line

second line

third line

`, + numberOfLines: 2, + }, + { + name: 'breaklines inside a block style are considered as different paragraphs', + slug: 'breaklines-head-fallback', + html: `

first line



`, + numberOfLines: 2, + }, + ]; + + for (const c of middleEllipsizeCases) { + test(c.name, async ({ page }) => { + await gotoTestEllipsize(page); + await setValue(page, c.html); + await setNumberOfLines(page, c.numberOfLines); + await setMode(page, 'middle'); + + await expect(displayLocator(page)).toHaveScreenshot( + `ellipsize-${c.slug}-middle.png` + ); + }); + } +}); + +test.describe('EnrichedText ellipsize - recomputes on style change', () => { + type StyleChangeCase = EllipsizeCase & { override: OverrideKey }; + + const styleChangeCases: StyleChangeCase[] = [ + { + name: 'recomputes when the text style (fontSize, lineHeight) changes', + slug: 'style-fontsize', + override: 'style', + html: LOREM_IPSUM, + numberOfLines: 3, + }, + { + name: 'recomputes when the heading font sizes in htmlStyle change', + slug: 'htmlstyle-heading-fontsize', + override: 'headings', + html: `

A fairly long level four heading that wraps onto more lines

`, + numberOfLines: 2, + }, + { + name: 'recomputes when the ordered list marginLeft in htmlStyle changes', + slug: 'htmlstyle-ol-marginleft', + override: 'ol', + html: + `
  1. first item that is reasonably long and wraps to a new line
  2. ` + + `
  3. second item also long enough to wrap onto another line
  4. ` + + `
  5. third item that is long enough to overflow the given lines
`, + numberOfLines: 3, + }, + { + name: 'recomputes when the blockquote gapWidth in htmlStyle changes', + slug: 'htmlstyle-blockquote-gapwidth', + override: 'blockquote', + html: `

this is a pretty long blockquote - a different block style paragraph, long enough to overflow in the given number of lines.

`, + numberOfLines: 3, + }, + ]; + + for (const c of styleChangeCases) { + test(c.name, async ({ page }) => { + await gotoTestEllipsize(page); + await setValue(page, c.html); + await setNumberOfLines(page, c.numberOfLines); + await setMode(page, 'tail'); + + await toggleOverride(page, c.override); + + await expect(displayLocator(page)).toHaveScreenshot( + `ellipsize-${c.slug}-recompute.png` + ); + }); + } +}); diff --git a/apps/example-web/src/App.tsx b/apps/example-web/src/App.tsx index 929d0dbea..b447699c8 100644 --- a/apps/example-web/src/App.tsx +++ b/apps/example-web/src/App.tsx @@ -234,7 +234,8 @@ function App() { ?.getHTML() .then((html) => { setEnrichedTextValue(html); - ref.current?.setValue(''); + // temporary for making the testing easier + // ref.current?.setValue(''); }) .catch((error: unknown) => { setEnrichedTextValue(''); diff --git a/apps/example-web/src/RouteSelector.tsx b/apps/example-web/src/RouteSelector.tsx index c31df8dd0..e40322d84 100644 --- a/apps/example-web/src/RouteSelector.tsx +++ b/apps/example-web/src/RouteSelector.tsx @@ -5,6 +5,7 @@ import { TestSetSelection } from './testScreens/TestSetSelection'; import { VisualRegression } from './testScreens/VisualRegression'; import { TestSubmitProps } from './testScreens/TestSubmitProps'; import { TestEnrichedText } from './testScreens/TestEnrichedText'; +import { TestEllipsize } from './testScreens/TestEllipsize'; import { useEffect, useState } from 'react'; export default function RouteSelector() { @@ -45,5 +46,9 @@ export default function RouteSelector() { return ; } + if (path === '/test-ellipsize') { + return ; + } + return ; } diff --git a/apps/example-web/src/components/EnrichedTextActions.css b/apps/example-web/src/components/EnrichedTextActions.css new file mode 100644 index 000000000..17e9aacdf --- /dev/null +++ b/apps/example-web/src/components/EnrichedTextActions.css @@ -0,0 +1,19 @@ +.actions-container { + display: flex; + flex-direction: column; + gap: 12px; + width: 100%; + margin-top: 16px; + margin-bottom: 12px; +} + +.btn-row { + display: flex; + gap: 8px; + width: 100%; +} + +.btn-active { + background-color: rgb(0, 26, 114); + color: white; +} diff --git a/apps/example-web/src/components/EnrichedTextActions.tsx b/apps/example-web/src/components/EnrichedTextActions.tsx new file mode 100644 index 000000000..eab716952 --- /dev/null +++ b/apps/example-web/src/components/EnrichedTextActions.tsx @@ -0,0 +1,54 @@ +import type { TextProps } from 'react-native'; +import './EnrichedTextActions.css'; + +type EllipsizeMode = NonNullable; + +const ELLIPSIZE_MODES: EllipsizeMode[] = ['tail', 'head', 'middle', 'clip']; +const LINE_OPTIONS = [0, 2, 3]; + +interface EnrichedTextActionsProps { + ellipsizeMode: EllipsizeMode; + numberOfLines: number; + onChangeEllipsizeMode: (mode: EllipsizeMode) => void; + onChangeNumberOfLines: (lines: number) => void; +} + +export function EnrichedTextActions({ + ellipsizeMode, + numberOfLines, + onChangeEllipsizeMode, + onChangeNumberOfLines, +}: EnrichedTextActionsProps) { + return ( +
+
+ {ELLIPSIZE_MODES.map((mode) => ( + + ))} +
+
+ {LINE_OPTIONS.map((lines) => ( + + ))} +
+
+ ); +} diff --git a/apps/example-web/src/components/TextRenderer.tsx b/apps/example-web/src/components/TextRenderer.tsx index 552f16d6b..01b597efc 100644 --- a/apps/example-web/src/components/TextRenderer.tsx +++ b/apps/example-web/src/components/TextRenderer.tsx @@ -1,5 +1,5 @@ -import { useRef } from 'react'; -import type { TextStyle } from 'react-native'; +import { useRef, useState } from 'react'; +import type { TextProps, TextStyle } from 'react-native'; import { EnrichedText, type BlurEvent, @@ -9,6 +9,7 @@ import { type OnMentionPressEvent, } from 'react-native-enriched-html'; import { WEB_DEFAULT_HTML_STYLE } from '../defaultHtmlStyle'; +import { EnrichedTextActions } from './EnrichedTextActions'; interface TextRendererProps { htmlValue: string; @@ -17,6 +18,10 @@ interface TextRendererProps { export function TextRenderer({ htmlValue }: TextRendererProps) { const ref = useRef(null); + const [ellipsizeMode, setEllipsizeMode] = + useState('tail'); + const [numberOfLines, setNumberOfLines] = useState(2); + const handleTextFocus = (e: FocusEvent) => { console.log('[EnrichedText] onFocus', e.nativeEvent); }; @@ -44,25 +49,24 @@ export function TextRenderer({ htmlValue }: TextRendererProps) { onBlur={handleTextBlur} onLinkPress={handleLinkPress} onMentionPress={handleMentionPress} + numberOfLines={numberOfLines} + ellipsizeMode={ellipsizeMode} + > + {htmlValue} + + + {/*temporary second component to make testing easier*/} + {htmlValue} -
- - -
); } diff --git a/apps/example-web/src/testScreens/TestEllipsize.tsx b/apps/example-web/src/testScreens/TestEllipsize.tsx new file mode 100644 index 000000000..43949bcc6 --- /dev/null +++ b/apps/example-web/src/testScreens/TestEllipsize.tsx @@ -0,0 +1,199 @@ +import { useMemo, useState, type ChangeEvent } from 'react'; +import { + EnrichedText, + type EnrichedTextProps, + type HtmlStyle, +} from 'react-native-enriched-html'; +import type { TextStyle } from 'react-native'; +import { WEB_DEFAULT_HTML_STYLE } from '../defaultHtmlStyle'; + +type EllipsizeMode = NonNullable; + +const ELLIPSIZE_MODES: EllipsizeMode[] = ['head', 'middle', 'tail', 'clip']; + +export function TestEllipsize() { + const [htmlInput, setHtmlInput] = useState(''); + const [value, setValue] = useState(''); + const [numberOfLines, setNumberOfLines] = useState(2); + const [ellipsizeMode, setEllipsizeMode] = useState('tail'); + + const [styleOverride, setStyleOverride] = useState(false); + const [headingsOverride, setHeadingsOverride] = useState(false); + const [olOverride, setOlOverride] = useState(false); + const [blockquoteOverride, setBlockquoteOverride] = useState(false); + + const style = useMemo( + () => + styleOverride + ? { ...enrichedTextStyle, ...STYLE_OVERRIDE } + : enrichedTextStyle, + [styleOverride] + ); + + const htmlStyle = useMemo(() => { + let next: HtmlStyle = WEB_DEFAULT_HTML_STYLE; + + if (headingsOverride) { + next = { + ...next, + h1: { ...next.h1, fontSize: 40 }, + h2: { ...next.h2, fontSize: 36 }, + h3: { ...next.h3, fontSize: 32 }, + h4: { ...next.h4, fontSize: 28 }, + h5: { ...next.h5, fontSize: 22 }, + h6: { ...next.h6, fontSize: 16 }, + }; + } + + if (olOverride) { + next = { ...next, ol: { ...next.ol, marginLeft: 96 } }; + } + + if (blockquoteOverride) { + next = { ...next, blockquote: { ...next.blockquote, gapWidth: 64 } }; + } + + return next; + }, [headingsOverride, olOverride, blockquoteOverride]); + + return ( +
+
+ + {value} + +
+ +
+ + ) => { + setNumberOfLines(Number(e.target.value)); + }} + /> +
+ +
+ {ELLIPSIZE_MODES.map((mode) => ( + + ))} +
+ +
+ + + + +
+ +