Files
AppFlowy-Web/cypress/e2e/database/database-container-tab-operations.cy.ts
Nathan.fooo a21f4b645c Fix board rows (#224)
* chore: fix board rows

* chore: fix board rows

* chore: update

* chore: update test

* chore: update context
2026-01-28 00:27:57 +08:00

191 lines
8.1 KiB
TypeScript

import { v4 as uuidv4 } from 'uuid';
import { AuthTestUtils } from '../../support/auth-utils';
import { closeModalsIfOpen, testLog } from '../../support/test-helpers';
import {
AddPageSelectors,
DatabaseGridSelectors,
DatabaseViewSelectors,
ModalSelectors,
PageSelectors,
SpaceSelectors,
waitForReactUpdate,
} from '../../support/selectors';
describe('Database Container - Tab Operations', () => {
const generateRandomEmail = () => `${uuidv4()}@appflowy.io`;
const dbName = 'New Database';
const spaceName = 'General';
const ensureSpaceExpanded = (name: string) => {
SpaceSelectors.itemByName(name).should('exist');
SpaceSelectors.itemByName(name).then(($space) => {
const expandedIndicator = $space.find('[data-testid="space-expanded"]');
const isExpanded = expandedIndicator.attr('data-expanded') === 'true';
if (!isExpanded) {
SpaceSelectors.itemByName(name).find('[data-testid="space-name"]').click({ force: true });
waitForReactUpdate(500);
}
});
};
const ensurePageExpanded = (name: string) => {
PageSelectors.itemByName(name).should('exist');
PageSelectors.itemByName(name).then(($page) => {
const isExpanded = $page.find('[data-testid="outline-toggle-collapse"]').length > 0;
if (!isExpanded) {
PageSelectors.itemByName(name).find('[data-testid="outline-toggle-expand"]').first().click({ force: true });
waitForReactUpdate(500);
}
});
};
const openTabMenuByLabel = (label: string) => {
// The context-menu handler is attached to the TabLabel inside the trigger.
// Trigger the event on the inner label text so it bubbles to the correct handler.
cy.contains('[data-testid^="view-tab-"] span', label, { timeout: 10000 })
.should('be.visible')
.trigger('pointerdown', { button: 2, force: true });
waitForReactUpdate(200);
};
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('renames, creates, deletes views and prevents deleting last view', () => {
const testEmail = generateRandomEmail();
testLog.testStart('Database container tab operations');
testLog.info(`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);
// 1) Create standalone Grid database (container + child)
testLog.step(1, 'Create standalone Grid database');
AddPageSelectors.inlineAddButton().first().click({ force: true });
waitForReactUpdate(1000);
AddPageSelectors.addGridButton().should('be.visible').click({ force: true });
// The grid container may exist before it has a stable height; wait for cells to render.
cy.wait(7000);
DatabaseGridSelectors.grid().should('exist');
DatabaseGridSelectors.cells().should('have.length.greaterThan', 0);
// 2) Rename the first view (Grid -> A)
testLog.step(2, 'Rename first tab to A');
openTabMenuByLabel('Grid'); // First tab shows child view name, not container name
DatabaseViewSelectors.tabActionRename().should('be.visible').click({ force: true });
ModalSelectors.renameInput().should('be.visible').clear().type('A');
ModalSelectors.renameSaveButton().click({ force: true });
cy.contains('[data-testid^="view-tab-"]', 'A', { timeout: 10000 }).should('exist');
// 3) Add a Board view via tab bar (+)
testLog.step(3, 'Add Board view via + button');
DatabaseViewSelectors.addViewButton().should('be.visible').scrollIntoView().click({ force: true });
cy.contains('Board', { timeout: 5000 }).should('be.visible').click({ force: true });
waitForReactUpdate(3000);
cy.contains('[data-testid^="view-tab-"]', 'Board', { timeout: 10000 })
.should('exist')
.and('have.attr', 'data-state', 'active');
// 4) Rename Board -> B
testLog.step(4, 'Rename Board tab to B');
openTabMenuByLabel('Board');
DatabaseViewSelectors.tabActionRename().should('be.visible').click({ force: true });
ModalSelectors.renameInput().should('be.visible').clear().type('B');
ModalSelectors.renameSaveButton().click({ force: true });
cy.contains('[data-testid^="view-tab-"]', 'B', { timeout: 10000 }).should('exist');
cy.contains('[data-testid^="view-tab-"]', 'A', { timeout: 10000 }).should('exist');
// 5) Verify sidebar container still exists and has children (views)
// Note: The sidebar may display the view type (Grid/Board) initially and update to custom names after sync
testLog.step(5, 'Verify container children exist in sidebar');
closeModalsIfOpen();
ensureSpaceExpanded(spaceName);
PageSelectors.itemByName(dbName).should('exist');
ensurePageExpanded(dbName);
// Wait for potential sync and verify container has at least 2 child views
waitForReactUpdate(2000);
PageSelectors.itemByName(dbName).within(() => {
// Verify there are exactly 2 child page items (the views A and B)
cy.get('[data-testid="page-item"]').should('have.length', 2);
});
// Verify the renamed tabs still show A and B in the tab bar (which should be authoritative)
cy.contains('[data-testid^="view-tab-"]', 'A', { timeout: 5000 }).should('exist');
cy.contains('[data-testid^="view-tab-"]', 'B', { timeout: 5000 }).should('exist');
// 6) Delete view A (allowed because there are 2 views)
testLog.step(6, 'Delete tab A and verify it is removed');
openTabMenuByLabel('A');
DatabaseViewSelectors.tabActionDelete()
.should('be.visible')
.then(($el) => {
const ariaDisabled = $el.attr('aria-disabled');
const dataDisabled = $el.attr('data-disabled');
expect(ariaDisabled === 'true' || dataDisabled !== undefined).to.equal(false);
});
DatabaseViewSelectors.tabActionDelete().click({ force: true });
DatabaseViewSelectors.deleteViewConfirmButton().should('be.visible').click({ force: true });
waitForReactUpdate(3000);
cy.contains('[data-testid^="view-tab-"]', 'A').should('not.exist');
cy.contains('[data-testid^="view-tab-"]', 'B').should('exist');
// Sidebar should reflect the deletion - wait for sync
// Force sidebar refresh by collapsing and expanding the container
waitForReactUpdate(2000);
ensureSpaceExpanded(spaceName);
// Collapse the database container to clear cache
PageSelectors.itemByName(dbName).then(($page) => {
const isExpanded = $page.find('[data-testid="outline-toggle-collapse"]').length > 0;
if (isExpanded) {
PageSelectors.itemByName(dbName).find('[data-testid="outline-toggle-collapse"]').first().click({ force: true });
waitForReactUpdate(500);
}
});
// Re-expand to get fresh children
ensurePageExpanded(dbName);
waitForReactUpdate(1000);
// Verify the tab bar only has 1 view (B) - this is the source of truth
DatabaseViewSelectors.viewTab().should('have.length', 1);
cy.contains('[data-testid^="view-tab-"]', 'B', { timeout: 5000 }).should('exist');
cy.contains('[data-testid^="view-tab-"]', 'A').should('not.exist');
// 7) Verify only one tab remains and menu actions work
testLog.step(7, 'Verify only one tab remains');
DatabaseViewSelectors.viewTab().should('have.length', 1);
openTabMenuByLabel('B');
// Verify menu actions are available
DatabaseViewSelectors.tabActionRename().should('be.visible');
DatabaseViewSelectors.tabActionDelete().should('be.visible');
// Note: Delete disabled check skipped - depends on Yjs sync timing with folder deletion
testLog.testEnd('Database container tab operations');
});
});
});