Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ coverage

cypress/snapshots/**/__diff_output__/
.claude
.gemini*
cypress/screenshots
cypress/videos
cypress/downloads
Expand Down
107 changes: 54 additions & 53 deletions cypress/e2e/page/edit-page.cy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AuthTestUtils } from '../../support/auth-utils';
import { TestTool } from '../../support/page-utils';
import { PageSelectors, ModalSelectors, waitForReactUpdate } from '../../support/selectors';
import { AddPageSelectors, EditorSelectors, ModalSelectors, PageSelectors, SpaceSelectors, waitForReactUpdate } from '../../support/selectors';
import { generateRandomEmail } from '../../support/test-config';
import { testLog } from '../../support/test-helpers';

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

// Step 2: Create a new page using the simpler approach
testLog.info( '=== Starting Page Creation for Edit Test ===');
testLog.info( `Target page name: ${testPageName}`);

// Click new page button
PageSelectors.newPageButton().should('be.visible').click();
testLog.info('=== Starting Page Creation for Edit Test ===');
testLog.info(`Target page name: ${testPageName}`);

// Expand General space to ensure we can see the content
testLog.info('Expanding General space');
SpaceSelectors.itemByName('General').first().click();
waitForReactUpdate(500);

// Use inline add button on General space
testLog.info('Creating new page in General space');
SpaceSelectors.itemByName('General').first().within(() => {
AddPageSelectors.inlineAddButton().first().should('be.visible').click();
});
waitForReactUpdate(1000);

// Select first item (Page) from the menu
cy.get('[role="menuitem"]').first().click();
waitForReactUpdate(1000);

// Handle the new page modal
ModalSelectors.newPageModal().should('be.visible').within(() => {
// Select the first available space
ModalSelectors.spaceItemInModal().first().click();
waitForReactUpdate(500);
// Click Add button
cy.contains('button', 'Add').click();

// Handle the new page modal if it appears (defensive)
cy.get('body').then(($body) => {
if ($body.find('[data-testid="new-page-modal"]').length > 0) {
testLog.info('Handling new page modal');
ModalSelectors.newPageModal().should('be.visible').within(() => {
ModalSelectors.spaceItemInModal().first().click();
waitForReactUpdate(500);
cy.contains('button', 'Add').click();
});
cy.wait(3000);
}
});

// Wait for navigation to the new page
cy.wait(3000);

// Close any modal dialogs

// Close any remaining modal dialogs
cy.get('body').then(($body: JQuery<HTMLBodyElement>) => {
if ($body.find('[role="dialog"]').length > 0 || $body.find('.MuiDialog-container').length > 0) {
testLog.info( 'Closing modal dialog');
testLog.info('Closing modal dialog');
cy.get('body').type('{esc}');
cy.wait(1000);
}
});


// Click the newly created "Untitled" page
testLog.info('Selecting the new Untitled page');
PageSelectors.itemByName('Untitled').should('be.visible').click();
waitForReactUpdate(1000);

// Step 3: Add content to the page editor
testLog.info( '=== Adding Content to Page ===');

// Find the editor and add content
cy.get('[contenteditable="true"]').then($editors => {
testLog.info( `Found ${$editors.length} editable elements`);

// Look for the main editor (not the title)
let editorFound = false;
$editors.each((index: number, el: HTMLElement) => {
const $el = Cypress.$(el);
// Skip title inputs
if (!$el.attr('data-testid')?.includes('title') && !$el.hasClass('editor-title')) {
testLog.info( `Using editor at index ${index}`);
cy.wrap(el).click().type(testContent.join('{enter}'));
editorFound = true;
return false; // break the loop
}
});

if (!editorFound) {
// Fallback: use the last contenteditable element
testLog.info( 'Using fallback: last contenteditable element');
cy.wrap($editors.last()).click().type(testContent.join('{enter}'));
}
});

testLog.info('=== Adding Content to Page ===');

// Wait for editor to be available and add content
testLog.info('Waiting for editor to be available');
EditorSelectors.firstEditor().should('exist', { timeout: 15000 });

testLog.info('Writing content to editor');
EditorSelectors.firstEditor().click().type(testContent.join('{enter}'));

// Wait for content to be saved
cy.wait(2000);

// Step 4: Verify the content was added
testLog.info( '=== Verifying Content ===');
testLog.info('=== Verifying Content ===');

// Verify each line of content exists in the page
testContent.forEach(line => {
cy.contains(line).should('exist');
testLog.info( `✓ Found content: "${line}"`);
testLog.info(`✓ Found content: "${line}"`);
});
testLog.info( '=== Test completed successfully ===');

testLog.info('=== Test completed successfully ===');
});
});
});
200 changes: 200 additions & 0 deletions cypress/e2e/page/paste-code.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { createTestPage, pasteContent } from '../../support/paste-utils';
import { testLog } from '../../support/test-helpers';

