diff --git a/.playwright/screenshots/enriched-text-all-headings.png b/.playwright/screenshots/enriched-text-all-headings.png new file mode 100644 index 00000000..39bce62c Binary files /dev/null and b/.playwright/screenshots/enriched-text-all-headings.png differ diff --git a/.playwright/screenshots/enriched-text-all-list-types.png b/.playwright/screenshots/enriched-text-all-list-types.png new file mode 100644 index 00000000..88c3a6da Binary files /dev/null and b/.playwright/screenshots/enriched-text-all-list-types.png differ diff --git a/.playwright/screenshots/enriched-text-blockquote-code-codeblock.png b/.playwright/screenshots/enriched-text-blockquote-code-codeblock.png new file mode 100644 index 00000000..cd7fc5f5 Binary files /dev/null and b/.playwright/screenshots/enriched-text-blockquote-code-codeblock.png differ diff --git a/.playwright/screenshots/enriched-text-checkbox-list-checked.png b/.playwright/screenshots/enriched-text-checkbox-list-checked.png new file mode 100644 index 00000000..bc9b6ca0 Binary files /dev/null and b/.playwright/screenshots/enriched-text-checkbox-list-checked.png differ diff --git a/.playwright/screenshots/enriched-text-checkbox-list-empty-items.png b/.playwright/screenshots/enriched-text-checkbox-list-empty-items.png new file mode 100644 index 00000000..043e5a24 Binary files /dev/null and b/.playwright/screenshots/enriched-text-checkbox-list-empty-items.png differ diff --git a/.playwright/screenshots/enriched-text-checkbox-list-unchecked.png b/.playwright/screenshots/enriched-text-checkbox-list-unchecked.png new file mode 100644 index 00000000..7ad09936 Binary files /dev/null and b/.playwright/screenshots/enriched-text-checkbox-list-unchecked.png differ diff --git a/.playwright/screenshots/enriched-text-images-inline.png b/.playwright/screenshots/enriched-text-images-inline.png new file mode 100644 index 00000000..50f458c4 Binary files /dev/null and b/.playwright/screenshots/enriched-text-images-inline.png differ diff --git a/.playwright/screenshots/enriched-text-images-inside-list.png b/.playwright/screenshots/enriched-text-images-inside-list.png new file mode 100644 index 00000000..40eb481e Binary files /dev/null and b/.playwright/screenshots/enriched-text-images-inside-list.png differ diff --git a/.playwright/screenshots/enriched-text-images-placeholder-inline.png b/.playwright/screenshots/enriched-text-images-placeholder-inline.png new file mode 100644 index 00000000..e54cc989 Binary files /dev/null and b/.playwright/screenshots/enriched-text-images-placeholder-inline.png differ diff --git a/.playwright/screenshots/enriched-text-images-placeholder-list.png b/.playwright/screenshots/enriched-text-images-placeholder-list.png new file mode 100644 index 00000000..aa9bebc3 Binary files /dev/null and b/.playwright/screenshots/enriched-text-images-placeholder-list.png differ diff --git a/.playwright/screenshots/enriched-text-line-wrapping.png b/.playwright/screenshots/enriched-text-line-wrapping.png new file mode 100644 index 00000000..db75829e Binary files /dev/null and b/.playwright/screenshots/enriched-text-line-wrapping.png differ diff --git a/.playwright/screenshots/enriched-text-mentions.png b/.playwright/screenshots/enriched-text-mentions.png new file mode 100644 index 00000000..8f467d81 Binary files /dev/null and b/.playwright/screenshots/enriched-text-mentions.png differ diff --git a/.playwright/screenshots/enriched-text-newlines-spaces.png b/.playwright/screenshots/enriched-text-newlines-spaces.png new file mode 100644 index 00000000..9fb1af06 Binary files /dev/null and b/.playwright/screenshots/enriched-text-newlines-spaces.png differ diff --git a/.playwright/screenshots/enriched-text-ordered-list-empty-items.png b/.playwright/screenshots/enriched-text-ordered-list-empty-items.png new file mode 100644 index 00000000..0bd291b8 Binary files /dev/null and b/.playwright/screenshots/enriched-text-ordered-list-empty-items.png differ diff --git a/.playwright/screenshots/enriched-text-ordered-list.png b/.playwright/screenshots/enriched-text-ordered-list.png new file mode 100644 index 00000000..aa9932a1 Binary files /dev/null and b/.playwright/screenshots/enriched-text-ordered-list.png differ diff --git a/.playwright/screenshots/enriched-text-rich-text.png b/.playwright/screenshots/enriched-text-rich-text.png new file mode 100644 index 00000000..44174ec4 Binary files /dev/null and b/.playwright/screenshots/enriched-text-rich-text.png differ diff --git a/.playwright/screenshots/enriched-text-unordered-list-empty-items.png b/.playwright/screenshots/enriched-text-unordered-list-empty-items.png new file mode 100644 index 00000000..e1b9a06d Binary files /dev/null and b/.playwright/screenshots/enriched-text-unordered-list-empty-items.png differ diff --git a/.playwright/screenshots/enriched-text-unordered-list.png b/.playwright/screenshots/enriched-text-unordered-list.png new file mode 100644 index 00000000..5f98bdc9 Binary files /dev/null and b/.playwright/screenshots/enriched-text-unordered-list.png differ diff --git a/.playwright/tests/enrichedTextVisual.spec.ts b/.playwright/tests/enrichedTextVisual.spec.ts new file mode 100644 index 00000000..63723852 --- /dev/null +++ b/.playwright/tests/enrichedTextVisual.spec.ts @@ -0,0 +1,235 @@ +import { test, expect, type Locator, type Page } from '@playwright/test'; + +test.setTimeout(90_000); + +const sel = { + root: '[data-testid="test-enriched-text-root"]', + htmlInput: '[data-testid="test-enriched-text-html-input"]', + setValueButton: '[data-testid="test-enriched-text-set-value-button"]', + valueOutput: '[data-testid="test-enriched-text-value-output"]', + display: '[data-testid="test-enriched-text-display"]', + displayInner: '[data-testid="test-enriched-text-display"] .et-view', +} as const; + +function displayLocator(page: Page): Locator { + return page.locator(sel.display); +} + +async function gotoTestEnrichedText(page: Page): Promise { + await page.goto('/test-enriched-text'); + await page.waitForSelector(sel.displayInner); +} + +async function setEnrichedTextValue(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); +} + +test.describe('EnrichedText display visual regression', () => { + const cases: { name: string; snapshot: string; html: string }[] = [ + { + name: 'rich text: heading, bold, italic and link', + snapshot: 'enriched-text-rich-text.png', + html: [ + '', + '

Heading

', + '

Some bold and italic text.

', + '

Some mixed text.

', + '

A link here.

', + '

A bold link here.

', + '', + ].join(''), + }, + { + name: 'unordered list', + snapshot: 'enriched-text-unordered-list.png', + html: '
  • Alpha
  • Beta
  • Gamma
', + }, + { + name: 'unordered list with empty items', + snapshot: 'enriched-text-unordered-list-empty-items.png', + html: '

Empty lists

  • Alpha
  • Gamma

bottom

', + }, + { + name: 'ordered list', + snapshot: 'enriched-text-ordered-list.png', + html: '
  1. One
  2. Two
  3. Three
', + }, + { + name: 'ordered list with empty items', + snapshot: 'enriched-text-ordered-list-empty-items.png', + html: '

Empty lists

  1. One
  2. Three

bottom

', + }, + { + name: 'checkbox list all unchecked', + snapshot: 'enriched-text-checkbox-list-unchecked.png', + html: '
  • one
  • two
', + }, + { + name: 'checkbox list with checked item', + snapshot: 'enriched-text-checkbox-list-checked.png', + html: '
  • one
  • two
', + }, + { + name: 'checkbox list with empty items', + snapshot: 'enriched-text-checkbox-list-empty-items.png', + html: '

Empty lists

  • one
  • three

bottom

', + }, + ]; + + for (const c of cases) { + test(c.name, async ({ page }) => { + await gotoTestEnrichedText(page); + await setEnrichedTextValue(page, c.html); + + await expect(displayLocator(page)).toHaveScreenshot(c.snapshot); + }); + } +}); + +test.describe('visual: complex lists and layouts', () => { + const cases = [ + { + name: 'all 3 types of the list at once', + snapshot: 'enriched-text-all-list-types.png', + html: [ + '', + '
  • Bullet item
', + '
  1. Numbered item
', + '
  • Checked item
  • Unchecked item
', + '', + ].join(''), + }, + ]; + + for (const c of cases) { + test(c.name, async ({ page }) => { + await gotoTestEnrichedText(page); + await setEnrichedTextValue(page, c.html); + await expect(displayLocator(page)).toHaveScreenshot(c.snapshot); + }); + } +}); + +test.describe('visual: typography, blocks, and wrapping', () => { + const cases = [ + { + name: 'all 6 headings', + snapshot: 'enriched-text-all-headings.png', + html: '

Heading 1

Heading 2

Heading 3

Heading 4

Heading 5
Heading 6
', + }, + { + name: 'blockquote, code, codeblock', + snapshot: 'enriched-text-blockquote-code-codeblock.png', + html: [ + '', + '

This is a blockquote. Blockquote for quoting in a block.

', + '

Here is some inline code mixed in text.

', + '

function test() {

return true;

}

', + '', + ].join(''), + }, + { + name: 'multiple newlines and multiple spaces', + snapshot: 'enriched-text-newlines-spaces.png', + html: [ + '', + '

Word spaced out a lot.

', + '


', + '


', + '

Text after empty newlines.

', + '', + ].join(''), + }, + { + name: 'line wrapping', + snapshot: 'enriched-text-line-wrapping.png', + html: [ + '', + '

This is a standard paragraph with enough text that it should naturally wrap to the next line when it reaches the edge of the container.

', + '

SuperLongWordWithoutAnySpacesThatShouldForceTheWordBreakOrOverflowWrapRuleToKickInAndPreventTheLayoutFromBreakingHorizontally

', + '', + ].join(''), + }, + ]; + + for (const c of cases) { + test(c.name, async ({ page }) => { + await gotoTestEnrichedText(page); + await setEnrichedTextValue(page, c.html); + await expect(displayLocator(page)).toHaveScreenshot(c.snapshot); + }); + } +}); + +test.describe('visual: mentions', () => { + test('display mentions', async ({ page }) => { + await gotoTestEnrichedText(page); + await setEnrichedTextValue( + page, + '

Hello @John Doe!

' + ); + await expect(displayLocator(page)).toHaveScreenshot( + 'enriched-text-mentions.png' + ); + }); +}); + +test.describe('visual: images', () => { + test.beforeEach(async ({ page }) => { + const routePattern = '**/pw-e2e-ok.png'; + const pngBody = Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', + 'base64' + ); + await page.route(routePattern, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'image/png', + body: pngBody, + }); + }); + + // Abort broken image to force placeholder + const brokenPattern = '**/pw-e2e-broken.png'; + await page.route(brokenPattern, (route) => route.abort()); + }); + + const cases = [ + { + name: 'inline images next to some text', + snapshot: 'enriched-text-images-inline.png', + html: '

Start text end text.

', + }, + { + name: 'inline images inside list', + snapshot: 'enriched-text-images-inside-list.png', + html: '
  • Bullet item with image.
', + }, + { + name: 'image placeholder display next to some text', + snapshot: 'enriched-text-images-placeholder-inline.png', + html: '

Look at this broken picture.

', + }, + { + name: 'image placeholder inside lists', + snapshot: 'enriched-text-images-placeholder-list.png', + html: '
  1. List with a broken image inside.
', + }, + ]; + + for (const c of cases) { + test(c.name, async ({ page }) => { + await gotoTestEnrichedText(page); + await setEnrichedTextValue(page, c.html); + + await page.waitForTimeout(100); + + await expect(displayLocator(page)).toHaveScreenshot(c.snapshot); + }); + } +}); diff --git a/apps/example-web/src/App.css b/apps/example-web/src/App.css index ab8b5de9..cce452ab 100644 --- a/apps/example-web/src/App.css +++ b/apps/example-web/src/App.css @@ -29,6 +29,10 @@ body { align-items: center; } +.enriched-text-container { + width: 100%; +} + .app-title { font-size: 24px; font-weight: bold; diff --git a/apps/example-web/src/App.tsx b/apps/example-web/src/App.tsx index 8ad87c25..7cf449f5 100644 --- a/apps/example-web/src/App.tsx +++ b/apps/example-web/src/App.tsx @@ -14,9 +14,10 @@ import { type OnSubmitEditing, type OnChangeMentionEvent, type OnMentionDetected, + EnrichedText, } from 'react-native-enriched-html'; import { WEB_DEFAULT_HTML_STYLE } from './defaultHtmlStyle'; -import type { NativeSyntheticEvent } from 'react-native'; +import type { NativeSyntheticEvent, TextStyle } from 'react-native'; import { EditorActions } from './components/EditorActions'; import { SetValueModal } from './components/SetValueModal'; import { ImageModal } from './components/ImageModal'; @@ -53,6 +54,8 @@ function App() { const [isLinkModalOpen, setIsLinkModalOpen] = useState(false); const [isImageModalOpen, setIsImageModalOpen] = useState(false); + const [enrichedTextValue, setEnrichedTextValue] = useState(''); + const isLinkActive = !!editorState?.link.isActive; const hasLinkUrl = currentLink.url.length > 0; const hasLinkSpan = currentLink.start !== 0 || currentLink.end !== 0; @@ -226,6 +229,18 @@ function App() { } }; + const handleSetEnrichedTextValue = () => { + ref.current + ?.getHTML() + .then((html) => { + setEnrichedTextValue(html); + }) + .catch((error: unknown) => { + setEnrichedTextValue(''); + console.error('Failed to get HTML:', error); + }); + }; + return (

Enriched Text Input

@@ -303,8 +318,26 @@ function App() { }} /> + + {showHtmlOutput && } +
+

