Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
235 changes: 235 additions & 0 deletions .playwright/tests/enrichedTextVisual.spec.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
await page.goto('/test-enriched-text');
await page.waitForSelector(sel.displayInner);
}

async function setEnrichedTextValue(page: Page, html: string): Promise<void> {
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: [
'<html>',
'<h3>Heading</h3>',
'<p>Some <b>bold</b> and <i>italic</i> text.</p>',
'<p><b>S</i>om</i>e</b> <b>mix</b><i>ed</i> <s>t<u>ex</u>t</s>.</p>',
'<p>A <a href="https://example.com">link</a> here.</p>',
'<p>A bold <a href="https://example.com">l<b>in<b/>k</a> here.</p>',
'</html>',
].join(''),
},
{
name: 'unordered list',
snapshot: 'enriched-text-unordered-list.png',
html: '<html><ul><li>Alpha</li><li>Beta</li><li>Gamma</li></ul></html>',
},
{
name: 'unordered list with empty items',
snapshot: 'enriched-text-unordered-list-empty-items.png',
html: '<html><h4>Empty lists</h4><ul><li></li><li>Alpha</li><li></li><li>Gamma</li><li></li></ul><p>bottom</p></html>',
},
{
name: 'ordered list',
snapshot: 'enriched-text-ordered-list.png',
html: '<html><ol><li>One</li><li>Two</li><li>Three</li></ol></html>',
},
{
name: 'ordered list with empty items',
snapshot: 'enriched-text-ordered-list-empty-items.png',
html: '<html><h4>Empty lists</h4><ol><li></li><li>One</li><li></li><li>Three</li><li></li></ol><p>bottom</p></html>',
},
{
name: 'checkbox list all unchecked',
snapshot: 'enriched-text-checkbox-list-unchecked.png',
html: '<html><ul data-type="checkbox"><li>one</li><li>two</li></ul></html>',
},
{
name: 'checkbox list with checked item',
snapshot: 'enriched-text-checkbox-list-checked.png',
html: '<html><ul data-type="checkbox"><li checked>one</li><li>two</li></ul></html>',
},
{
name: 'checkbox list with empty items',
snapshot: 'enriched-text-checkbox-list-empty-items.png',
html: '<html><h4>Empty lists</h4><ul data-type="checkbox"><li></li><li checked>one</li><li></li><li>three</li><li checked></li></ul><p>bottom</p></html>',
},
];

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: [
'<html>',
'<ul><li>Bullet item</li></ul>',
'<ol><li>Numbered item</li></ol>',
'<ul data-type="checkbox"><li checked>Checked item</li><li>Unchecked item</li></ul>',
'</html>',
].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: '<html><h1>Heading 1</h1><h2>Heading 2</h2><h3>Heading 3</h3><h4>Heading 4</h4><h5>Heading 5</h5><h6>Heading 6</h6></html>',
},
{
name: 'blockquote, code, codeblock',
snapshot: 'enriched-text-blockquote-code-codeblock.png',
html: [
'<html>',
'<blockquote><p>This is a blockquote. Blockquote for quoting in a block.</p></blockquote>',
'<p>Here is some <code>inline code</code> mixed in text.</p>',
'<codeblock><p>function test() {</p><p> return true;</p><p>}</p></codeblock>',
'</html>',
].join(''),
},
{
name: 'multiple newlines and multiple spaces',
snapshot: 'enriched-text-newlines-spaces.png',
html: [
'<html>',
'<p>Word spaced out a lot.</p>',
'<p><br></p>',
'<p><br></p>',
'<p>Text after empty newlines.</p>',
'</html>',
].join(''),
},
{
name: 'line wrapping',
snapshot: 'enriched-text-line-wrapping.png',
html: [
'<html>',
'<p>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.</p>',
'<p>SuperLongWordWithoutAnySpacesThatShouldForceTheWordBreakOrOverflowWrapRuleToKickInAndPreventTheLayoutFromBreakingHorizontally</p>',
'</html>',
].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,
'<html><p>Hello <mention indicator="@" text="@John Doe">@Jo<s>hn D</s>oe</mention>!</p></html>'
);
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: '<html><p>Start text <img src="/pw-e2e-ok.png" width="40" height="40" /> end text.</p></html>',
},
{
name: 'inline images inside list',
snapshot: 'enriched-text-images-inside-list.png',
html: '<html><ul><li>Bullet item <img src="/pw-e2e-ok.png" width="20" height="20" /> with image.</li></ul></html>',
},
{
name: 'image placeholder display next to some text',
snapshot: 'enriched-text-images-placeholder-inline.png',
html: '<html><p>Look at this broken <img src="/pw-e2e-broken.png" width="60" height="60" /> picture.</p></html>',
},
{
name: 'image placeholder inside lists',
snapshot: 'enriched-text-images-placeholder-list.png',
html: '<html><ol><li>List with a broken image <img src="" width="20" height="20" /> inside.</li></ol></html>',
},
];

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);
});
}
});
4 changes: 4 additions & 0 deletions apps/example-web/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ body {
align-items: center;
}

.enriched-text-container {
width: 100%;
}

.app-title {
font-size: 24px;
font-weight: bold;
Expand Down
45 changes: 44 additions & 1 deletion apps/example-web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 (
<div className="container">
<h1 className="app-title">Enriched Text Input</h1>
Expand Down Expand Up @@ -303,8 +318,26 @@ function App() {
}}
/>

<button
className="btn btn-full"
data-testid="set-enriched-text-value"
onClick={handleSetEnrichedTextValue}
>
Set Enriched Text
</button>

{showHtmlOutput && <HtmlOutputPanel html={currentHtml} />}

<div className="container enriched-text-container">
<h1 className="app-title">Enriched Text</h1>
<EnrichedText
style={enrichedTextStyle}
htmlStyle={WEB_DEFAULT_HTML_STYLE}
>
{enrichedTextValue}
</EnrichedText>
</div>

{isSetValueModalOpen && (
<SetValueModal
onSetValue={(value) => {
Expand Down Expand Up @@ -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;
5 changes: 5 additions & 0 deletions apps/example-web/src/RouteSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -40,5 +41,9 @@ export default function RouteSelector() {
return <TestMentions />;
}

if (path === '/test-enriched-text') {
return <TestEnrichedText />;
}

return <App />;
}
Loading
Loading