Skip to content

Commit 61b66e3

Browse files
test: add tests for textShortcuts
1 parent de5fcf5 commit 61b66e3

6 files changed

Lines changed: 294 additions & 0 deletions

File tree

.playwright/helpers/visual-regression.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const visualRegressionSelectors = {
77
setValueButton: '[data-testid="visual-regression-set-value-button"]',
88
editorHtmlOutput: '[data-testid="visual-regression-editor-html-output"]',
99
htmlStyleOverride: '[data-testid="visual-regression-html-style-override"]',
10+
textShortcutsOverride: '[data-testid="visual-regression-text-shortcuts"]',
1011
} as const;
1112

1213
export function editorLocator(page: Page): Locator {
@@ -52,3 +53,10 @@ export async function setHtmlStyleOverride(
5253
): Promise<void> {
5354
await page.fill(visualRegressionSelectors.htmlStyleOverride, json);
5455
}
56+
57+
export async function setTextShortcutsOverride(
58+
page: Page,
59+
json: string
60+
): Promise<void> {
61+
await page.fill(visualRegressionSelectors.textShortcutsOverride, json);
62+
}
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import { test, expect, type Page } from '@playwright/test';
2+
3+
import {
4+
focusEnrichedEditable,
5+
getSerializedHtml,
6+
gotoVisualRegression,
7+
setEditorHtml,
8+
setTextShortcutsOverride,
9+
} from '../helpers/visual-regression';
10+
import { toolbarButton } from '../helpers/toolbar';
11+
12+
const DELAY_MS = 80;
13+
14+
async function typeText(page: Page, text: string): Promise<void> {
15+
const editor = await focusEnrichedEditable(page);
16+
await editor.pressSequentially(text, { delay: DELAY_MS });
17+
}
18+
19+
test.describe('text shortcuts — paragraph (default)', () => {
20+
test.beforeEach(async ({ page }) => {
21+
await gotoVisualRegression(page);
22+
});
23+
24+
test('typing "- " at paragraph start creates an unordered list', async ({
25+
page,
26+
}) => {
27+
await setEditorHtml(page, '<html><p></p></html>');
28+
await typeText(page, '- ');
29+
30+
await expect
31+
.poll(async () => {
32+
const html = await getSerializedHtml(page);
33+
return /<ul/i.test(html) && /<li/i.test(html);
34+
})
35+
.toBe(true);
36+
});
37+
38+
test('typing "1. " at paragraph start creates an ordered list', async ({
39+
page,
40+
}) => {
41+
await setEditorHtml(page, '<html><p></p></html>');
42+
await typeText(page, '1. ');
43+
44+
await expect
45+
.poll(async () => {
46+
const html = await getSerializedHtml(page);
47+
return /<ol/i.test(html) && /<li/i.test(html);
48+
})
49+
.toBe(true);
50+
});
51+
52+
test('typing "- " in the middle of text does not trigger list shortcut', async ({
53+
page,
54+
}) => {
55+
await setEditorHtml(page, '<html><p></p></html>');
56+
// Type some text first so "- " is not at the paragraph start
57+
await typeText(page, 'hello - ');
58+
59+
await expect.poll(async () => getSerializedHtml(page)).toMatch(/hello - /);
60+
61+
const html = await getSerializedHtml(page);
62+
expect(html).not.toMatch(/<ul/i);
63+
});
64+
65+
test('typing "- " inside an existing list does not create a new list', async ({
66+
page,
67+
}) => {
68+
await setEditorHtml(page, '<html><ul><li>existing item</li></ul></html>');
69+
const editor = await focusEnrichedEditable(page);
70+
await editor.press('End');
71+
await editor.pressSequentially('1. ', { delay: DELAY_MS });
72+
73+
const html = await getSerializedHtml(page);
74+
expect(html).not.toMatch(/<ol/i);
75+
});
76+
});
77+
78+
test.describe('text shortcuts — paragraph (custom)', () => {
79+
test.beforeEach(async ({ page }) => {
80+
await gotoVisualRegression(page);
81+
});
82+
83+
test('custom "# " shortcut converts to h1', async ({ page }) => {
84+
await setTextShortcutsOverride(page, '[{"trigger":"# ","style":"h1"}]');
85+
await setEditorHtml(page, '<html><p></p></html>');
86+
await typeText(page, '# ');
87+
88+
await expect.poll(async () => getSerializedHtml(page)).toMatch(/<h1/i);
89+
});
90+
91+
test('custom "> " shortcut converts to blockquote', async ({ page }) => {
92+
await setTextShortcutsOverride(
93+
page,
94+
'[{"trigger":"> ","style":"blockquote"}]'
95+
);
96+
await setEditorHtml(page, '<html><p></p></html>');
97+
await typeText(page, '> ');
98+
99+
await expect
100+
.poll(async () => getSerializedHtml(page))
101+
.toMatch(/<blockquote/i);
102+
});
103+
104+
test('custom paragraph shortcut trigger text is removed from the output', async ({
105+
page,
106+
}) => {
107+
await setTextShortcutsOverride(page, '[{"trigger":"# ","style":"h1"}]');
108+
await setEditorHtml(page, '<html><p></p></html>');
109+
await typeText(page, '# ');
110+
111+
const html = await getSerializedHtml(page);
112+
expect(html).not.toMatch(/# /);
113+
});
114+
115+
test('empty textShortcuts array disables all shortcuts', async ({ page }) => {
116+
await setTextShortcutsOverride(page, '[]');
117+
await setEditorHtml(page, '<html><p></p></html>');
118+
await typeText(page, '- ');
119+
120+
const html = await getSerializedHtml(page);
121+
expect(html).not.toMatch(/<ul/i);
122+
expect(html).toMatch(/- /);
123+
});
124+
});
125+
126+
test.describe('text shortcuts — inline (custom)', () => {
127+
test.beforeEach(async ({ page }) => {
128+
await gotoVisualRegression(page);
129+
});
130+
131+
test('single-char delimiter: "*hello*" applies italic', async ({ page }) => {
132+
await setTextShortcutsOverride(page, '[{"trigger":"*","style":"italic"}]');
133+
await setEditorHtml(page, '<html><p></p></html>');
134+
await typeText(page, '*hello*');
135+
136+
await expect
137+
.poll(async () => getSerializedHtml(page))
138+
.toMatch(/<i>hello<\/i>/i);
139+
});
140+
141+
test('double-char delimiter: "**hello**" applies bold', async ({ page }) => {
142+
await setTextShortcutsOverride(page, '[{"trigger":"**","style":"bold"}]');
143+
await setEditorHtml(page, '<html><p></p></html>');
144+
await typeText(page, '**hello**');
145+
146+
await expect
147+
.poll(async () => getSerializedHtml(page))
148+
.toMatch(/<b>hello<\/b>/i);
149+
});
150+
151+
test('backtick delimiter: "`hello`" applies inline code', async ({
152+
page,
153+
}) => {
154+
await setTextShortcutsOverride(
155+
page,
156+
'[{"trigger":"`","style":"inline_code"}]'
157+
);
158+
await setEditorHtml(page, '<html><p></p></html>');
159+
await typeText(page, '`hello`');
160+
161+
await expect
162+
.poll(async () => getSerializedHtml(page))
163+
.toMatch(/<code>hello<\/code>/i);
164+
});
165+
166+
test('inline shortcut removes both delimiters from output', async ({
167+
page,
168+
}) => {
169+
await setTextShortcutsOverride(page, '[{"trigger":"*","style":"italic"}]');
170+
await setEditorHtml(page, '<html><p></p></html>');
171+
await typeText(page, '*hello*');
172+
173+
const html = await getSerializedHtml(page);
174+
expect(html).not.toMatch(/\*/);
175+
});
176+
177+
test('longer trigger takes precedence: "**" does not trigger "*" shortcut', async ({
178+
page,
179+
}) => {
180+
await setTextShortcutsOverride(
181+
page,
182+
'[{"trigger":"*","style":"italic"},{"trigger":"**","style":"bold"}]'
183+
);
184+
await setEditorHtml(page, '<html><p></p></html>');
185+
await typeText(page, '**hello**');
186+
187+
const html = await getSerializedHtml(page);
188+
// Should apply bold, NOT italic
189+
expect(html).toMatch(/<b>hello<\/b>/i);
190+
expect(html).not.toMatch(/<i>/i);
191+
});
192+
193+
test('inline shortcut does not fire when there is no matching opening delimiter', async ({
194+
page,
195+
}) => {
196+
await setTextShortcutsOverride(page, '[{"trigger":"*","style":"italic"}]');
197+
await setEditorHtml(page, '<html><p></p></html>');
198+
await typeText(page, 'hello*');
199+
200+
const html = await getSerializedHtml(page);
201+
expect(html).not.toMatch(/<i>/i);
202+
expect(html).toMatch(/hello\*/);
203+
});
204+
});
205+
206+
test.describe('text shortcuts — mark clearing after inline shortcut', () => {
207+
test.beforeEach(async ({ page }) => {
208+
await gotoVisualRegression(page);
209+
});
210+
211+
test('text typed immediately after bold shortcut is not bold', async ({
212+
page,
213+
}) => {
214+
await setTextShortcutsOverride(page, '[{"trigger":"**","style":"bold"}]');
215+
await setEditorHtml(page, '<html><p></p></html>');
216+
217+
// Apply bold via shortcut
218+
await typeText(page, '**hello**');
219+
220+
// Type more text right after — it should NOT be bold
221+
const editor = await focusEnrichedEditable(page);
222+
await editor.pressSequentially(' world', { delay: DELAY_MS });
223+
224+
await expect(toolbarButton(page, 'bold')).not.toHaveClass(
225+
/toolbar-btn--active/
226+
);
227+
228+
const html = await getSerializedHtml(page);
229+
// " world" must be outside the <b> tag
230+
expect(html).not.toMatch(/<b>hello world<\/b>/i);
231+
expect(html).toMatch(/<b>hello<\/b>/i);
232+
});
233+
234+
test('text typed immediately after italic shortcut is not italic', async ({
235+
page,
236+
}) => {
237+
await setTextShortcutsOverride(page, '[{"trigger":"*","style":"italic"}]');
238+
await setEditorHtml(page, '<html><p></p></html>');
239+
240+
await typeText(page, '*hello*');
241+
242+
const editor = await focusEnrichedEditable(page);
243+
await editor.pressSequentially(' world', { delay: DELAY_MS });
244+
245+
await expect(toolbarButton(page, 'italic')).not.toHaveClass(
246+
/toolbar-btn--active/
247+
);
248+
249+
const html = await getSerializedHtml(page);
250+
expect(html).not.toMatch(/<i>hello world<\/i>/i);
251+
expect(html).toMatch(/<i>hello<\/i>/i);
252+
});
253+
});

apps/example-web/src/testScreens/VisualRegression.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
type EnrichedTextInputInstance,
66
type HtmlStyle,
77
type OnChangeStateEvent,
8+
type TextShortcut,
89
} from 'react-native-enriched-html';
910
import { Toolbar } from '../components/Toolbar';
1011
import { WEB_DEFAULT_HTML_STYLE } from '../defaultHtmlStyle';
@@ -35,6 +36,7 @@ export function VisualRegression() {
3536
);
3637
const [editorHtml, setEditorHtml] = useState('');
3738
const [htmlStyleOverrideJson, setHtmlStyleOverrideJson] = useState('');
39+
const [textShortcutsJson, setTextShortcutsJson] = useState('');
3840

3941
const htmlStyle = useMemo<HtmlStyle>(() => {
4042
const raw = htmlStyleOverrideJson.trim();
@@ -49,6 +51,17 @@ export function VisualRegression() {
4951
}
5052
}, [htmlStyleOverrideJson]);
5153

54+
const textShortcuts = useMemo<TextShortcut[] | undefined>(() => {
55+
const raw = textShortcutsJson.trim();
56+
if (!raw) return undefined;
57+
try {
58+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
59+
return JSON.parse(raw) as TextShortcut[];
60+
} catch {
61+
return undefined;
62+
}
63+
}, [textShortcutsJson]);
64+
5265
const handleSetValue = () => {
5366
ref.current?.setValue(htmlInput);
5467
};
@@ -77,6 +90,7 @@ export function VisualRegression() {
7790
onChangeState={(e) => {
7891
setEditorState(e.nativeEvent);
7992
}}
93+
textShortcuts={textShortcuts}
8094
/>
8195
</div>
8296

@@ -105,6 +119,16 @@ export function VisualRegression() {
105119
rows={3}
106120
style={styles.htmlStyleOverrideInput}
107121
/>
122+
<textarea
123+
data-testid="visual-regression-text-shortcuts"
124+
value={textShortcutsJson}
125+
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
126+
setTextShortcutsJson(e.target.value);
127+
}}
128+
placeholder={'e.g. [{"trigger":"* ","style":"italic"}]'}
129+
rows={2}
130+
style={styles.htmlStyleOverrideInput}
131+
/>
108132
<textarea
109133
data-testid="visual-regression-html-input"
110134
value={htmlInput}

src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ export type {
2121
EnrichedTextHtmlStyle,
2222
OnMentionPressEvent,
2323
OnLinkPressEvent,
24+
TextShortcut,
2425
} from './types';

src/web/formats/EnrichedOrderedList.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { OrderedList } from '@tiptap/extension-list';
33
import { applyWrappingListToSelection } from './applyWrappingListToSelection';
44

55
export const EnrichedOrderedList = OrderedList.extend({
6+
addInputRules() {
7+
return [];
8+
},
9+
610
addKeyboardShortcuts() {
711
return {};
812
},

src/web/formats/EnrichedUnorderedList.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ declare module '@tiptap/core' {
1414
export const EnrichedUnorderedList = BulletList.extend({
1515
name: 'unorderedList',
1616

17+
addInputRules() {
18+
return [];
19+
},
20+
1721
addKeyboardShortcuts() {
1822
return {};
1923
},

0 commit comments

Comments
 (0)