diff --git a/cypress/e2e/database/row-deletion.cy.ts b/cypress/e2e/database/row-deletion.cy.ts new file mode 100644 index 00000000..0cc099d0 --- /dev/null +++ b/cypress/e2e/database/row-deletion.cy.ts @@ -0,0 +1,171 @@ +import { v4 as uuidv4 } from 'uuid'; +import { AuthTestUtils } from '../../support/auth-utils'; +import { + AddPageSelectors, + DatabaseGridSelectors, + RowControlsSelectors, + waitForReactUpdate +} from '../../support/selectors'; + +describe('Database Row Deletion', () => { + 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')) { + return false; + } + return true; + }); + + cy.viewport(1280, 720); + }); + + it('should delete a row from the grid', () => { + const testEmail = generateRandomEmail(); + const testContent = `Test Row ${Date.now()}`; + + cy.log(`[TEST START] Testing row deletion - Test email: ${testEmail}`); + + // Login + cy.log('[STEP 1] Visiting login page'); + cy.visit('/login', { failOnStatusCode: false }); + cy.wait(2000); + + const authUtils = new AuthTestUtils(); + cy.log('[STEP 2] Starting authentication'); + authUtils.signInWithTestUrl(testEmail).then(() => { + cy.log('[STEP 3] Authentication successful'); + cy.url({ timeout: 30000 }).should('include', '/app'); + cy.wait(3000); + + // Create a new grid + cy.log('[STEP 4] Creating new grid'); + AddPageSelectors.inlineAddButton().first().should('be.visible').click(); + waitForReactUpdate(1000); + AddPageSelectors.addGridButton().should('be.visible').click(); + cy.wait(8000); + + // Verify grid exists + cy.log('[STEP 5] Verifying grid exists'); + DatabaseGridSelectors.grid().should('exist'); + + // Get cells and edit the first one + cy.log('[STEP 6] Getting cells and editing first cell'); + DatabaseGridSelectors.cells().should('exist'); + + DatabaseGridSelectors.cells().then($cells => { + cy.log(`[STEP 7] Found ${$cells.length} cells`); + + // Click first cell and type content + cy.wrap($cells.first()).click(); + waitForReactUpdate(500); + cy.focused().type(testContent); + cy.focused().type('{enter}'); + waitForReactUpdate(1000); + + // Verify data was entered + cy.log('[STEP 8] Verifying content was added'); + cy.wrap($cells.first()).should('contain.text', testContent); + }); + + // Get initial row count + cy.log('[STEP 9] Getting initial row count'); + DatabaseGridSelectors.rows().then($rows => { + const initialRowCount = $rows.length; + cy.log(`[STEP 9.1] Initial row count: ${initialRowCount}`); + + // Now hover over the first row to show controls + cy.log('[STEP 10] Hovering over first row to show controls'); + + // Get the first row and hover over it + DatabaseGridSelectors.firstRow() + .parent() + .parent() + .trigger('mouseenter', { force: true }) + .trigger('mouseover', { force: true }); + + cy.wait(1000); + + // Click on the drag icon/row accessory button + cy.log('[STEP 11] Looking for and clicking row accessory button'); + + RowControlsSelectors.rowAccessoryButton().then($button => { + if ($button.length > 0) { + cy.log('[STEP 11.1] Found row accessory button with data-testid'); + cy.wrap($button.first()).click({ force: true }); + } else { + cy.log('[STEP 11.1] Looking for drag icon by class'); + cy.get('div[class*="cursor-pointer"]').first().click({ force: true }); + } + }); + + cy.wait(1000); + + // Now the dropdown menu should be open + cy.log('[STEP 12] Looking for delete option in dropdown menu'); + + // Wait for the dropdown menu to be visible + cy.get('[role="menu"], [data-slot="dropdown-menu-content"]').should('be.visible'); + + // Click on the delete option + RowControlsSelectors.rowMenuDelete().then($delete => { + if ($delete.length > 0) { + cy.log('[STEP 12.1] Found delete menu item by data-testid'); + cy.wrap($delete).click(); + } else { + cy.log('[STEP 12.1] Looking for delete by text'); + // Look for delete option by text + cy.get('[role="menuitem"]').contains(/delete/i).click(); + } + }); + + cy.wait(1000); + + // Handle the confirmation dialog + cy.log('[STEP 13] Handling deletion confirmation dialog'); + + // Click the confirm button + RowControlsSelectors.deleteRowConfirmButton().then($confirm => { + if ($confirm.length > 0) { + cy.log('[STEP 13.1] Found delete confirmation button by data-testid'); + cy.wrap($confirm).click(); + } else { + cy.log('[STEP 13.1] Looking for delete confirmation by button text'); + // Look for a button with text "Delete" or similar + cy.get('button').contains(/delete/i).click(); + } + }); + + cy.wait(2000); + + // Verify the row was deleted + cy.log('[STEP 14] Verifying row was deleted'); + + // Check that we now have fewer rows + DatabaseGridSelectors.rows().should('have.length', initialRowCount - 1); + + // Verify the content is gone + cy.log('[STEP 15] Verifying deleted content is gone'); + DatabaseGridSelectors.cells().then($allCells => { + let foundContent = false; + + $allCells.each((index, cell) => { + const text = Cypress.$(cell).text(); + if (text.includes(testContent)) { + foundContent = true; + cy.log(`[STEP 15.1] ERROR: Found deleted content at index ${index}`); + } + }); + + // The content should be gone + expect(foundContent).to.be.false; + }); + + cy.log('[STEP 16] Row deletion test completed successfully'); + }); + }); + }); +}); \ No newline at end of file diff --git a/cypress/e2e/database/row-duplication.cy.ts b/cypress/e2e/database/row-duplication.cy.ts new file mode 100644 index 00000000..56129e5d --- /dev/null +++ b/cypress/e2e/database/row-duplication.cy.ts @@ -0,0 +1,164 @@ +import { v4 as uuidv4 } from 'uuid'; +import { AuthTestUtils } from '../../support/auth-utils'; +import { + AddPageSelectors, + DatabaseGridSelectors, + RowControlsSelectors, + waitForReactUpdate +} from '../../support/selectors'; + +describe('Database Row Duplication', () => { + 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')) { + return false; + } + return true; + }); + + cy.viewport(1280, 720); + }); + + it('should create a new grid, add content to first row, and duplicate it', () => { + const testEmail = generateRandomEmail(); + const testContent = `Test Content ${Date.now()}`; + + cy.log(`[TEST START] Testing row duplication - Test email: ${testEmail}`); + + // Login + cy.log('[STEP 1] Visiting login page'); + cy.visit('/login', { failOnStatusCode: false }); + cy.wait(2000); + + const authUtils = new AuthTestUtils(); + cy.log('[STEP 2] Starting authentication'); + authUtils.signInWithTestUrl(testEmail).then(() => { + cy.log('[STEP 3] Authentication successful'); + cy.url({ timeout: 30000 }).should('include', '/app'); + cy.wait(3000); + + // Create a new grid + cy.log('[STEP 4] Creating new grid'); + AddPageSelectors.inlineAddButton().first().should('be.visible').click(); + waitForReactUpdate(1000); + AddPageSelectors.addGridButton().should('be.visible').click(); + cy.wait(8000); + + // Verify grid exists + cy.log('[STEP 5] Verifying grid exists'); + DatabaseGridSelectors.grid().should('exist'); + + // Get cells and edit the first one + cy.log('[STEP 6] Getting cells and editing first cell'); + DatabaseGridSelectors.cells().should('exist'); + + DatabaseGridSelectors.cells().then($cells => { + cy.log(`[STEP 7] Found ${$cells.length} cells`); + + // Click first cell and type content + cy.wrap($cells.first()).click(); + waitForReactUpdate(500); + cy.focused().type(testContent); + cy.focused().type('{enter}'); + waitForReactUpdate(1000); + + // Verify data was entered + cy.log('[STEP 8] Verifying content was added'); + cy.wrap($cells.first()).should('contain.text', testContent); + }); + + // Now hover over the first row to show controls + cy.log('[STEP 9] Hovering over first row to show controls'); + + // Get the first row and hover over it + DatabaseGridSelectors.firstRow() + .parent() + .parent() + .trigger('mouseenter', { force: true }) + .trigger('mouseover', { force: true }); + + cy.wait(1000); + + // Click on the drag icon/row accessory button + cy.log('[STEP 10] Looking for and clicking row accessory button'); + + // The drag icon should be visible after hovering + // Try to find it by the data-testid we added or by its class + RowControlsSelectors.rowAccessoryButton().then($button => { + if ($button.length > 0) { + cy.log('[STEP 10.1] Found row accessory button with data-testid'); + cy.wrap($button.first()).click({ force: true }); + } else { + cy.log('[STEP 10.1] Looking for drag icon by class'); + // Try to find the drag icon button + cy.get('div[class*="cursor-pointer"]').first().click({ force: true }); + } + }); + + cy.wait(1000); + + // Now the dropdown menu should be open + cy.log('[STEP 11] Looking for duplicate option in dropdown menu'); + + // Wait for the dropdown menu to be visible + cy.get('[role="menu"], [data-slot="dropdown-menu-content"]').should('be.visible'); + + // Click on the duplicate option + cy.get('[role="menuitem"]').then($items => { + let found = false; + $items.each((index, item) => { + const text = Cypress.$(item).text(); + if (text.includes('Duplicate') || text.includes('duplicate')) { + cy.log('[STEP 11.1] Found duplicate menu item by text'); + cy.wrap(item).click(); + found = true; + return false; + } + }); + + if (!found) { + // Try clicking by the data-testid we added + RowControlsSelectors.rowMenuDuplicate().then($duplicate => { + if ($duplicate.length > 0) { + cy.log('[STEP 11.2] Found duplicate menu item by data-testid'); + cy.wrap($duplicate).click(); + } else { + // Fallback: click the third menu item (typically duplicate is after insert above/below) + cy.log('[STEP 11.3] Clicking third menu item as fallback'); + cy.get('[role="menuitem"]').eq(2).click(); + } + }); + } + }); + + cy.wait(2000); + + // Verify the row was duplicated + cy.log('[STEP 12] Verifying row was duplicated'); + DatabaseGridSelectors.rows().should('have.length.at.least', 2); + + // Get all cells again and verify the duplicated content + cy.log('[STEP 13] Verifying duplicated content'); + DatabaseGridSelectors.cells().then($allCells => { + // The duplicated row should have the same content + // Find cells that contain our test content + let contentCount = 0; + $allCells.each((index, cell) => { + if (Cypress.$(cell).text().includes(testContent)) { + contentCount++; + } + }); + + cy.log(`[STEP 14] Found ${contentCount} cells with test content`); + // We should have at least 2 cells with the same content (original and duplicate) + expect(contentCount).to.be.at.least(2); + }); + + cy.log('[STEP 15] Row duplication test completed successfully'); + }); + }); +}); \ No newline at end of file diff --git a/cypress/e2e/database/row-insertion.cy.ts b/cypress/e2e/database/row-insertion.cy.ts new file mode 100644 index 00000000..a682acc7 --- /dev/null +++ b/cypress/e2e/database/row-insertion.cy.ts @@ -0,0 +1,238 @@ +import { v4 as uuidv4 } from 'uuid'; +import { AuthTestUtils } from '../../support/auth-utils'; +import { + AddPageSelectors, + DatabaseGridSelectors, + RowControlsSelectors, + waitForReactUpdate +} from '../../support/selectors'; + +describe('Database Row Insertion', () => { + 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')) { + return false; + } + return true; + }); + + cy.viewport(1280, 720); + }); + + it('should insert rows above and below existing row', () => { + const testEmail = generateRandomEmail(); + const originalContent = `Original Row ${Date.now()}`; + const aboveContent = `Above Row ${Date.now()}`; + const belowContent = `Below Row ${Date.now()}`; + + cy.log(`[TEST START] Testing row insertion above and below - Test email: ${testEmail}`); + + // Login + cy.log('[STEP 1] Visiting login page'); + cy.visit('/login', { failOnStatusCode: false }); + cy.wait(2000); + + const authUtils = new AuthTestUtils(); + cy.log('[STEP 2] Starting authentication'); + authUtils.signInWithTestUrl(testEmail).then(() => { + cy.log('[STEP 3] Authentication successful'); + cy.url({ timeout: 30000 }).should('include', '/app'); + cy.wait(3000); + + // Create a new grid + cy.log('[STEP 4] Creating new grid'); + AddPageSelectors.inlineAddButton().first().should('be.visible').click(); + waitForReactUpdate(1000); + AddPageSelectors.addGridButton().should('be.visible').click(); + cy.wait(8000); + + // Verify grid exists + cy.log('[STEP 5] Verifying grid exists'); + DatabaseGridSelectors.grid().should('exist'); + + // Get cells and edit the first one + cy.log('[STEP 6] Getting cells and editing first cell'); + DatabaseGridSelectors.cells().should('exist'); + + DatabaseGridSelectors.cells().then($cells => { + cy.log(`[STEP 7] Found ${$cells.length} cells`); + + // Click first cell and type content + cy.wrap($cells.first()).click(); + waitForReactUpdate(500); + cy.focused().type(originalContent); + cy.focused().type('{enter}'); + waitForReactUpdate(1000); + + // Verify data was entered + cy.log('[STEP 8] Verifying content was added'); + cy.wrap($cells.first()).should('contain.text', originalContent); + }); + + // Get initial row count + cy.log('[STEP 9] Getting initial row count'); + DatabaseGridSelectors.rows().then($rows => { + const initialRowCount = $rows.length; + cy.log(`[STEP 9.1] Initial row count: ${initialRowCount}`); + + // Insert a row above + cy.log('[STEP 10] Inserting row above'); + + // Get the first row and hover over it + DatabaseGridSelectors.firstRow() + .parent() + .parent() + .trigger('mouseenter', { force: true }) + .trigger('mouseover', { force: true }); + + cy.wait(1000); + + // Click on the drag icon/row accessory button + cy.log('[STEP 11] Looking for and clicking row accessory button'); + + RowControlsSelectors.rowAccessoryButton().then($button => { + if ($button.length > 0) { + cy.log('[STEP 11.1] Found row accessory button with data-testid'); + cy.wrap($button.first()).click({ force: true }); + } else { + cy.log('[STEP 11.1] Looking for drag icon by class'); + cy.get('div[class*="cursor-pointer"]').first().click({ force: true }); + } + }); + + cy.wait(1000); + + // Now the dropdown menu should be open + cy.log('[STEP 12] Looking for insert above option in dropdown menu'); + + // Wait for the dropdown menu to be visible + cy.get('[role="menu"], [data-slot="dropdown-menu-content"]').should('be.visible'); + + // Click on the insert above option - using data-testid first + RowControlsSelectors.rowMenuInsertAbove().then($insertAbove => { + if ($insertAbove.length > 0) { + cy.log('[STEP 12.1] Found insert above menu item by data-testid'); + cy.wrap($insertAbove).click(); + } else { + cy.log('[STEP 12.1] Looking for insert above by position'); + // First menu item is typically insert above + cy.get('[role="menuitem"]').first().click(); + } + }); + + cy.wait(2000); + + // Verify a new row was added + cy.log('[STEP 13] Verifying row was inserted above'); + DatabaseGridSelectors.rows().should('have.length', initialRowCount + 1); + + // Add content to the newly inserted row (which should be the first row now) + cy.log('[STEP 14] Adding content to newly inserted row above'); + DatabaseGridSelectors.cells().first().click(); + waitForReactUpdate(500); + cy.focused().type(aboveContent); + cy.focused().type('{enter}'); + waitForReactUpdate(1000); + + // Now insert a row below the original row (which is now the second row) + cy.log('[STEP 15] Inserting row below original row'); + + // Get the second row and hover over it + DatabaseGridSelectors.rows().eq(1) + .parent() + .parent() + .trigger('mouseenter', { force: true }) + .trigger('mouseover', { force: true }); + + cy.wait(1000); + + // Click on the drag icon/row accessory button again + cy.log('[STEP 16] Looking for and clicking row accessory button for second row'); + + // The row controls should appear for the second row after hovering + RowControlsSelectors.rowAccessoryButton().then($buttons => { + if ($buttons.length > 0) { + cy.log('[STEP 16.1] Found row accessory button'); + // Click the last visible button (should be for the hovered row) + cy.wrap($buttons.last()).click({ force: true }); + } else { + cy.log('[STEP 16.1] Looking for drag icon by class'); + cy.get('div[class*="cursor-pointer"]').last().click({ force: true }); + } + }); + + cy.wait(1000); + + // Now the dropdown menu should be open + cy.log('[STEP 17] Looking for insert below option in dropdown menu'); + + // Wait for the dropdown menu to be visible + cy.get('[role="menu"], [data-slot="dropdown-menu-content"]').should('be.visible'); + + // Click on the insert below option - using data-testid first + RowControlsSelectors.rowMenuInsertBelow().then($insertBelow => { + if ($insertBelow.length > 0) { + cy.log('[STEP 17.1] Found insert below menu item by data-testid'); + cy.wrap($insertBelow).click(); + } else { + cy.log('[STEP 17.1] Looking for insert below by position'); + // Second menu item is typically insert below + cy.get('[role="menuitem"]').eq(1).click(); + } + }); + + cy.wait(2000); + + // Verify another new row was added + cy.log('[STEP 18] Verifying row was inserted below'); + DatabaseGridSelectors.rows().should('have.length', initialRowCount + 2); + + // Add content to the newly inserted row below (should be the third row) + cy.log('[STEP 19] Adding content to newly inserted row below'); + DatabaseGridSelectors.rows().eq(2).within(() => { + DatabaseGridSelectors.cells().first().click(); + }); + waitForReactUpdate(500); + cy.focused().type(belowContent); + cy.focused().type('{enter}'); + waitForReactUpdate(1000); + + // Final verification - check all cells have the correct content + cy.log('[STEP 20] Final verification of content'); + DatabaseGridSelectors.cells().then($allCells => { + // Find cells that contain our test content + let foundAbove = false; + let foundOriginal = false; + let foundBelow = false; + + $allCells.each((index, cell) => { + const text = Cypress.$(cell).text(); + if (text.includes(aboveContent)) { + foundAbove = true; + cy.log(`[STEP 20.1] Found above content at index ${index}`); + } + if (text.includes(originalContent)) { + foundOriginal = true; + cy.log(`[STEP 20.2] Found original content at index ${index}`); + } + if (text.includes(belowContent)) { + foundBelow = true; + cy.log(`[STEP 20.3] Found below content at index ${index}`); + } + }); + + // Verify all content was found + expect(foundAbove).to.be.true; + expect(foundOriginal).to.be.true; + expect(foundBelow).to.be.true; + }); + + cy.log('[STEP 21] Row insertion test completed successfully'); + }); + }); + }); +}); \ No newline at end of file diff --git a/cypress/support/selectors.ts b/cypress/support/selectors.ts index a5d5e089..b03aefb3 100644 --- a/cypress/support/selectors.ts +++ b/cypress/support/selectors.ts @@ -363,6 +363,23 @@ export const FieldType = { FileMedia: 14 }; +/** + * Database Row Controls selectors + */ +export const RowControlsSelectors = { + // Row accessory button (appears on hover) + rowAccessoryButton: () => cy.get(byTestId('row-accessory-button')), + + // Row menu items + rowMenuDuplicate: () => cy.get(byTestId('row-menu-duplicate')), + rowMenuInsertAbove: () => cy.get(byTestId('row-menu-insert-above')), + rowMenuInsertBelow: () => cy.get(byTestId('row-menu-insert-below')), + rowMenuDelete: () => cy.get(byTestId('row-menu-delete')), + + // Delete confirmation + deleteRowConfirmButton: () => cy.get(byTestId('delete-row-confirm-button')), +}; + export function waitForReactUpdate(ms: number = 500) { return cy.wait(ms); } \ No newline at end of file diff --git a/src/components/database/components/database-row/DeleteRowConfirm.tsx b/src/components/database/components/database-row/DeleteRowConfirm.tsx index 2bf687d2..ffc474ed 100644 --- a/src/components/database/components/database-row/DeleteRowConfirm.tsx +++ b/src/components/database/components/database-row/DeleteRowConfirm.tsx @@ -68,7 +68,7 @@ export function DeleteRowConfirm({ - diff --git a/src/components/database/components/grid/controls/HoverControls.tsx b/src/components/database/components/grid/controls/HoverControls.tsx index e6c7c234..72a084ae 100644 --- a/src/components/database/components/grid/controls/HoverControls.tsx +++ b/src/components/database/components/grid/controls/HoverControls.tsx @@ -98,6 +98,7 @@ export function HoverControls ({ rowId, dragHandleRef }: {
{ setMenuOpen(true); }} diff --git a/src/components/database/components/grid/controls/RowMenu.tsx b/src/components/database/components/grid/controls/RowMenu.tsx index dc682cc4..fee21e82 100644 --- a/src/components/database/components/grid/controls/RowMenu.tsx +++ b/src/components/database/components/grid/controls/RowMenu.tsx @@ -77,6 +77,13 @@ function RowMenu({ rowId, onClose }: { rowId: string; onClose: () => void }) { {actions.map((item) => ( { e.preventDefault(); item.onSelect();