mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2025-11-29 10:47:56 +08:00
441 lines
17 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|