Skip to content

Commit 0596847

Browse files
authored
refactor: parse pasted content from html and md (#163)
* refactor: paste text and add test for pasting * fix: paste bullet list with empty line * chore: lint * chore: fmt code * chore: fmt jest test * chore: fix test * chore: lint * chore: fix table
1 parent 2629852 commit 0596847

33 files changed

+5926
-240
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ coverage
3434

3535
cypress/snapshots/**/__diff_output__/
3636
.claude
37+
.gemini*
3738
cypress/screenshots
3839
cypress/videos
3940
cypress/downloads

cypress/e2e/page/edit-page.cy.ts

Lines changed: 54 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { AuthTestUtils } from '../../support/auth-utils';
22
import { TestTool } from '../../support/page-utils';
3-
import { PageSelectors, ModalSelectors, waitForReactUpdate } from '../../support/selectors';
3+
import { AddPageSelectors, EditorSelectors, ModalSelectors, PageSelectors, SpaceSelectors, waitForReactUpdate } from '../../support/selectors';
44
import { generateRandomEmail } from '../../support/test-config';
55
import { testLog } from '../../support/test-helpers';
66

@@ -47,74 +47,75 @@ describe('Page Edit Tests', () => {
4747
cy.wait(2000);
4848

4949
// Step 2: Create a new page using the simpler approach
50-
testLog.info( '=== Starting Page Creation for Edit Test ===');
51-
testLog.info( `Target page name: ${testPageName}`);
52-
53-
// Click new page button
54-
PageSelectors.newPageButton().should('be.visible').click();
50+
testLog.info('=== Starting Page Creation for Edit Test ===');
51+
testLog.info(`Target page name: ${testPageName}`);
52+
53+
// Expand General space to ensure we can see the content
54+
testLog.info('Expanding General space');
55+
SpaceSelectors.itemByName('General').first().click();
56+
waitForReactUpdate(500);
57+
58+
// Use inline add button on General space
59+
testLog.info('Creating new page in General space');
60+
SpaceSelectors.itemByName('General').first().within(() => {
61+
AddPageSelectors.inlineAddButton().first().should('be.visible').click();
62+
});
63+
waitForReactUpdate(1000);
64+
65+
// Select first item (Page) from the menu
66+
cy.get('[role="menuitem"]').first().click();
5567
waitForReactUpdate(1000);
56-
57-
// Handle the new page modal
58-
ModalSelectors.newPageModal().should('be.visible').within(() => {
59-
// Select the first available space
60-
ModalSelectors.spaceItemInModal().first().click();
61-
waitForReactUpdate(500);
62-
// Click Add button
63-
cy.contains('button', 'Add').click();
68+
69+
// Handle the new page modal if it appears (defensive)
70+
cy.get('body').then(($body) => {
71+
if ($body.find('[data-testid="new-page-modal"]').length > 0) {
72+
testLog.info('Handling new page modal');
73+
ModalSelectors.newPageModal().should('be.visible').within(() => {
74+
ModalSelectors.spaceItemInModal().first().click();
75+
waitForReactUpdate(500);
76+
cy.contains('button', 'Add').click();
77+
});
78+
cy.wait(3000);
79+
}
6480
});
65-
66-
// Wait for navigation to the new page
67-
cy.wait(3000);
68-
69-
// Close any modal dialogs
81+
82+
// Close any remaining modal dialogs
7083
cy.get('body').then(($body: JQuery<HTMLBodyElement>) => {
7184
if ($body.find('[role="dialog"]').length > 0 || $body.find('.MuiDialog-container').length > 0) {
72-
testLog.info( 'Closing modal dialog');
85+
testLog.info('Closing modal dialog');
7386
cy.get('body').type('{esc}');
7487
cy.wait(1000);
7588
}
7689
});
77-
90+
91+
// Click the newly created "Untitled" page
92+
testLog.info('Selecting the new Untitled page');
93+
PageSelectors.itemByName('Untitled').should('be.visible').click();
94+
waitForReactUpdate(1000);
95+
7896
// Step 3: Add content to the page editor
79-
testLog.info( '=== Adding Content to Page ===');
80-
81-
// Find the editor and add content
82-
cy.get('[contenteditable="true"]').then($editors => {
83-
testLog.info( `Found ${$editors.length} editable elements`);
84-
85-
// Look for the main editor (not the title)
86-
let editorFound = false;
87-
$editors.each((index: number, el: HTMLElement) => {
88-
const $el = Cypress.$(el);
89-
// Skip title inputs
90-
if (!$el.attr('data-testid')?.includes('title') && !$el.hasClass('editor-title')) {
91-
testLog.info( `Using editor at index ${index}`);
92-
cy.wrap(el).click().type(testContent.join('{enter}'));
93-
editorFound = true;
94-
return false; // break the loop
95-
}
96-
});
97-
98-
if (!editorFound) {
99-
// Fallback: use the last contenteditable element
100-
testLog.info( 'Using fallback: last contenteditable element');
101-
cy.wrap($editors.last()).click().type(testContent.join('{enter}'));
102-
}
103-
});
104-
97+
testLog.info('=== Adding Content to Page ===');
98+
99+
// Wait for editor to be available and add content
100+
testLog.info('Waiting for editor to be available');
101+
EditorSelectors.firstEditor().should('exist', { timeout: 15000 });
102+
103+
testLog.info('Writing content to editor');
104+
EditorSelectors.firstEditor().click().type(testContent.join('{enter}'));
105+
105106
// Wait for content to be saved
106107
cy.wait(2000);
107-
108+
108109
// Step 4: Verify the content was added
109-
testLog.info( '=== Verifying Content ===');
110-
110+
testLog.info('=== Verifying Content ===');
111+
111112
// Verify each line of content exists in the page
112113
testContent.forEach(line => {
113114
cy.contains(line).should('exist');
114-
testLog.info( `✓ Found content: "${line}"`);
115+
testLog.info(`✓ Found content: "${line}"`);
115116
});
116-
117-
testLog.info( '=== Test completed successfully ===');
117+
118+
testLog.info('=== Test completed successfully ===');
118119
});
119120
});
120121
});

