Files
AppFlowy-Web/cypress/e2e/embeded/database/database-bottom-scroll.cy.ts

232 lines
9.8 KiB
TypeScript

import { v4 as uuidv4 } from 'uuid';
import { AuthTestUtils } from '../../../support/auth-utils';
import { getSlashMenuItemName } from '../../../support/i18n-constants';
import {
AddPageSelectors,
DatabaseGridSelectors,
EditorSelectors,
ModalSelectors,
SlashCommandSelectors,
waitForReactUpdate
} from '../../../support/selectors';
describe('Embedded Database - Bottom Scroll Preservation', () => {
const generateRandomEmail = () => `${uuidv4()}@appflowy.io`;
beforeEach(() => {
cy.on('uncaught:exception', (err) => {
if (err.message.includes('Minified React error') ||
err.message.includes('View not found') ||
err.message.includes('No workspace or service found') ||
err.message.includes('Cannot resolve a DOM point from Slate point') ||
err.message.includes('No range and node found')) {
return false;
}
return true;
});
cy.viewport(1280, 720);
});
it('should preserve scroll position when creating grid database at bottom of long document', () => {
const testEmail = generateRandomEmail();
cy.task('log', `[TEST START] Testing scroll preservation when creating database at bottom - Test email: ${testEmail}`);
// Step 1: Login
cy.task('log', '[STEP 1] Visiting login page');
cy.visit('/login', { failOnStatusCode: false });
cy.wait(2000);
const authUtils = new AuthTestUtils();
cy.task('log', '[STEP 2] Starting authentication');
authUtils.signInWithTestUrl(testEmail).then(() => {
cy.task('log', '[STEP 3] Authentication successful');
cy.url({ timeout: 30000 }).should('include', '/app');
cy.wait(3000);
// Step 2: Create a new document
cy.task('log', '[STEP 4] Creating new document');
AddPageSelectors.inlineAddButton().first().as('addBtn');
cy.get('@addBtn').should('be.visible').click();
waitForReactUpdate(1000);
cy.get('[role="menuitem"]').first().as('menuItem');
cy.get('@menuItem').click();
waitForReactUpdate(1000);
// Handle the new page modal if it appears
cy.get('body').then(($body) => {
if ($body.find('[data-testid="new-page-modal"]').length > 0) {
cy.task('log', '[STEP 4.1] Handling new page modal');
ModalSelectors.newPageModal().should('be.visible').within(() => {
ModalSelectors.spaceItemInModal().first().as('spaceItem');
cy.get('@spaceItem').click();
waitForReactUpdate(500);
cy.contains('button', 'Add').click();
});
cy.wait(3000);
} else {
cy.wait(3000);
}
});
// Step 3: Wait for editor to be available and stable
cy.task('log', '[STEP 5] Waiting for editor to be available');
EditorSelectors.firstEditor().should('exist', { timeout: 15000 });
waitForReactUpdate(2000); // Give extra time for editor to stabilize
// Step 4: Add many lines to exceed screen height
cy.task('log', '[STEP 6] Adding multiple lines to exceed screen height');
// Click editor to focus it
EditorSelectors.firstEditor().click({ force: true });
waitForReactUpdate(500);
// Build text content with 30 lines (enough to exceed viewport)
let textContent = '';
for (let i = 1; i <= 30; i++) {
textContent += `Line ${i} - This is a longer line of text to ensure we have enough content to scroll{enter}`;
}
cy.task('log', '[STEP 6.1] Typing 30 lines of content');
// Use cy.focused() to type - more stable than re-querying editor element
cy.focused().type(textContent, { delay: 1 });
cy.task('log', '[STEP 6.2] Content added successfully');
waitForReactUpdate(2000);
// Step 5: Get the scroll container and record initial state
cy.task('log', '[STEP 7] Finding scroll container');
cy.get('.appflowy-scroll-container').first().as('scrollContainer');
// Step 6: Scroll to the bottom
cy.task('log', '[STEP 8] Scrolling to bottom of document');
cy.get('@scrollContainer').then(($container) => {
const scrollHeight = $container[0].scrollHeight;
const clientHeight = $container[0].clientHeight;
const scrollToPosition = scrollHeight - clientHeight;
cy.task('log', `[STEP 8.1] Scroll metrics: scrollHeight=${scrollHeight}, clientHeight=${clientHeight}, scrollToPosition=${scrollToPosition}`);
// Scroll to bottom
cy.get('@scrollContainer').scrollTo(0, scrollToPosition);
waitForReactUpdate(500);
// Verify we're at the bottom
cy.get('@scrollContainer').then(($cont) => {
const currentScrollTop = $cont[0].scrollTop;
cy.task('log', `[STEP 8.2] Current scroll position after scrolling: ${currentScrollTop}`);
// Allow some tolerance (within 50px of bottom)
expect(currentScrollTop).to.be.greaterThan(scrollToPosition - 50);
});
});
// Step 7: Store the scroll position before opening slash menu
let scrollPositionBeforeSlashMenu = 0;
cy.get('@scrollContainer').then(($container) => {
scrollPositionBeforeSlashMenu = $container[0].scrollTop;
cy.task('log', `[STEP 9] Scroll position before opening slash menu: ${scrollPositionBeforeSlashMenu}`);
});
// Step 8: Open slash menu at the bottom
cy.task('log', '[STEP 10] Opening slash menu at bottom');
// Click editor at the end and type slash
EditorSelectors.firstEditor().click().type('{enter}/');
waitForReactUpdate(500);
// Step 9: Verify slash menu is visible
cy.task('log', '[STEP 11] Verifying slash menu is visible');
SlashCommandSelectors.slashPanel().should('be.visible');
// Step 10: Check that scroll position is preserved after opening slash menu
cy.get('@scrollContainer').then(($container) => {
const scrollAfterSlashMenu = $container[0].scrollTop;
cy.task('log', `[STEP 11.1] Scroll position after opening slash menu: ${scrollAfterSlashMenu}`);
// Allow some tolerance (within 100px) since the menu might cause minor layout shifts
const scrollDifference = Math.abs(scrollAfterSlashMenu - scrollPositionBeforeSlashMenu);
cy.task('log', `[STEP 11.2] Scroll difference: ${scrollDifference}px`);
// The scroll should not jump to the top (which would be < 1000)
// It should stay near the bottom
expect(scrollAfterSlashMenu).to.be.greaterThan(scrollPositionBeforeSlashMenu - 200);
if (scrollDifference > 100) {
cy.task('log', `[WARNING] Scroll position changed by ${scrollDifference}px when opening slash menu`);
}
});
// Step 11: Select Grid option from slash menu
cy.task('log', '[STEP 12] Selecting Grid option from slash menu');
let scrollBeforeGridCreation = 0;
cy.get('@scrollContainer').then(($container) => {
scrollBeforeGridCreation = $container[0].scrollTop;
cy.task('log', `[STEP 12.1] Scroll position before creating grid: ${scrollBeforeGridCreation}`);
});
SlashCommandSelectors.slashPanel().within(() => {
SlashCommandSelectors.slashMenuItem(getSlashMenuItemName('grid')).first().as('gridMenuItem');
cy.get('@gridMenuItem').should('be.visible').click();
});
waitForReactUpdate(2000);
// Step 12: Verify the modal opened (database opens in a modal)
cy.task('log', '[STEP 13] Verifying database modal opened');
cy.get('[role="dialog"]', { timeout: 10000 }).should('be.visible');
// Step 13: CRITICAL CHECK - Verify scroll position is preserved after creating database
cy.task('log', '[STEP 14] CRITICAL: Verifying scroll position after creating database');
cy.get('@scrollContainer').then(($container) => {
const scrollAfterGridCreation = $container[0].scrollTop;
const scrollHeight = $container[0].scrollHeight;
const clientHeight = $container[0].clientHeight;
cy.task('log', `[STEP 14.1] Scroll position after creating grid: ${scrollAfterGridCreation}`);
cy.task('log', `[STEP 14.2] scrollHeight: ${scrollHeight}, clientHeight: ${clientHeight}`);
const scrollDifference = Math.abs(scrollAfterGridCreation - scrollBeforeGridCreation);
cy.task('log', `[STEP 14.3] Scroll difference after grid creation: ${scrollDifference}px`);
// CRITICAL ASSERTION: The document should NOT scroll to the top
// If it scrolled to top, scrollAfterGridCreation would be close to 0
// We expect it to stay near the bottom
expect(scrollAfterGridCreation).to.be.greaterThan(scrollBeforeGridCreation - 300);
// Also verify it's not at the very top
expect(scrollAfterGridCreation).to.be.greaterThan(500);
if (scrollAfterGridCreation < 500) {
cy.task('log', `[CRITICAL FAILURE] Document scrolled to top! Position: ${scrollAfterGridCreation}`);
throw new Error(`Document scrolled to top (position: ${scrollAfterGridCreation}) when creating grid at bottom`);
}
if (scrollDifference > 300) {
cy.task('log', `[WARNING] Large scroll change detected: ${scrollDifference}px`);
} else {
cy.task('log', `[SUCCESS] Scroll position preserved! Difference: ${scrollDifference}px`);
}
});
// Step 14: Close the modal and verify final state
cy.task('log', '[STEP 15] Closing database modal');
cy.get('[role="dialog"]').within(() => {
cy.get('button').first().click(); // Click close button
});
waitForReactUpdate(1000);
// Step 15: Verify the grid database was actually created in the document
cy.task('log', '[STEP 16] Verifying grid database exists in document');
cy.get('[class*="appflowy-database"]').should('exist');
DatabaseGridSelectors.grid().should('exist');
cy.task('log', '[TEST COMPLETE] Scroll preservation test passed successfully');
});
});
});