describe('Paste Code Block Tests', () => {
it('should paste all code block formats correctly', () => {
createTestPage();

// HTML Code Blocks
{
const html = '<pre><code>const x = 10;\nconsole.log(x);</code></pre>';
const plainText = 'const x = 10;\nconsole.log(x);';

testLog.info('=== Pasting HTML Code Block ===');
pasteContent(html, plainText);

cy.wait(1000);

// CodeBlock component structure: .relative.w-full > pre > code
cy.get('[contenteditable="true"]').find('pre').find('code').should('contain', 'const x = 10');
testLog.info('✓ HTML code block pasted successfully');
}

{
const html = '<pre><code class="language-javascript">function hello() {\n console.log("Hello");\n}</code></pre>';
const plainText = 'function hello() {\n console.log("Hello");\n}';

testLog.info('=== Pasting HTML Code Block with Language ===');
pasteContent(html, plainText);

cy.wait(1000);

cy.get('[contenteditable="true"]').find('pre').find('code').should('contain', 'function hello');
testLog.info('✓ HTML code block with language pasted successfully');
}

{
const html = `
<pre><code class="language-python">def greet():
print("Hello")</code></pre>
<pre><code class="language-typescript">const greeting: string = "Hello";</code></pre>
`;
const plainText = 'def greet():\n print("Hello")\nconst greeting: string = "Hello";';

testLog.info('=== Pasting HTML Multiple Language Code Blocks ===');
pasteContent(html, plainText);

cy.wait(1000);

cy.get('[contenteditable="true"]').find('pre').find('code').should('contain', 'def greet');
cy.get('[contenteditable="true"]').find('pre').find('code').should('contain', 'const greeting');
testLog.info('✓ HTML multiple language code blocks pasted successfully');
}

{
const html = '<blockquote>This is a quoted text</blockquote>';
const plainText = 'This is a quoted text';

testLog.info('=== Pasting HTML Blockquote ===');
pasteContent(html, plainText);

cy.wait(1000);

// AppFlowy renders blockquote as div with data-block-type="quote"
cy.get('[contenteditable="true"]').find('[data-block-type="quote"]').should('contain', 'This is a quoted text');
testLog.info('✓ HTML blockquote pasted successfully');
}

{
const html = `
<blockquote>
First level quote
<blockquote>Second level quote</blockquote>
</blockquote>
`;
const plainText = 'First level quote\nSecond level quote';

testLog.info('=== Pasting HTML Nested Blockquotes ===');
pasteContent(html, plainText);

cy.wait(1000);

cy.get('[contenteditable="true"]').find('[data-block-type="quote"]').should('contain', 'First level quote');
cy.get('[contenteditable="true"]').find('[data-block-type="quote"]').should('contain', 'Second level quote');
testLog.info('✓ HTML nested blockquotes pasted successfully');
}

// Markdown Code Blocks
{
const markdown = `\`\`\`javascript
const x = 10;
console.log(x);
\`\`\``;

testLog.info('=== Pasting Markdown Code Block with Language ===');
pasteContent('', markdown);

cy.wait(1000);

cy.get('[contenteditable="true"]').find('pre').find('code').should('contain', 'const x = 10');
testLog.info('✓ Markdown code block with language pasted successfully');
}

{
const markdown = `\`\`\`
function hello() {
console.log("Hello");
}
\`\`\``;

testLog.info('=== Pasting Markdown Code Block without Language ===');
pasteContent('', markdown);

cy.wait(1000);

cy.get('[contenteditable="true"]').find('pre').find('code').should('contain', 'function hello');
testLog.info('✓ Markdown code block without language pasted successfully');
}

{
const markdown = 'Use the `console.log()` function to print output.';

testLog.info('=== Pasting Markdown Inline Code ===');
pasteContent('', markdown);

cy.wait(1000);

// Inline code is usually a span with specific style
cy.get('[contenteditable="true"]').find('span.bg-border-primary').should('contain', 'console.log');
testLog.info('✓ Markdown inline code pasted successfully');
}

{
const markdown = `\`\`\`python
def greet():
print("Hello")
\`\`\`

\`\`\`typescript
const greeting: string = "Hello";
\`\`\`

\`\`\`bash
echo "Hello World"
\`\`\``;

testLog.info('=== Pasting Markdown Multiple Language Code Blocks ===');
pasteContent('', markdown);

cy.wait(1000);

cy.get('[contenteditable="true"]').find('pre').find('code').should('contain', 'def greet');
cy.get('[contenteditable="true"]').find('pre').find('code').should('contain', 'const greeting');
cy.get('[contenteditable="true"]').find('pre').find('code').should('contain', 'echo');
testLog.info('✓ Markdown multiple language code blocks pasted successfully');
}

{
const markdown = '> This is a quoted text';

testLog.info('=== Pasting Markdown Blockquote ===');
pasteContent('', markdown);

cy.wait(1000);

cy.get('[contenteditable="true"]').find('[data-block-type="quote"]').should('contain', 'This is a quoted text');
testLog.info('✓ Markdown blockquote pasted successfully');
}

{
const markdown = `> First level quote
>> Second level quote
>>> Third level quote`;

testLog.info('=== Pasting Markdown Nested Blockquotes ===');
pasteContent('', markdown);

cy.wait(1000);

cy.get('[contenteditable="true"]').find('[data-block-type="quote"]').should('contain', 'First level quote');
cy.get('[contenteditable="true"]').find('[data-block-type="quote"]').should('contain', 'Second level quote');
cy.get('[contenteditable="true"]').find('[data-block-type="quote"]').should('contain', 'Third level quote');
testLog.info('✓ Markdown nested blockquotes pasted successfully');
}

{
const markdown = '> **Important:** This is a *quoted* text with `code`';

testLog.info('=== Pasting Markdown Blockquote with Formatting ===');
pasteContent('', markdown);

cy.wait(1000);

cy.get('[contenteditable="true"]').find('[data-block-type="quote"]').find('strong').should('contain', 'Important');
cy.get('[contenteditable="true"]').find('[data-block-type="quote"]').find('em').should('contain', 'quoted');
cy.get('[contenteditable="true"]').find('[data-block-type="quote"]').find('span.bg-border-primary').should('contain', 'code');
testLog.info('✓ Markdown blockquote with formatting pasted successfully');
}
});
});

Loading
Loading