cypress/e2e/page/paste-code.cy.ts

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { createTestPage, pasteContent } from '../../support/paste-utils';
2+
import { testLog } from '../../support/test-helpers';
3+
4+
describe('Paste Code Block Tests', () => {
5+
it('should paste all code block formats correctly', () => {
6+
createTestPage();
7+
8+
// HTML Code Blocks
9+
{
10+
const html = '<pre><code>const x = 10;\nconsole.log(x);</code></pre>';
11+
const plainText = 'const x = 10;\nconsole.log(x);';
12+
13+
testLog.info('=== Pasting HTML Code Block ===');
14+
pasteContent(html, plainText);
15+
16+
cy.wait(1000);
17+
18+
// CodeBlock component structure: .relative.w-full > pre > code
19+
cy.get('[contenteditable="true"]').find('pre').find('code').should('contain', 'const x = 10');
20+
testLog.info('✓ HTML code block pasted successfully');
21+
}
22+
23+
{
24+
const html = '<pre><code class="language-javascript">function hello() {\n console.log("Hello");\n}</code></pre>';
25+
const plainText = 'function hello() {\n console.log("Hello");\n}';
26+
27+
testLog.info('=== Pasting HTML Code Block with Language ===');
28+
pasteContent(html, plainText);
29+
30+
cy.wait(1000);
31+
32+
cy.get('[contenteditable="true"]').find('pre').find('code').should('contain', 'function hello');
33+
testLog.info('✓ HTML code block with language pasted successfully');
34+
}
35+
36+
{
37+
const html = `
38+
<pre><code class="language-python">def greet():
39+
print("Hello")</code></pre>
40+
<pre><code class="language-typescript">const greeting: string = "Hello";</code></pre>
41+
`;
42+
const plainText = 'def greet():\n print("Hello")\nconst greeting: string = "Hello";';
43+
44+
testLog.info('=== Pasting HTML Multiple Language Code Blocks ===');
45+
pasteContent(html, plainText);
46+
47+
cy.wait(1000);
48+
49+
cy.get('[contenteditable="true"]').find('pre').find('code').should('contain', 'def greet');
50+
cy.get('[contenteditable="true"]').find('pre').find('code').should('contain', 'const greeting');
51+
testLog.info('✓ HTML multiple language code blocks pasted successfully');
52+
}
53+
54+
{
55+
const html = '<blockquote>This is a quoted text</blockquote>';
56+
const plainText = 'This is a quoted text';
57+
58+
testLog.info('=== Pasting HTML Blockquote ===');
59+
pasteContent(html, plainText);
60+
61+
cy.wait(1000);
62+
63+
// AppFlowy renders blockquote as div with data-block-type="quote"
64+
cy.get('[contenteditable="true"]').find('[data-block-type="quote"]').should('contain', 'This is a quoted text');
65+
testLog.info('✓ HTML blockquote pasted successfully');
66+
}
67+
68+
{
69+
const html = `
70+
<blockquote>
71+
First level quote
72+
<blockquote>Second level quote</blockquote>
73+
</blockquote>
74+
`;
75+
const plainText = 'First level quote\nSecond level quote';
76+
77+
testLog.info('=== Pasting HTML Nested Blockquotes ===');
78+
pasteContent(html, plainText);
79+
80+
cy.wait(1000);
81+
82+
cy.get('[contenteditable="true"]').find('[data-block-type="quote"]').should('contain', 'First level quote');
83+
cy.get('[contenteditable="true"]').find('[data-block-type="quote"]').should('contain', 'Second level quote');
84+
testLog.info('✓ HTML nested blockquotes pasted successfully');
85+
}
86+
87+
// Markdown Code Blocks
88+
{
89+
const markdown = `\`\`\`javascript
90+
const x = 10;
91+
console.log(x);
92+
\`\`\``;
93+
94+
testLog.info('=== Pasting Markdown Code Block with Language ===');
95+
pasteContent('', markdown);
96+
97+
cy.wait(1000);
98+
99+
cy.get('[contenteditable="true"]').find('pre').find('code').should('contain', 'const x = 10');
100+
testLog.info('✓ Markdown code block with language pasted successfully');
101+
}
102+
103+
{
104+
const markdown = `\`\`\`
105+
function hello() {
106+
console.log("Hello");
107+
}
108+
\`\`\``;
109+
110+
testLog.info('=== Pasting Markdown Code Block without Language ===');
111+
pasteContent('', markdown);
112+
113+
cy.wait(1000);
114+
115+
cy.get('[contenteditable="true"]').find('pre').find('code').should('contain', 'function hello');
116+
testLog.info('✓ Markdown code block without language pasted successfully');
117+
}
118+
119+
{
120+
const markdown = 'Use the `console.log()` function to print output.';
121+
122+
testLog.info('=== Pasting Markdown Inline Code ===');
123+
pasteContent('', markdown);
124+
125+
cy.wait(1000);
126+
127+
// Inline code is usually a span with specific style
128+
cy.get('[contenteditable="true"]').find('span.bg-border-primary').should('contain', 'console.log');
129+
testLog.info('✓ Markdown inline code pasted successfully');
130+
}
131+
132+
{
133+
const markdown = `\`\`\`python
134+
def greet():
135+
print("Hello")
136+
\`\`\`
137+
138+
\`\`\`typescript
139+
const greeting: string = "Hello";
140+
\`\`\`
141+
142+
\`\`\`bash
143+
echo "Hello World"
144+
\`\`\``;
145+
146+
testLog.info('=== Pasting Markdown Multiple Language Code Blocks ===');
147+
pasteContent('', markdown);
148+
149+
cy.wait(1000);
150+
151+
cy.get('[contenteditable="true"]').find('pre').find('code').should('contain', 'def greet');
152+
cy.get('[contenteditable="true"]').find('pre').find('code').should('contain', 'const greeting');
153+
cy.get('[contenteditable="true"]').find('pre').find('code').should('contain', 'echo');
154+
testLog.info('✓ Markdown multiple language code blocks pasted successfully');
155+
}
156+
157+
{
158+
const markdown = '> This is a quoted text';
159+
160+
testLog.info('=== Pasting Markdown Blockquote ===');
161+
pasteContent('', markdown);
162+
163+
cy.wait(1000);
164+
165+
cy.get('[contenteditable="true"]').find('[data-block-type="quote"]').should('contain', 'This is a quoted text');
166+
testLog.info('✓ Markdown blockquote pasted successfully');
167+
}
168+
169+
{
170+
const markdown = `> First level quote
171+
>> Second level quote
172+
>>> Third level quote`;
173+
174+
testLog.info('=== Pasting Markdown Nested Blockquotes ===');
175+
pasteContent('', markdown);
176+
177+
cy.wait(1000);
178+
179+
cy.get('[contenteditable="true"]').find('[data-block-type="quote"]').should('contain', 'First level quote');
180+
cy.get('[contenteditable="true"]').find('[data-block-type="quote"]').should('contain', 'Second level quote');
181+
cy.get('[contenteditable="true"]').find('[data-block-type="quote"]').should('contain', 'Third level quote');
182+
testLog.info('✓ Markdown nested blockquotes pasted successfully');
183+
}
184+
185+
{
186+
const markdown = '> **Important:** This is a *quoted* text with `code`';
187+
188+
testLog.info('=== Pasting Markdown Blockquote with Formatting ===');
189+
pasteContent('', markdown);
190+
191+
cy.wait(1000);
192+
193+
cy.get('[contenteditable="true"]').find('[data-block-type="quote"]').find('strong').should('contain', 'Important');
194+
cy.get('[contenteditable="true"]').find('[data-block-type="quote"]').find('em').should('contain', 'quoted');
195+
cy.get('[contenteditable="true"]').find('[data-block-type="quote"]').find('span.bg-border-primary').should('contain', 'code');
196+
testLog.info('✓ Markdown blockquote with formatting pasted successfully');
197+
}
198+
});
199+
});
200+

0 commit comments

Comments
 (0)