Enriched Text

+ + {enrichedTextValue} + +
+ {isSetValueModalOpen && ( { @@ -345,4 +378,14 @@ const enrichedInputStyle: EnrichedInputStyle = { fontSize: 18, }; +const enrichedTextStyle: TextStyle = { + backgroundColor: 'gainsboro', + width: '100%', + marginVertical: 12, + paddingVertical: 12, + paddingHorizontal: 14, + borderRadius: 8, + fontSize: 18, +}; + export default App; diff --git a/apps/example-web/src/RouteSelector.tsx b/apps/example-web/src/RouteSelector.tsx index d04f3d2c..c31df8dd 100644 --- a/apps/example-web/src/RouteSelector.tsx +++ b/apps/example-web/src/RouteSelector.tsx @@ -4,6 +4,7 @@ import { TestLinks } from './testScreens/TestLinks'; import { TestSetSelection } from './testScreens/TestSetSelection'; import { VisualRegression } from './testScreens/VisualRegression'; import { TestSubmitProps } from './testScreens/TestSubmitProps'; +import { TestEnrichedText } from './testScreens/TestEnrichedText'; import { useEffect, useState } from 'react'; export default function RouteSelector() { @@ -40,5 +41,9 @@ export default function RouteSelector() { return ; } + if (path === '/test-enriched-text') { + return ; + } + return ; } diff --git a/apps/example-web/src/testScreens/TestEnrichedText.tsx b/apps/example-web/src/testScreens/TestEnrichedText.tsx new file mode 100644 index 00000000..912031dc --- /dev/null +++ b/apps/example-web/src/testScreens/TestEnrichedText.tsx @@ -0,0 +1,60 @@ +import { useState, type ChangeEvent } from 'react'; +import { EnrichedText } from 'react-native-enriched-html'; +import type { TextStyle } from 'react-native'; +import { WEB_DEFAULT_HTML_STYLE } from '../defaultHtmlStyle'; + +const INITIAL_VALUE = '

'; + +export function TestEnrichedText() { + const [htmlInput, setHtmlInput] = useState(INITIAL_VALUE); + const [value, setValue] = useState(INITIAL_VALUE); + + return ( +
+
+ + {value} + +
+ +