Skip to content
Closed
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
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