mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2026-03-13 10:00:26 +08:00
502 lines
21 KiB
TypeScript
502 lines
21 KiB
TypeScript
import { v4 as uuidv4 } from 'uuid';
|
|
import { AuthTestUtils } from '../../../support/auth-utils';
|
|
import { getSlashMenuItemName } from '../../../support/i18n-constants';
|
|
import {
|
|
AddPageSelectors,
|
|
byTestId,
|
|
EditorSelectors,
|
|
ModalSelectors,
|
|
PageSelectors,
|
|
SlashCommandSelectors,
|
|
SpaceSelectors,
|
|
waitForReactUpdate,
|
|
} from '../../../support/selectors';
|
|
|
|
describe('Embedded Database View Isolation', () => {
|
|
const generateRandomEmail = () => `${uuidv4()}@appflowy.io`;
|
|
const dbName = 'New Database';
|
|
const docName = `Doc ${uuidv4()}`;
|
|
const spaceName = 'General'; // Default space name
|
|
|
|
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('useAppHandlers must be used within') ||
|
|
err.message.includes('Cannot resolve a DOM node from Slate') ||
|
|
err.message.includes('ResizeObserver loop')
|
|
) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
cy.viewport(1280, 720);
|
|
});
|
|
|
|
/**
|
|
* Expand a space in the sidebar by clicking on it
|
|
*/
|
|
function expandSpaceInSidebar(spaceNameToExpand: string) {
|
|
cy.task('log', `[ACTION] Expanding space "${spaceNameToExpand}" in sidebar`);
|
|
|
|
// Check if space is already expanded
|
|
SpaceSelectors.itemByName(spaceNameToExpand, { timeout: 30000 }).then(($space) => {
|
|
const expandedIndicator = $space.find('[data-testid="space-expanded"]');
|
|
const isExpanded = expandedIndicator.attr('data-expanded') === 'true';
|
|
|
|
if (!isExpanded) {
|
|
cy.task('log', `[ACTION] Space "${spaceNameToExpand}" is collapsed, clicking to expand`);
|
|
// Click on the space name to expand it
|
|
SpaceSelectors.itemByName(spaceNameToExpand, { timeout: 30000 })
|
|
.find(byTestId('space-name'))
|
|
.click({ force: true });
|
|
waitForReactUpdate(500);
|
|
} else {
|
|
cy.task('log', `[ACTION] Space "${spaceNameToExpand}" is already expanded`);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Expand a page in the sidebar to show its children
|
|
*/
|
|
function expandPageInSidebar(pageName: string) {
|
|
cy.task('log', `[ACTION] Expanding page "${pageName}" in sidebar`);
|
|
PageSelectors.itemByName(pageName, { timeout: 30000 }).should('exist');
|
|
PageSelectors.itemByName(pageName, { timeout: 30000 })
|
|
.find(byTestId('outline-toggle-collapse'))
|
|
.then(($collapse) => {
|
|
if ($collapse.length > 0) return;
|
|
|
|
PageSelectors.itemByName(pageName, { timeout: 30000 })
|
|
.find(byTestId('outline-toggle-expand'))
|
|
.should('exist')
|
|
.first()
|
|
.click({ force: true });
|
|
waitForReactUpdate(500);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if a page has the expand toggle (indicating it has or can have children)
|
|
*/
|
|
function assertPageHasExpandToggle(pageName: string) {
|
|
cy.task('log', `[ASSERT] Checking "${pageName}" has expand toggle in sidebar`);
|
|
PageSelectors.itemByName(pageName, { timeout: 30000 }).within(() => {
|
|
cy.get(`${byTestId('outline-toggle-expand')}, ${byTestId('outline-toggle-collapse')}`, { timeout: 30000 }).should(
|
|
'exist'
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Assert that a page has NO expand toggle (no children)
|
|
*/
|
|
function assertPageHasNoExpandToggle(pageName: string) {
|
|
cy.task('log', `[ASSERT] Checking "${pageName}" has NO expand toggle in sidebar`);
|
|
PageSelectors.itemByName(pageName).then(($pageItem) => {
|
|
const hasExpandToggle =
|
|
$pageItem.find('[data-testid="outline-toggle-expand"], [data-testid="outline-toggle-collapse"]').length > 0;
|
|
cy.task('log', `[ASSERT] "${pageName}" has expand toggle: ${hasExpandToggle}`);
|
|
expect(hasExpandToggle).to.equal(false, `"${pageName}" should NOT have expand toggle (no children)`);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Assert that a page has NO children in the sidebar
|
|
* Children are detected by the absence of expand/collapse toggles
|
|
*/
|
|
function assertPageHasNoChildren(pageName: string) {
|
|
cy.task('log', `[ASSERT] Checking "${pageName}" has NO children in sidebar`);
|
|
PageSelectors.itemByName(pageName).then(($pageItem) => {
|
|
// A page with children will have either an expand or collapse toggle
|
|
const hasExpandToggle = $pageItem.find('[data-testid="outline-toggle-expand"]').length > 0;
|
|
const hasCollapseToggle = $pageItem.find('[data-testid="outline-toggle-collapse"]').length > 0;
|
|
const hasChildren = hasExpandToggle || hasCollapseToggle;
|
|
|
|
cy.task('log', `[ASSERT] "${pageName}" hasExpandToggle: ${hasExpandToggle}, hasCollapseToggle: ${hasCollapseToggle}`);
|
|
|
|
// Also log nested page-item count for debugging
|
|
const nestedPageItems = $pageItem.find('[data-testid="page-item"]');
|
|
cy.task('log', `[DEBUG] Nested page-item count: ${nestedPageItems.length}`);
|
|
|
|
expect(hasChildren).to.equal(false, `"${pageName}" should NOT have children toggles (no children)`);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Assert that a page HAS children in the sidebar (after expanding)
|
|
*/
|
|
function assertPageHasChildren(pageName: string, expectedMinCount = 1) {
|
|
cy.task('log', `[ASSERT] Checking "${pageName}" HAS children in sidebar`);
|
|
|
|
// First log all page names in sidebar for debugging
|
|
PageSelectors.names().then(($names) => {
|
|
const names = Array.from($names).map((el) => Cypress.$(el).text().trim());
|
|
cy.task('log', `[DEBUG] All page names in sidebar: ${JSON.stringify(names)}`);
|
|
});
|
|
|
|
PageSelectors.itemByName(pageName).then(($pageItem) => {
|
|
const childCount = $pageItem.find('[data-testid="page-item"]').length;
|
|
cy.task('log', `[ASSERT] "${pageName}" has ${childCount} children`);
|
|
|
|
// Log the HTML structure for debugging (use [0] to get the raw DOM element)
|
|
cy.task('log', `[DEBUG] Page item HTML length: ${$pageItem[0]?.innerHTML?.length || 0}`);
|
|
|
|
expect(childCount).to.be.at.least(
|
|
expectedMinCount,
|
|
`"${pageName}" should have at least ${expectedMinCount} children in sidebar`
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Assert that a child view exists under a parent in the sidebar
|
|
*/
|
|
function assertChildViewExists(parentName: string, childNameContains: string) {
|
|
cy.task('log', `[ASSERT] Checking "${parentName}" has child containing "${childNameContains}"`);
|
|
PageSelectors.itemByName(parentName).within(() => {
|
|
cy.get('[data-testid="page-name"]').contains(childNameContains).should('exist');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the number of descendant page items under a page in the sidebar.
|
|
* This counts all nested children rendered within the page item.
|
|
*/
|
|
function getDescendantPageItemCount(pageName: string) {
|
|
return PageSelectors.itemByName(pageName).then(($pageItem) => {
|
|
return $pageItem.find('[data-testid="page-item"]').length;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Assert that none of the descendant views under a page contain specific text.
|
|
* Useful to ensure embedded views (e.g. "View of ...") do NOT appear under a standalone database container.
|
|
*/
|
|
function assertNoChildViewContains(parentName: string, forbiddenText: string) {
|
|
cy.task('log', `[ASSERT] Checking "${parentName}" has NO child containing "${forbiddenText}"`);
|
|
PageSelectors.itemByName(parentName).within(() => {
|
|
cy.get('[data-testid="page-name"]').should('not.contain.text', forbiddenText);
|
|
});
|
|
}
|
|
|
|
// Skip: Document unexpectedly has children immediately after creation.
|
|
// The test flow creates a standalone Grid, then a Document, but the Document ends up
|
|
// with a Grid child before any embedding. This may be a bug in page creation flow
|
|
// or test setup issue with the new-page-modal handling.
|
|
it.skip('should show embedded view as document child, NOT original database child', () => {
|
|
const testEmail = generateRandomEmail();
|
|
|
|
cy.task('log', `[TEST START] Testing embedded view appears as document child - 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();
|
|
authUtils.signInWithTestUrl(testEmail).then(() => {
|
|
cy.task('log', '[STEP 2] Authentication successful');
|
|
cy.url({ timeout: 30000 }).should('include', '/app');
|
|
cy.wait(3000);
|
|
|
|
// Step 3: Create a standalone Grid database
|
|
cy.task('log', '[STEP 3] Creating standalone Grid database');
|
|
AddPageSelectors.inlineAddButton().first().click({ force: true });
|
|
waitForReactUpdate(1000);
|
|
AddPageSelectors.addGridButton().should('be.visible').click({ force: true });
|
|
cy.wait(5000);
|
|
|
|
// Step 4: Capture original database children (database container)
|
|
cy.task('log', '[STEP 4] Capturing original database children');
|
|
expandSpaceInSidebar(spaceName);
|
|
waitForReactUpdate(1000);
|
|
|
|
getDescendantPageItemCount(dbName).then((count) => {
|
|
cy.task('log', `[STEP 4.1] Original database descendant view count: ${count}`);
|
|
expect(count).to.be.at.least(1);
|
|
cy.wrap(count).as('originalDbChildCount');
|
|
});
|
|
assertNoChildViewContains(dbName, 'View of');
|
|
|
|
// Step 5: Create a new Document page
|
|
cy.task('log', '[STEP 5] Creating new document page');
|
|
|
|
// Close any open modals first
|
|
cy.get('body').then(($body) => {
|
|
if ($body.find('[role="dialog"]').length > 0) {
|
|
cy.get('body').type('{esc}');
|
|
cy.wait(500);
|
|
}
|
|
});
|
|
|
|
AddPageSelectors.inlineAddButton().first().click({ force: true });
|
|
waitForReactUpdate(1000);
|
|
cy.get('[role="menuitem"]').first().click({ force: true });
|
|
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 5.1] Handling new page modal');
|
|
ModalSelectors.newPageModal()
|
|
.should('be.visible')
|
|
.within(() => {
|
|
ModalSelectors.spaceItemInModal().first().click({ force: true });
|
|
waitForReactUpdate(500);
|
|
cy.contains('button', 'Add').click({ force: true });
|
|
});
|
|
}
|
|
});
|
|
|
|
cy.wait(3000);
|
|
|
|
// Step 5.2: Give the document a unique title to avoid matching other "Untitled" pages in the sidebar
|
|
cy.task('log', `[STEP 5.2] Setting document title to "${docName}"`);
|
|
PageSelectors.titleInput({ timeout: 30000 })
|
|
.first()
|
|
.should('be.visible')
|
|
.click({ force: true })
|
|
.clear({ force: true })
|
|
.type(docName, { force: true })
|
|
.type('{enter}', { force: true });
|
|
waitForReactUpdate(1000);
|
|
expandSpaceInSidebar(spaceName);
|
|
PageSelectors.nameContaining(docName, { timeout: 30000 }).should('exist');
|
|
|
|
// Step 6: Verify document initially has NO children
|
|
cy.task('log', '[STEP 6] Verifying document initially has NO children');
|
|
assertPageHasNoChildren(docName);
|
|
|
|
// Step 7: Insert embedded database via slash menu
|
|
cy.task('log', '[STEP 7] Inserting embedded database via slash menu');
|
|
EditorSelectors.firstEditor().should('exist', { timeout: 15000 });
|
|
EditorSelectors.firstEditor().click().type('/');
|
|
waitForReactUpdate(500);
|
|
|
|
cy.task('log', '[STEP 7.1] Selecting Linked Grid option');
|
|
SlashCommandSelectors.slashPanel()
|
|
.should('be.visible', { timeout: 5000 })
|
|
.within(() => {
|
|
SlashCommandSelectors.slashMenuItem(getSlashMenuItemName('linkedGrid')).first().click({ force: true });
|
|
});
|
|
|
|
waitForReactUpdate(1000);
|
|
|
|
cy.task('log', `[STEP 7.2] Selecting database: ${dbName}`);
|
|
SlashCommandSelectors.selectDatabase(dbName);
|
|
waitForReactUpdate(3000);
|
|
|
|
// Step 8: Verify embedded database was created successfully
|
|
cy.task('log', '[STEP 8] Verifying embedded database was created successfully');
|
|
|
|
// 8.1: Verify no error notification appeared
|
|
cy.task('log', '[STEP 8.1] Checking no error notification appeared');
|
|
cy.get('[data-sonner-toast][data-type="error"]', { timeout: 2000 }).should('not.exist');
|
|
|
|
// 8.2: Wait for embedded database container to appear
|
|
cy.task('log', '[STEP 8.2] Waiting for embedded database container');
|
|
cy.get('[class*="appflowy-database"]', { timeout: 15000 }).should('exist').last().should('be.visible');
|
|
|
|
// 8.3: Verify the embedded view tab shows "View of" prefix (indicates successful creation)
|
|
cy.task('log', '[STEP 8.3] Verifying embedded view has correct name with "View of" prefix');
|
|
cy.get('[role="tab"]', { timeout: 10000 }).should('be.visible').and('contain.text', 'View of');
|
|
|
|
// 8.4: Verify the grid structure is visible (columns exist)
|
|
cy.task('log', '[STEP 8.4] Verifying grid structure is visible');
|
|
cy.get('[class*="appflowy-database"]')
|
|
.last()
|
|
.within(() => {
|
|
// Check for column headers or grid structure
|
|
cy.get('button').should('have.length.at.least', 1);
|
|
});
|
|
|
|
cy.task('log', '[STEP 8.5] Embedded database successfully created and visible');
|
|
|
|
// Step 9: KEY SIDEBAR VERIFICATION - Document should have subview, Database should NOT
|
|
cy.task('log', '[STEP 9] Verifying sidebar structure after embedding');
|
|
waitForReactUpdate(2000);
|
|
|
|
// 9.0: First expand the space to see pages in sidebar
|
|
cy.task('log', '[STEP 9.0] Expanding space to see pages in sidebar');
|
|
expandSpaceInSidebar(spaceName);
|
|
waitForReactUpdate(1000);
|
|
|
|
// 9.1: Verify the document now has an expand toggle (indicating it has children)
|
|
cy.task('log', '[STEP 9.1] Verifying document has expand toggle (has children)');
|
|
assertPageHasExpandToggle(docName);
|
|
|
|
// 9.2: Expand the document to see its children
|
|
cy.task('log', '[STEP 9.2] Expanding document to see children');
|
|
expandPageInSidebar(docName);
|
|
waitForReactUpdate(1000);
|
|
|
|
// 9.3: Verify the document has children (the embedded view)
|
|
cy.task('log', '[STEP 9.3] Verifying document has children (embedded view)');
|
|
assertPageHasChildren(docName);
|
|
|
|
// 9.4: Verify the embedded view shows "View of" prefix in sidebar
|
|
cy.task('log', '[STEP 9.4] Verifying embedded view name contains "View of"');
|
|
assertChildViewExists(docName, 'View of');
|
|
|
|
// Step 10: Verify original database STILL has NO children
|
|
// This is the KEY assertion - embedded views should NOT appear as children of the original database
|
|
cy.task('log', '[STEP 10] Verifying original database did NOT gain embedded children');
|
|
expandSpaceInSidebar(spaceName);
|
|
waitForReactUpdate(500);
|
|
|
|
cy.get<number>('@originalDbChildCount').then((initialCount) => {
|
|
getDescendantPageItemCount(dbName).then((count) => {
|
|
expect(count).to.equal(initialCount);
|
|
});
|
|
});
|
|
assertNoChildViewContains(dbName, 'View of');
|
|
|
|
// Step 11: Create a SECOND view in the embedded database (using + button)
|
|
// This view will also be a child of the document, not the original database
|
|
cy.task('log', '[STEP 11] Creating second view in embedded database using + button');
|
|
|
|
// Navigate back to the document first
|
|
cy.task('log', '[STEP 11.1] Navigating back to document');
|
|
PageSelectors.nameContaining(docName).first().click({ force: true });
|
|
waitForReactUpdate(3000);
|
|
|
|
// Get the embedded database container
|
|
cy.task('log', '[STEP 11.2] Finding embedded database in document');
|
|
cy.get('[class*="appflowy-database"]', { timeout: 10000 }).should('exist').last().as('embeddedDBInDoc');
|
|
|
|
// Click the + button to add a new view
|
|
cy.task('log', '[STEP 11.3] Clicking + button to add second view');
|
|
cy.get('@embeddedDBInDoc').find('[data-testid="add-view-button"]').scrollIntoView().click({ force: true });
|
|
|
|
waitForReactUpdate(500);
|
|
|
|
// Select Board view type from dropdown - wait for menu and click Board option
|
|
cy.task('log', '[STEP 11.4] Selecting Board view type');
|
|
|
|
// Wait for the menu to appear and find the Board option within the visible dropdown
|
|
cy.get('[role="menu"], [role="listbox"], .MuiMenu-list, .MuiPopover-paper', { timeout: 5000 })
|
|
.should('be.visible')
|
|
.contains('Board')
|
|
.click({ force: true });
|
|
|
|
// Wait for view to be created
|
|
waitForReactUpdate(3000);
|
|
|
|
// Step 12: Verify second view was created (now 2 tabs in the embedded database)
|
|
cy.task('log', '[STEP 12] Verifying second view was created (2 tabs)');
|
|
// Re-query the embedded database block to avoid stale alias issues after view creation.
|
|
cy.get('[class*="appflowy-database"]', { timeout: 10000 }).should('exist').last().as('embeddedDBInDocFresh');
|
|
cy.get('@embeddedDBInDocFresh').within(() => {
|
|
cy.get('[data-testid^="view-tab-"]', { timeout: 10000 }).should('have.length', 2);
|
|
});
|
|
|
|
// Step 13: Verify document now has TWO children, database still has ZERO
|
|
cy.task('log', '[STEP 13] Verifying document has 2 children, database has 0');
|
|
waitForReactUpdate(2000);
|
|
|
|
// Expand space again if needed
|
|
expandSpaceInSidebar(spaceName);
|
|
waitForReactUpdate(500);
|
|
|
|
// Expand document to see children
|
|
cy.task('log', '[STEP 13.1] Expanding document to verify children');
|
|
PageSelectors.itemByName(docName).then(($docItem) => {
|
|
// Check if already expanded by looking for collapse toggle
|
|
const isExpanded = $docItem.find('[data-testid="outline-toggle-collapse"]').length > 0;
|
|
if (!isExpanded) {
|
|
expandPageInSidebar(docName);
|
|
waitForReactUpdate(500);
|
|
}
|
|
});
|
|
|
|
cy.task('log', '[STEP 13.2] Verifying document has 2 children');
|
|
assertPageHasChildren(docName, 2);
|
|
|
|
cy.task('log', '[STEP 13.3] Verifying database did NOT gain embedded children');
|
|
cy.get<number>('@originalDbChildCount').then((initialCount) => {
|
|
getDescendantPageItemCount(dbName).then((count) => {
|
|
expect(count).to.equal(initialCount);
|
|
});
|
|
});
|
|
assertNoChildViewContains(dbName, 'View of');
|
|
|
|
// Step 14: Navigate to the original database and create a NEW view directly in it
|
|
cy.task('log', '[STEP 14] Navigating to original database to create a direct view');
|
|
PageSelectors.nameContaining(dbName).first().click({ force: true });
|
|
waitForReactUpdate(3000);
|
|
|
|
// Step 15: Create a new view directly in the database using the + button
|
|
cy.task('log', '[STEP 15] Creating new view directly in database');
|
|
|
|
// Get the database view and find the add-view-button
|
|
cy.get('[class*="appflowy-database"]', { timeout: 10000 }).should('exist').first().as('standaloneDB');
|
|
|
|
cy.task('log', '[STEP 15.1] Clicking + button to add view');
|
|
cy.get('@standaloneDB').find('[data-testid="add-view-button"]').scrollIntoView().click({ force: true });
|
|
|
|
waitForReactUpdate(500);
|
|
|
|
// Select Board view type from dropdown - wait for menu and click Board option
|
|
cy.task('log', '[STEP 15.2] Selecting Board view type');
|
|
|
|
// Wait for the menu to appear and find the Board option within the visible dropdown
|
|
cy.get('[role="menu"], [role="listbox"], .MuiMenu-list, .MuiPopover-paper', { timeout: 5000 })
|
|
.should('be.visible')
|
|
.contains('Board')
|
|
.click({ force: true });
|
|
|
|
// Wait for view to be created
|
|
waitForReactUpdate(3000);
|
|
|
|
// Step 16: Verify database gained a direct child view (created in standalone DB)
|
|
cy.task('log', '[STEP 16] Verifying database gained a direct child view');
|
|
|
|
// Expand space again if needed
|
|
expandSpaceInSidebar(spaceName);
|
|
waitForReactUpdate(500);
|
|
|
|
// Database should now have an expand toggle
|
|
cy.task('log', '[STEP 16.1] Verifying database has expand toggle');
|
|
assertPageHasExpandToggle(dbName);
|
|
|
|
// Expand database to see its child
|
|
cy.task('log', '[STEP 16.2] Expanding database to see children');
|
|
expandPageInSidebar(dbName);
|
|
waitForReactUpdate(500);
|
|
|
|
// Verify database child count increased (directly created view appears under the database container)
|
|
cy.task('log', '[STEP 16.3] Verifying database child count increased');
|
|
cy.get<number>('@originalDbChildCount').then((initialCount) => {
|
|
getDescendantPageItemCount(dbName).then((count) => {
|
|
expect(count).to.be.greaterThan(initialCount);
|
|
});
|
|
});
|
|
assertChildViewExists(dbName, 'Board');
|
|
|
|
// Step 17: Verify document STILL has exactly 2 children (unchanged)
|
|
cy.task('log', '[STEP 17] Verifying document still has 2 children');
|
|
|
|
// Expand document if needed
|
|
PageSelectors.itemByName(docName).then(($docItem) => {
|
|
const isExpanded = $docItem.find('[data-testid="outline-toggle-collapse"]').length > 0;
|
|
if (!isExpanded) {
|
|
expandPageInSidebar(docName);
|
|
waitForReactUpdate(500);
|
|
}
|
|
});
|
|
|
|
assertPageHasChildren(docName, 2);
|
|
|
|
cy.task('log', '[TEST COMPLETE] Embedded view isolation verified:');
|
|
cy.task('log', ' - Document has 2 children (both embedded views)');
|
|
cy.task('log', ' - Database has 1 child (only the directly created view)');
|
|
cy.task('log', ' - Embedded views do NOT appear as children of their source database');
|
|
});
|
|
});
|
|
});
|