Files
AppFlowy-Web/cypress/e2e/embeded/database/linked-database-plus-button.cy.ts
2025-11-21 19:54:04 +08:00

441 lines
17 KiB
TypeScript

import { v4 as uuidv4 } from 'uuid';
import { AuthTestUtils } from '../../../support/auth-utils';
import { getSlashMenuItemName } from '../../../support/i18n-constants';
import {
AddPageSelectors,
EditorSelectors,
SlashCommandSelectors,
waitForReactUpdate
} from '../../../support/selectors';
describe('Embedded Database - Plus Button View Creation', () => {
const generateRandomEmail = () => `${uuidv4()}@appflowy.io`;
const clickAddViewButtonOnConditions = () => {
// Wait for database to fully load
cy.wait(1000);
// Use .then() to force fresh query and avoid DOM detachment
cy.get('@embeddedDB').then($db => {
// Re-query the button to get a fresh reference
cy.wrap($db).find('[data-testid="add-view-button"]')
.scrollIntoView()
.click({ force: true }); // force click to avoid detachment issues
});
};
const waitForDialogsToClose = () => {
// Wait for any modal/dialog to close
// Simple fixed wait to account for MUI dialog animation (225ms) + removal + buffer
cy.wait(1500); // Sufficient time for dialog to close and be removed from DOM
};
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('ResizeObserver loop')) {
return false;
}
return true;
});
cy.viewport(1280, 720);
});
it('should create new view using + button, auto-select it, and scroll into view', () => {
const testEmail = generateRandomEmail();
cy.task('log', `[TEST START] Testing plus button view creation - 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 1: Create source database
cy.task('log', '[STEP 4] Creating source database');
AddPageSelectors.inlineAddButton().first().as('addBtnPlus');
cy.get('@addBtnPlus').should('be.visible').click();
waitForReactUpdate(1000);
AddPageSelectors.addGridButton().should('be.visible').as('gridBtnPlus');
cy.get('@gridBtnPlus').click();
cy.wait(5000);
const dbName = 'New Grid';
// Step 2: Create document at same level as database
cy.task('log', '[STEP 5] Creating document at same level as database');
AddPageSelectors.inlineAddButton().first().as('addDocBtnPlus1');
cy.get('@addDocBtnPlus1').should('be.visible').click();
waitForReactUpdate(1000);
cy.get('[role="menuitem"]').first().as('menuItemPlus1');
cy.get('@menuItemPlus1').click();
waitForReactUpdate(2000);
EditorSelectors.firstEditor().should('exist', { timeout: 10000 });
// Step 3: Insert linked database
cy.task('log', '[STEP 6] Inserting linked database');
EditorSelectors.firstEditor().click().type('/');
waitForReactUpdate(500);
SlashCommandSelectors.slashPanel()
.should('be.visible')
.within(() => {
SlashCommandSelectors.slashMenuItem(getSlashMenuItemName('linkedGrid')).first().as('linkedGridPlus');
cy.get('@linkedGridPlus').click();
});
waitForReactUpdate(1000);
SlashCommandSelectors.selectDatabase(dbName);
waitForReactUpdate(2000);
// Get the embedded database (should be the LAST one, not the first)
// The first is the standalone "New Grid" page, the last is the embedded database in the document
cy.get('[class*="appflowy-database"]', { timeout: 10000 })
.should('exist')
.last()
.as('embeddedDB');
// Step 4: Verify embedded database shows 1 tab (the reference view itself)
cy.task('log', '[STEP 7] Verifying embedded database shows reference view tab');
cy.get('@embeddedDB').within(() => {
// Embedded database should show 1 tab: the reference view itself (like "View of New Grid")
cy.get('[data-testid^="view-tab-"]', { timeout: 10000 })
.should('have.length', 1)
.and('be.visible');
cy.task('log', '[STEP 7.1] Confirmed: 1 tab in embedded database (reference view)');
});
// Step 5: Find and click the + button to add a new view
cy.task('log', '[STEP 8] Looking for + button to add new view');
clickAddViewButtonOnConditions();
waitForReactUpdate(500);
// Step 6: Select Board view type from dropdown
cy.task('log', '[STEP 9] Looking for view type options in dropdown');
// Wait for menu to appear and click Board option directly
cy.task('log', '[STEP 9.1] Selecting Board view type');
cy.contains('Board', { timeout: 5000 }).should('be.visible').click();
// Wait for dialog/menu to close
waitForDialogsToClose();
// Step 7: Wait for new view to be created and synced
cy.task('log', '[STEP 10] Waiting for new Board view to appear (should be within 200-500ms)');
// Wait longer for view to sync
cy.wait(3000);
const viewCreationStart = Date.now();
// Re-query the embedded DB fresh to avoid stale alias issues
cy.task('log', '[DEBUG] Re-querying embedded DB fresh to avoid stale alias');
cy.get('[class*="appflowy-database"]').last().as('embeddedDBFresh');
// Debug: Log all existing tabs with fresh query
cy.get('@embeddedDBFresh').within(() => {
cy.get('[data-testid^="view-tab-"]').then($tabs => {
const tabNames = Array.from($tabs).map((t: any) => t.textContent).join(', ');
cy.task('log', `[DEBUG FRESH] All tabs after Board creation: ${tabNames} (count: ${$tabs.length})`);
});
});
// Step 8: Verify new Board tab exists and measure timing using fresh query
cy.get('@embeddedDBFresh').within(() => {
cy.contains('[data-testid^="view-tab-"]', 'Board', { timeout: 10000 })
.should('exist')
.and('be.visible')
.then(() => {
const viewCreationTime = Date.now() - viewCreationStart;
cy.task('log', `[STEP 10.1] Board view created in ${viewCreationTime}ms`);
// Assert it was created (allow up to 30s for CI/sync)
expect(viewCreationTime).to.be.lessThan(30000);
});
// Step 9: Verify the new Board tab is selected (active) and scrolled into view
cy.task('log', '[STEP 11] Verifying Board tab is automatically selected and visible');
cy.contains('[data-testid^="view-tab-"]', 'Board')
.should('have.attr', 'data-state', 'active')
.should('be.visible')
.then(() => {
cy.task('log', '[STEP 11.1] Confirmed: Board tab is active/selected and visible');
});
// Step 10: Verify Board view content is displayed
cy.task('log', '[STEP 12] Verifying Board view content is displayed');
// Board views typically have a kanban-style layout
cy.get('[class*="board"], [class*="kanban"], [data-testid*="board"]', { timeout: 5000 })
.should('exist')
.then(() => {
cy.task('log', '[STEP 12.1] Board view content confirmed');
});
});
// Step 11: Verify we have at least 2 tabs (reference view + Board)
cy.task('log', '[STEP 13] Verifying total tab count');
cy.get('@embeddedDBFresh').within(() => {
cy.get('[data-testid^="view-tab-"]')
.should('have.length.at.least', 2)
.then(($tabs) => {
cy.task('log', `[STEP 13.1] Total tabs: ${$tabs.length}`);
});
});
cy.task('log', '[TEST COMPLETE] Plus button view creation test passed successfully');
});
});
it('should measure view creation performance', () => {
const testEmail = generateRandomEmail();
cy.task('log', `[TEST START] Testing view creation performance - Test email: ${testEmail}`);
cy.visit('/login', { failOnStatusCode: false });
cy.wait(2000);
const authUtils = new AuthTestUtils();
authUtils.signInWithTestUrl(testEmail).then(() => {
cy.url({ timeout: 30000 }).should('include', '/app');
cy.wait(3000);
// Step 1: Create source database
cy.task('log', '[STEP 1] Creating source database');
AddPageSelectors.inlineAddButton().first().as('addBtnPlus');
cy.get('@addBtnPlus').should('be.visible').click();
waitForReactUpdate(1000);
AddPageSelectors.addGridButton().should('be.visible').as('gridBtnPlus');
cy.get('@gridBtnPlus').click();
cy.wait(5000);
const dbName = 'New Grid';
// Step 2: Create document at same level as database
cy.task('log', '[STEP 2] Creating document at same level as database');
AddPageSelectors.inlineAddButton().first().as('addDocBtnPerf');
cy.get('@addDocBtnPerf').should('be.visible').click();
waitForReactUpdate(1000);
cy.get('[role="menuitem"]').first().as('menuItemPerf');
cy.get('@menuItemPerf').click();
waitForReactUpdate(2000);
EditorSelectors.firstEditor().should('exist', { timeout: 10000 });
// Step 3: Insert linked database
cy.task('log', '[STEP 3] Inserting linked database');
EditorSelectors.firstEditor().click().type('/');
waitForReactUpdate(500);
SlashCommandSelectors.slashPanel()
.should('be.visible')
.within(() => {
SlashCommandSelectors.slashMenuItem(getSlashMenuItemName('linkedGrid')).first().as('linkedGridPlus');
cy.get('@linkedGridPlus').click();
});
waitForReactUpdate(1000);
SlashCommandSelectors.selectDatabase(dbName);
waitForReactUpdate(2000);
cy.get('[class*="appflowy-database"]', { timeout: 10000 })
.should('exist')
.last()
.as('embeddedDB');
// Wait for initial view to load
cy.get('@embeddedDB').within(() => {
cy.get('[data-testid^="view-tab-"]', { timeout: 10000 })
.should('exist')
.and('be.visible');
});
// Record start time for performance measurement
const startTime = Date.now();
cy.task('log', '[STEP 4] Starting performance measurement for view creation');
// Click + button to create new view
clickAddViewButtonOnConditions();
waitForReactUpdate(500);
cy.contains('Board', { timeout: 5000 }).should('be.visible').click();
// Wait for dialog to close
waitForDialogsToClose();
waitForReactUpdate(3000);
// Re-query the embedded DB fresh to avoid stale alias issues
cy.get('[class*="appflowy-database"]').last().as('embeddedDBPerf');
// Verify new view appears within performance target
cy.get('@embeddedDBPerf').within(() => {
cy.contains('[data-testid^="view-tab-"]', 'Board', { timeout: 30000 })
.should('exist')
.then(() => {
const elapsed = Date.now() - startTime;
cy.task('log', `[PERFORMANCE] View created in ${elapsed}ms`);
// Assert performance target met (200-500ms typical, max 30s for CI)
expect(elapsed).to.be.lessThan(30000);
// Warn if slower than expected
if (elapsed > 500) {
cy.task('log', `[PERFORMANCE WARNING] View creation took ${elapsed}ms (expected 200-500ms)`);
} else {
cy.task('log', '[PERFORMANCE SUCCESS] View created within expected 200-500ms window');
}
});
});
cy.task('log', '[TEST COMPLETE] Performance test passed');
});
});
// Test removed - duplicates Test 1's scroll behavior verification
// Test 1 already comprehensively tests view creation, auto-selection, and scroll-into-view
it.skip('should scroll new view into viewport when tabs overflow', () => {
const testEmail = generateRandomEmail();
cy.task('log', `[TEST START] Testing scroll behavior with multiple views - Test email: ${testEmail}`);
cy.visit('/login', { failOnStatusCode: false });
cy.wait(2000);
const authUtils = new AuthTestUtils();
authUtils.signInWithTestUrl(testEmail).then(() => {
cy.url({ timeout: 30000 }).should('include', '/app');
cy.wait(3000);
// Step 1: Create source database
cy.task('log', '[STEP 1] Creating source database');
AddPageSelectors.inlineAddButton().first().as('addBtnPlus');
cy.get('@addBtnPlus').should('be.visible').click();
waitForReactUpdate(1000);
AddPageSelectors.addGridButton().should('be.visible').as('gridBtnPlus');
cy.get('@gridBtnPlus').click();
cy.wait(5000);
const dbName = 'New Grid';
// Step 2: Create document at same level as database
cy.task('log', '[STEP 2] Creating document at same level as database');
AddPageSelectors.inlineAddButton().first().as('addDocBtnPerf');
cy.get('@addDocBtnPerf').should('be.visible').click();
waitForReactUpdate(1000);
cy.get('[role="menuitem"]').first().as('menuItemPerf');
cy.get('@menuItemPerf').click();
waitForReactUpdate(2000);
EditorSelectors.firstEditor().should('exist', { timeout: 10000 });
// Step 3: Insert linked database
cy.task('log', '[STEP 3] Inserting linked database');
EditorSelectors.firstEditor().click().type('/');
waitForReactUpdate(500);
SlashCommandSelectors.slashPanel()
.should('be.visible')
.within(() => {
SlashCommandSelectors.slashMenuItem(getSlashMenuItemName('linkedGrid')).first().as('linkedGridPlus');
cy.get('@linkedGridPlus').click();
});
waitForReactUpdate(1000);
SlashCommandSelectors.selectDatabase(dbName);
waitForReactUpdate(2000);
cy.get('[class*="appflowy-database"]', { timeout: 10000 })
.should('exist')
.last()
.as('embeddedDB');
// Create multiple views to trigger horizontal scrolling
// Using 2 views for reliability (Board, Calendar)
const viewTypes = ['Board', 'Calendar'];
cy.task('log', `[STEP 4] Creating ${viewTypes.length} additional views to test scrolling`);
viewTypes.forEach((viewType, index) => {
cy.task('log', `[STEP 4.${index + 1}] Creating view ${index + 1}: ${viewType}`);
clickAddViewButtonOnConditions();
waitForReactUpdate(1500);
cy.contains(viewType, { timeout: 5000 }).should('be.visible').click({ force: true });
// Wait for dialog/menu to close after selecting view type
waitForDialogsToClose();
// Longer wait to ensure view is fully created and synced
waitForReactUpdate(8000);
// Re-query the embedded DB fresh to avoid stale alias issues
cy.get('[class*="appflowy-database"]').last().as(`embeddedDBScroll${index}`);
// Log current tab count for debugging
cy.get(`@embeddedDBScroll${index}`).within(() => {
cy.get('[data-testid^="view-tab-"]').then($tabs => {
cy.task('log', `[DEBUG] Current tab count after creating view ${index + 1}: ${$tabs.length}`);
});
});
// Verify the newly created tab is visible in viewport (scrolled into view)
cy.task('log', `[STEP 4.${index + 1}.1] Verifying new tab is visible`);
cy.get(`@embeddedDBScroll${index}`).within(() => {
// Get all tabs with the current view type
cy.get('[data-testid^="view-tab-"]')
.filter(`:contains("${viewType}")`)
.last()
.then(($tab) => {
// Check if element is in viewport
const rect = $tab[0].getBoundingClientRect();
const isVisible = rect.left >= 0 && rect.right <= window.innerWidth;
if (isVisible) {
cy.task('log', `[STEP 4.${index + 1}.2] Tab is visible in viewport (scrolled into view)`);
} else {
cy.task('log', `[STEP 4.${index + 1}.2] WARNING: Tab may not be fully visible`);
}
// Verify tab is selected
cy.wrap($tab).should('have.attr', 'data-state', 'active');
});
});
});
// Final verification - count total tabs
cy.task('log', '[STEP 5] Final verification of all created views');
// Re-query fresh for final verification
cy.get('[class*="appflowy-database"]').last().as('embeddedDBFinal');
cy.get('@embeddedDBFinal').within(() => {
// Expect: 2 initial tabs (Grid + "View of Grid") + 3 created views = 5 total
cy.get('[data-testid^="view-tab-"]')
.should('have.length.at.least', viewTypes.length + 2) // +2 for initial Grid and linked view
.then(($tabs) => {
cy.task('log', `[STEP 5.1] Total tabs created: ${$tabs.length} (expected at least ${viewTypes.length + 2})`);
});
});
cy.task('log', '[TEST COMPLETE] Scroll behavior test passed');
});
});
});